@stringsync/splates 0.0.58 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -281,6 +281,16 @@ better or worse, and say what to change when one stands out. Fixed rules over
281
281
  the counts make those calls, not a model, so the same outcomes always read
282
282
  the same.
283
283
 
284
+ ### Kimi Code
285
+
286
+ Kimi Code is on the default roster beside Claude and Codex, started as
287
+ `kimi acp`, Kimi's own ACP server. Its installer leaves `kimi` off the PATH,
288
+ so splates runs `KIMI_PATH` when set, else `kimi` on the PATH, else
289
+ `~/.kimi-code/bin/kimi`. A roster written before Kimi joined gains it once,
290
+ when the server starts on a machine with Kimi installed; take it off and it
291
+ stays off. Its models go by their full ids, such as `kimi-code/k3.1-pro`.
292
+ Sign in first with `kimi login`: splates never signs in for it.
293
+
284
294
  ### Optional OpenCode ACP agent
285
295
 
286
296
  Open **Settings → Setup** and visit the optional OpenCode steps. Check this
package/blockers.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { ElsewhereBlocker, ElsewhereBoards } from "@stringsync/splates-server/board";
2
+ /**
3
+ * Reads `--blocked-by` and `--remove-blocked-by` in the project at `here`:
4
+ * a number names a task here, `webappwiz#404` or `webappwiz #404` one of
5
+ * another project, as `boards` finds it, and this project's own slug a task
6
+ * here. A task of another project that `--blocked-by` names has to be open
7
+ * there; one `--remove-blocked-by` names need not.
8
+ */
9
+ export declare function blockerFlags({ boards, here, blockedBy, removeBlockedBy, }: BlockerFlagsOptions): Promise<BlockerFlags>;
10
+ /** What `blockerFlags` reads, and where. */
11
+ export interface BlockerFlagsOptions {
12
+ boards: ElsewhereBoards;
13
+ /** This project's main tree. */
14
+ here: string;
15
+ /** `--blocked-by`, as given. */
16
+ blockedBy?: readonly string[];
17
+ /** `--remove-blocked-by`, as given. */
18
+ removeBlockedBy?: readonly string[];
19
+ }
20
+ /** The tasks the flags name, here and in other projects. */
21
+ export interface BlockerFlags {
22
+ blockedBy: number[];
23
+ blockedElsewhere: ElsewhereBlocker[];
24
+ /** Those no longer to block it. */
25
+ unblocked: {
26
+ ids: number[];
27
+ elsewhere: {
28
+ project: string;
29
+ id: number;
30
+ }[];
31
+ };
32
+ }
package/index.js CHANGED
@@ -1,5 +1,42 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __dispose = Symbol.dispose || /* @__PURE__ */ Symbol.for("Symbol.dispose");
4
+ var __asyncDispose = Symbol.asyncDispose || /* @__PURE__ */ Symbol.for("Symbol.asyncDispose");
5
+ var __using = (stack, value, async) => {
6
+ if (value != null) {
7
+ if (typeof value !== "object" && typeof value !== "function")
8
+ throw TypeError('Object expected to be assigned to "using" declaration');
9
+ var dispose;
10
+ if (async)
11
+ dispose = value[__asyncDispose];
12
+ if (dispose === undefined)
13
+ dispose = value[__dispose];
14
+ if (typeof dispose !== "function")
15
+ throw TypeError("Object not disposable");
16
+ stack.push([async, dispose, value]);
17
+ } else if (async) {
18
+ stack.push([async]);
19
+ }
20
+ return value;
21
+ };
22
+ var __callDispose = (stack, error, hasError) => {
23
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
24
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
25
+ }, fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e), next = (it) => {
26
+ while (it = stack.pop()) {
27
+ try {
28
+ var result = it[1] && it[1].call(it[2]);
29
+ if (it[0])
30
+ return Promise.resolve(result).then(next, (e) => (fail(e), next()));
31
+ } catch (e) {
32
+ fail(e);
33
+ }
34
+ }
35
+ if (hasError)
36
+ throw error;
37
+ };
38
+ return next();
39
+ };
3
40
 
4
41
  // index.ts
5
42
  import { basename as basename2 } from "path";
@@ -48,9 +85,9 @@ async function handOffDev(argv, cwd) {
48
85
  // splates.ts
49
86
  import { agents } from "@stringsync/splates-server/agents";
50
87
  import { attach } from "@stringsync/splates-server/artifacts";
51
- import { commaList as commaList5 } from "@stringsync/splates-server/comma-list";
88
+ import { commaList as commaList6 } from "@stringsync/splates-server/comma-list";
52
89
  import { refuseGroomer as refuseGroomer4 } from "@stringsync/splates-server/conversations";
53
- import { exits, fail as fail9 } from "@stringsync/splates-server/exit";
90
+ import { exits, fail as fail10 } from "@stringsync/splates-server/exit";
54
91
  import { DEFAULT_COUNT, log as showLog } from "@stringsync/splates-server/log";
55
92
  import { merge } from "@stringsync/splates-server/merge";
56
93
  import { portsLs } from "@stringsync/splates-server/ports";
@@ -59,11 +96,13 @@ import {
59
96
  MIGRATING,
60
97
  MIGRATION_LIMIT,
61
98
  migrateRecords,
62
- repository as repository10
99
+ OpenBoards as OpenBoards2,
100
+ repository as repository11
63
101
  } from "@stringsync/splates-server/records";
64
102
  import {
65
103
  newest,
66
104
  recordsFirst,
105
+ refreshHere,
67
106
  rosterOf as rosterOf2,
68
107
  seeded as seeded2,
69
108
  update
@@ -85,9 +124,9 @@ import {
85
124
  untilAfter as untilAfter3,
86
125
  wait
87
126
  } from "@stringsync/splates-server/tasks";
88
- import { cli as cli15 } from "webappwiz/cmd";
127
+ import { cli as cli16 } from "webappwiz/cmd";
89
128
  import { Duration as Duration3 } from "webappwiz/time";
90
- import { z as z16 } from "zod";
129
+ import { z as z17 } from "zod";
91
130
 
92
131
  // artifact.ts
93
132
  import { basename, resolve as resolve2 } from "node:path";
@@ -244,9 +283,87 @@ splatesArtifact.command("remove").use(repository({ needs: "own" })).description(
244
283
  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)));
245
284
  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));
246
285
 
286
+ // blockers.ts
287
+ import { commaList as commaList3 } from "@stringsync/splates-server/comma-list";
288
+ import { fail as fail3 } from "@stringsync/splates-server/exit";
289
+ async function blockerFlags({
290
+ boards,
291
+ here,
292
+ blockedBy = [],
293
+ removeBlockedBy = []
294
+ }) {
295
+ const ids = [];
296
+ const blockedElsewhere = [];
297
+ for (const named of await namedIn(boards, here, blockedBy)) {
298
+ if (named.project === null) {
299
+ ids.push(named.id);
300
+ continue;
301
+ }
302
+ const found = await boards.find(named.project, named.id);
303
+ if (found === null) {
304
+ fail3("not_found", `the tasks of ${named.written} cannot be read: is ${named.slug} set up with \`splates init\`?`, { blockedBy: named.written });
305
+ }
306
+ if (found.status === "merged" || found.status === "removed") {
307
+ fail3("not_found", `${named.written} is ${found.status === "merged" ? `merged, as ${found.commit}` : "gone, removed without merging"}: nothing waits on it any more`, { blockedBy: named.written });
308
+ }
309
+ blockedElsewhere.push({
310
+ project: named.project,
311
+ id: named.id,
312
+ subject: found.subject
313
+ });
314
+ }
315
+ const unblocked = { ids: [], elsewhere: [] };
316
+ for (const named of await namedIn(boards, here, removeBlockedBy)) {
317
+ if (named.project === null) {
318
+ unblocked.ids.push(named.id);
319
+ } else {
320
+ unblocked.elsewhere.push({ project: named.project, id: named.id });
321
+ }
322
+ }
323
+ return { blockedBy: ids, blockedElsewhere, unblocked };
324
+ }
325
+ function id(digits) {
326
+ const parsed = Number(digits);
327
+ if (!Number.isInteger(parsed) || parsed <= 0) {
328
+ fail3("usage", `'${digits}' is not a task number`, { number: digits });
329
+ }
330
+ return parsed;
331
+ }
332
+ async function namedIn(boards, here, raw) {
333
+ const named = [];
334
+ for (const value of commaList3(raw.join(","))) {
335
+ const local = /^#?(\d+)$/.exec(value);
336
+ if (local !== null) {
337
+ named.push({
338
+ project: null,
339
+ slug: "",
340
+ id: id(local[1]),
341
+ written: value
342
+ });
343
+ continue;
344
+ }
345
+ const other = /^#?([\w.-]+)(?:\s+task)?\s*#(\d+)$/i.exec(value);
346
+ if (other === null) {
347
+ fail3("usage", `'${value}' is not a task: pass its number, or <project>#<number> for one of another project`, { blockedBy: value });
348
+ }
349
+ const slug = other[1];
350
+ const root = await boards.root(slug);
351
+ if (root === null) {
352
+ fail3("not_found", `no splates project '${slug}' on this machine: \`splates projects\` lists them`, { blockedBy: value });
353
+ }
354
+ named.push({
355
+ project: root === here ? null : root,
356
+ slug,
357
+ id: id(other[2]),
358
+ written: `${slug} #${other[2]}`
359
+ });
360
+ }
361
+ return named;
362
+ }
363
+
247
364
  // creds.ts
248
365
  import { DEFAULT_TIMEOUT, PROVIDERS } from "@stringsync/splates-server/creds";
249
- import { fail as fail3 } from "@stringsync/splates-server/exit";
366
+ import { fail as fail4 } from "@stringsync/splates-server/exit";
250
367
  import { repository as repository2 } from "@stringsync/splates-server/records";
251
368
  import { untilAfter } from "@stringsync/splates-server/tasks";
252
369
  import { cli as cli2 } from "webappwiz/cmd";
@@ -296,7 +413,7 @@ splatesCreds.command("wait").use(repository2({ needs: "own" })).description("blo
296
413
  await ctx.journal.waiting("creds wait", null, { name: opts.name, until: untilAfter(timeout) }, async () => {
297
414
  const ended = await ctx.creds.wait(opts.name, { timeout });
298
415
  if (ended === "dismissed") {
299
- fail3("dismissed", `${opts.name} was dismissed: carry on without it, or ask the person why`, { name: opts.name });
416
+ fail4("dismissed", `${opts.name} was dismissed: carry on without it, or ask the person why`, { name: opts.name });
300
417
  }
301
418
  });
302
419
  ctx.log.info(`${color.green("saved")} ${opts.name}`);
@@ -328,7 +445,7 @@ import {
328
445
  inspectBatch,
329
446
  writeImported
330
447
  } from "@stringsync/splates-server/evals";
331
- import { fail as fail4 } from "@stringsync/splates-server/exit";
448
+ import { fail as fail5 } from "@stringsync/splates-server/exit";
332
449
  import { cli as cli3 } from "webappwiz/cmd";
333
450
  import { color as color2 } from "webappwiz/log";
334
451
  import { z as z4 } from "zod";
@@ -344,7 +461,7 @@ splatesEvals.command("import").description("turn a framework's results into a sp
344
461
  const file = resolve3(ctx.ps.cwd(), opts.file);
345
462
  const dir = resolve3(ctx.ps.cwd(), opts["batch-dir"]);
346
463
  if (!await ctx.fs.exists(file)) {
347
- fail4("not_found", `no file at ${file}`, { file });
464
+ fail5("not_found", `no file at ${file}`, { file });
348
465
  }
349
466
  const imported = inspectBatch(await inspectLog(ctx, file), opts["pass-at"]);
350
467
  await writeImported(dir, imported);
@@ -358,12 +475,12 @@ async function inspectLog(ctx, file) {
358
475
  return log;
359
476
  }
360
477
  } catch {}
361
- return fail4("usage", `${file} is no Inspect eval log`, { file });
478
+ return fail5("usage", `${file} is no Inspect eval log`, { file });
362
479
  }
363
480
  async function dumped(ctx, file) {
364
- const { exitCode, stdout, stderr } = await ctx.ps.spawnCapture(["inspect", "log", "dump", file]).catch(() => fail4("usage", "`inspect` is not on PATH: activate the environment Inspect is installed in, or pass a log written with --log-format json"));
481
+ const { exitCode, stdout, stderr } = await ctx.ps.spawnCapture(["inspect", "log", "dump", file]).catch(() => fail5("usage", "`inspect` is not on PATH: activate the environment Inspect is installed in, or pass a log written with --log-format json"));
365
482
  if (exitCode !== 0) {
366
- fail4("usage", `inspect log dump ${file} failed: ${stderr.trim()}`, {
483
+ fail5("usage", `inspect log dump ${file} failed: ${stderr.trim()}`, {
367
484
  file
368
485
  });
369
486
  }
@@ -371,8 +488,8 @@ async function dumped(ctx, file) {
371
488
  }
372
489
 
373
490
  // hook.ts
374
- import { commaList as commaList3 } from "@stringsync/splates-server/comma-list";
375
- import { fail as fail5 } from "@stringsync/splates-server/exit";
491
+ import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
492
+ import { fail as fail6 } from "@stringsync/splates-server/exit";
376
493
  import {
377
494
  HookStore,
378
495
  hookAdd,
@@ -425,13 +542,13 @@ splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run
425
542
  let hook;
426
543
  if (opts.run !== "") {
427
544
  if (pick != null || opts.prompt !== "") {
428
- fail5("usage", "--run goes alone: a shell command runs no agent", {});
545
+ fail6("usage", "--run goes alone: a shell command runs no agent", {});
429
546
  }
430
547
  hook = { kind: "run", command: opts.run };
431
548
  } else if (pick == null) {
432
- fail5("usage", "say what runs: --agent with --prompt, or --run", {});
549
+ fail6("usage", "say what runs: --agent with --prompt, or --run", {});
433
550
  } else if (opts.prompt === "") {
434
- fail5("usage", "an agent hook needs --prompt: what to tell it", {});
551
+ fail6("usage", "an agent hook needs --prompt: what to tell it", {});
435
552
  } else {
436
553
  hook = { kind: "agent", prompt: opts.prompt, ...pick };
437
554
  }
@@ -471,7 +588,7 @@ splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("ch
471
588
  const { id, agent, model, effort, ...change } = opts;
472
589
  const pick = await assigneeIn(ctx, agent, model, effort);
473
590
  if (pick === null) {
474
- fail5("usage", "an agent automation needs an agent: name one, or --run", {});
591
+ fail6("usage", "an agent automation needs an agent: name one, or --run", {});
475
592
  }
476
593
  await hookEdit({ hooks: storeOf(ctx), log: ctx.log }, id, {
477
594
  ...change,
@@ -501,24 +618,24 @@ splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description(
501
618
  }).action(async (opts, ctx) => {
502
619
  const pick = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
503
620
  if (opts.off === (pick != null)) {
504
- fail5("usage", "say how it runs with --agent, or turn it off with --off", {});
621
+ fail6("usage", "say how it runs with --agent, or turn it off with --off", {});
505
622
  }
506
623
  await hookGroomer({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null);
507
624
  });
508
625
  function triggerOf(opts) {
509
626
  const given = [opts.event, opts.every, opts.cron].filter((each) => each !== undefined && each !== "");
510
627
  if (given.length !== 1) {
511
- fail5("usage", "say what sets it off: a task event, --every or --cron, one of them", {});
628
+ fail6("usage", "say what sets it off: a task event, --every or --cron, one of them", {});
512
629
  }
513
630
  if (opts.event === undefined || opts.event === "") {
514
631
  if (opts.stage !== "" || opts.lane !== "") {
515
- fail5("usage", "--stage and --lane narrow a task event, not a schedule", {});
632
+ fail6("usage", "--stage and --lane narrow a task event, not a schedule", {});
516
633
  }
517
634
  return opts.every !== "" ? { every: opts.every } : { cron: opts.cron };
518
635
  }
519
636
  const filter = {};
520
637
  if (opts.stage !== "") {
521
- filter.stages = stagesNamed(commaList3(opts.stage));
638
+ filter.stages = stagesNamed(commaList4(opts.stage));
522
639
  }
523
640
  if (opts.lane !== "") {
524
641
  filter.lanes = lanesNamed(opts.lane);
@@ -532,10 +649,10 @@ function storeOf(ctx) {
532
649
 
533
650
  // lane.ts
534
651
  import {
535
- APPROVAL,
536
652
  BACKLOG_LANE as BACKLOG_LANE2,
537
653
  laneAdd,
538
654
  laneList,
655
+ laneQueue,
539
656
  laneRemove,
540
657
  laneShow,
541
658
  laneUpdate,
@@ -544,7 +661,10 @@ import {
544
661
  } from "@stringsync/splates-server/board";
545
662
  import { refuseGroomer } from "@stringsync/splates-server/conversations";
546
663
  import { UNTRIAGED } from "@stringsync/splates-server/plans";
547
- import { refuseAgentQueue } from "@stringsync/splates-server/queue";
664
+ import {
665
+ refuseAgentQueue,
666
+ sayUnqueuedBlockers
667
+ } from "@stringsync/splates-server/queue";
548
668
  import { repository as repository4 } from "@stringsync/splates-server/records";
549
669
  import { cli as cli5 } from "webappwiz/cmd";
550
670
  import { z as z6 } from "zod";
@@ -552,7 +672,7 @@ var laneNumber = z6.union([
552
672
  z6.literal("backlog").transform(() => BACKLOG_LANE2),
553
673
  z6.coerce.number().int().nonnegative()
554
674
  ]);
555
- var splatesLane = cli5("lane").description("lanes: named groups of tasks, for you to see related work together on the board; queue one whole with `splates lane queue`. Backlog (lane 0) is permanent and holds unmatched deferred work. Untriaged is groomed into lanes and cannot be queued whole");
675
+ var splatesLane = cli5("lane").description("lanes: named groups of tasks, for you to see related work together on the board; queue all its tasks at once with `splates lane queue`. Backlog (lane 0) is permanent and holds unmatched deferred work. Untriaged is groomed into lanes and cannot be queued as a lane");
556
676
  splatesLane.command("list").use(repository4({ needs: "read" })).description("every lane: its name, its queued tasks in order, the started tasks on it, its next task, and what blocks that in other lanes").option("json", z6.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneList(ctx, { json: opts.json }));
557
677
  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], {
558
678
  error: "pass a lane's number, backlog, or untriaged"
@@ -572,27 +692,19 @@ splatesLane.command("update").use(repository4({ needs: "tasks" })).description("
572
692
  const change = { name: opts.name, position: opts.position };
573
693
  await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
574
694
  });
575
- splatesLane.command("queue").use(repository4({ needs: "tasks" })).description("queue a lane whole: each of its tasks is queued as soon as nothing blocks it, now and as tasks land, until the lane is empty; a task you take out with `splates queue remove` stays out until you queue it again. Untriaged cannot be queued whole").arg("lane", laneNumber, {
695
+ splatesLane.command("queue").use(repository4({ needs: "tasks" })).description("queue every task in a lane now, once: the queue starts each once nothing it is blocked by is open; one blocked by a task outside the queue waits queued until that is queued too, as the command names; a task put in the lane later is queued on its own. Untriaged cannot be queued as a lane").arg("lane", laneNumber, {
576
696
  description: "lane number"
577
697
  }).action(async (opts, ctx) => {
578
698
  refuseAgentQueue(ctx.service, "lane queue");
579
699
  await refuseGroomer(ctx.service, "lane queue");
580
- const change = { queued: true };
581
- await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
700
+ const queued = await laneQueue(ctx, opts.lane, true);
701
+ await sayUnqueuedBlockers(ctx, queued.map((task) => task.id));
582
702
  });
583
- splatesLane.command("approve").use(repository4({ needs: "tasks" })).description("let a queued lane queue more tasks on its own: it stops when its budget is spent, which starts at its tasks then plus 5, and this adds 10 to what is left; refused from an agent").arg("lane", laneNumber, {
584
- description: "lane number"
585
- }).action(async (opts, ctx) => {
586
- refuseAgentQueue(ctx.service, "lane approve");
587
- const change = { approve: APPROVAL };
588
- await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
589
- });
590
- splatesLane.command("unqueue").use(repository4({ needs: "tasks" })).description("stop queueing a lane whole, taking out of the queue each of its tasks that has yet to start; those under way go on").arg("lane", laneNumber, {
703
+ splatesLane.command("unqueue").use(repository4({ needs: "tasks" })).description("take each of a lane's queued tasks out of the queue; those under way go on").arg("lane", laneNumber, {
591
704
  description: "lane number"
592
705
  }).action(async (opts, ctx) => {
593
706
  refuseAgentQueue(ctx.service, "lane unqueue");
594
- const change = { queued: false };
595
- await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
707
+ await laneQueue(ctx, opts.lane, false);
596
708
  });
597
709
  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, {
598
710
  description: "lane number"
@@ -672,7 +784,7 @@ for (const command of ["pause", "resume"]) {
672
784
  import { dirname as dirname2 } from "node:path";
673
785
  import { loadConfig } from "@stringsync/splates-server/config";
674
786
  import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
675
- import { fail as fail6 } from "@stringsync/splates-server/exit";
787
+ import { fail as fail7 } from "@stringsync/splates-server/exit";
676
788
  import { Projects } from "@stringsync/splates-server/projects";
677
789
  import {
678
790
  FORMAT,
@@ -714,7 +826,7 @@ splatesReplica.command("restore").description("with `splates server` stopped, re
714
826
  ]);
715
827
  const config = await loadConfig(root, { fs: ctx.fs });
716
828
  if (config.replica === null) {
717
- fail6("usage", "no replica: name one as `replica` in splates.config.ts first");
829
+ fail7("usage", "no replica: name one as `replica` in splates.config.ts first");
718
830
  }
719
831
  const replica = openReplica(config.replica, {
720
832
  root,
@@ -741,7 +853,7 @@ splatesReplica.command("restore").description("with `splates server` stopped, re
741
853
  ].join(`
742
854
  `));
743
855
  } catch (error) {
744
- fail6("usage", error instanceof Error ? error.message : String(error));
856
+ fail7("usage", error instanceof Error ? error.message : String(error));
745
857
  }
746
858
  });
747
859
  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(), {
@@ -762,7 +874,7 @@ async function mainTree(ctx) {
762
874
  "--git-common-dir"
763
875
  ]);
764
876
  if (exitCode !== 0) {
765
- fail6("usage", `not a git repository: ${ctx.ps.cwd()}`);
877
+ fail7("usage", `not a git repository: ${ctx.ps.cwd()}`);
766
878
  }
767
879
  return dirname2(stdout.trim());
768
880
  }
@@ -807,7 +919,7 @@ splatesServer.command("run").description("serve every splates project on this ma
807
919
  // setup.ts
808
920
  import { dirname as dirname3 } from "node:path";
809
921
  import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
810
- import { fail as fail7 } from "@stringsync/splates-server/exit";
922
+ import { fail as fail8 } from "@stringsync/splates-server/exit";
811
923
  import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
812
924
  import { Projects as Projects2 } from "@stringsync/splates-server/projects";
813
925
  import {
@@ -823,15 +935,15 @@ import { fetchWorkers } from "@stringsync/splates-server/workers";
823
935
  import { color as color3 } from "webappwiz/log";
824
936
  async function setup(deps, opts) {
825
937
  if (opts.step === "" && (opts.skip || opts.unskip || opts.undo)) {
826
- fail7("usage", "--skip and --unskip name a step, --undo an area", {});
938
+ fail8("usage", "--skip and --unskip name a step, --undo an area", {});
827
939
  }
828
940
  if (opts.undo && (opts.skip || opts.unskip)) {
829
- fail7("usage", "--undo names an area, not a step to skip", {
941
+ fail8("usage", "--undo names an area, not a step to skip", {
830
942
  area: opts.step
831
943
  });
832
944
  }
833
945
  if (opts.skip && opts.unskip) {
834
- fail7("usage", "--skip or --unskip, not both", { step: opts.step });
946
+ fail8("usage", "--skip or --unskip, not both", { step: opts.step });
835
947
  }
836
948
  const machine = await setupOf(deps);
837
949
  const { log } = deps;
@@ -976,12 +1088,38 @@ function printUndo(log, plan) {
976
1088
  log.info(color3.dim("the Setup page's Undo does it, after the user confirms"));
977
1089
  }
978
1090
 
979
- // slots.ts
980
- import { Slots } from "@stringsync/splates-server/ports";
1091
+ // ship.ts
1092
+ import { repository as repository8 } from "@stringsync/splates-server/records";
1093
+ import { DEFAULT_PORT as DEFAULT_PORT2 } from "@stringsync/splates-server/server";
981
1094
  import { cli as cli10 } from "webappwiz/cmd";
982
1095
  import { z as z11 } from "zod";
983
- var splatesSlots = cli10("slots").description("the words `splates server --slot` hands out on this machine, each held by the tree that claimed it: a task's worktree holds its word until the task merges or is removed, which stops its server and gives the word back");
984
- splatesSlots.command("list").description("print each claimed word, the tree holding it, its server's pid while one runs, and whether it is orphaned: its tree gone, or a splates repository's main tree, which every agent shares").option("json", z11.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
1096
+ var splatesShip = cli10("ship").description("open the Ships page with a project picked").fallback("run");
1097
+ splatesShip.command("run").use(repository8({ needs: "read" })).arg("project", z11.string(), {
1098
+ default: "",
1099
+ description: "the registered project to show"
1100
+ }).option("dev", z11.string(), {
1101
+ default: "",
1102
+ description: "the splates server URL"
1103
+ }).action((opts, ctx) => {
1104
+ const page = new URL("/", opts.dev || ctx.ps.env("SPLATES_DEV") || `http://localhost:${DEFAULT_PORT2}`);
1105
+ page.hash = "ships";
1106
+ if (opts.project !== "") {
1107
+ page.searchParams.set("ship", opts.project);
1108
+ }
1109
+ ctx.log.info(`Open ship plan: ${page}`);
1110
+ const local = new URL(page);
1111
+ local.hostname = "localhost";
1112
+ local.protocol = "http:";
1113
+ local.port = page.hostname === "localhost" || page.hostname === "127.0.0.1" ? page.port : String(DEFAULT_PORT2);
1114
+ ctx.log.info(`Local: ${local}`);
1115
+ });
1116
+
1117
+ // slots.ts
1118
+ import { Slots } from "@stringsync/splates-server/ports";
1119
+ import { cli as cli11 } from "webappwiz/cmd";
1120
+ import { z as z12 } from "zod";
1121
+ 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");
1122
+ 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) => {
985
1123
  const held = await Slots.ofMachine(ctx).status();
986
1124
  if (opts.json) {
987
1125
  ctx.log.info(JSON.stringify(held.map(({ slot, tree, pid, orphaned }) => ({
@@ -1024,19 +1162,19 @@ import {
1024
1162
  tasksNamed as tasksNamed2,
1025
1163
  taskUpdate
1026
1164
  } from "@stringsync/splates-server/board";
1027
- import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
1165
+ import { commaList as commaList5 } from "@stringsync/splates-server/comma-list";
1028
1166
  import { refuseGroomer as refuseGroomer3 } from "@stringsync/splates-server/conversations";
1029
- import { fail as fail8 } from "@stringsync/splates-server/exit";
1030
- import { repository as repository8 } from "@stringsync/splates-server/records";
1167
+ import { fail as fail9 } from "@stringsync/splates-server/exit";
1168
+ import { OpenBoards, repository as repository9 } from "@stringsync/splates-server/records";
1031
1169
  import {
1032
1170
  DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
1033
1171
  taskWait,
1034
1172
  untilAfter as untilAfter2
1035
1173
  } from "@stringsync/splates-server/tasks";
1036
- import { cli as cli11 } from "webappwiz/cmd";
1174
+ import { cli as cli12 } from "webappwiz/cmd";
1037
1175
  import { Duration as Duration2 } from "webappwiz/time";
1038
- import { z as z12 } from "zod";
1039
- var splatesTask = cli11("task").description("the numbered tasks: defer work for later instead of growing the task you are in, and `splates add <slug> --task <n>` starts one; `merge` recommends the next one when a task lands. The person puts tasks in `splates queue`, for workers to start");
1176
+ import { z as z13 } from "zod";
1177
+ 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");
1040
1178
  async function projectFrom(ctx) {
1041
1179
  if (ctx.elsewhere === null) {
1042
1180
  return;
@@ -1044,62 +1182,77 @@ async function projectFrom(ctx) {
1044
1182
  return (await ctx.projects.list()).find((each) => each.root === ctx.git.root)?.slug;
1045
1183
  }
1046
1184
  function adding(name, description, ready) {
1047
- splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z12.string(), {
1185
+ splatesTask.command(name).use(repository9({ needs: "queue" })).description(description).arg("subject", z13.string(), {
1048
1186
  description: "what is left to do, in a line"
1049
- }).arg("text", z12.string(), {
1187
+ }).arg("text", z13.string(), {
1050
1188
  default: "",
1051
1189
  description: "whatever more there is to say about it, in markdown"
1052
- }).option("slug", z12.string(), {
1190
+ }).option("slug", z13.string(), {
1053
1191
  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"
1054
1192
  }).option("position", position, {
1055
1193
  description: "where it goes in the list, 1 at the top, pushing those from there down (default: the bottom)"
1056
- }).repeated("file", z12.string(), {
1194
+ }).repeated("file", z13.string(), {
1057
1195
  description: "files to attach, comma separated or the flag repeated: copied under .git/splates/tasks/<n>/"
1058
- }).repeated("blocked-by", z12.string(), {
1059
- description: "numbers of the tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts"
1060
- }).option("lane", z12.string(), {
1196
+ }).repeated("blocked-by", z13.string(), {
1197
+ description: "the tasks blocking it, comma separated or the flag repeated: a number for one here, <project>#<number> for one of another project (`webappwiz#404`); each has to merge before this one starts"
1198
+ }).option("lane", z13.string(), {
1061
1199
  default: "",
1062
1200
  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)"
1063
- }).option("agent", z12.string(), {
1201
+ }).option("agent", z13.string(), {
1064
1202
  default: "",
1065
1203
  description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first"
1066
- }).option("model", z12.string(), {
1204
+ }).option("model", z13.string(), {
1067
1205
  default: "",
1068
1206
  description: "with --agent: the model, by its id or name on the roster"
1069
- }).option("effort", z12.string(), {
1207
+ }).option("effort", z13.string(), {
1070
1208
  default: "",
1071
1209
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
1072
1210
  }).action(async (opts, ctx) => {
1073
- if (ctx.elsewhere !== null && opts.lane !== "") {
1074
- fail8("not_granted", "a task from another project goes in its untriaged lane, for whoever orchestrates it there to place: leave --lane off", { subject: opts.subject });
1211
+ let __stack = [];
1212
+ try {
1213
+ if (ctx.elsewhere !== null && opts.lane !== "") {
1214
+ fail9("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 });
1215
+ }
1216
+ const from = await here(ctx);
1217
+ const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
1218
+ const origin = ctx.elsewhere === null ? undefined : {
1219
+ project: ctx.elsewhere,
1220
+ task: ctx.project === undefined ? null : ctx.service.taskFor(await ctx.git.currentBranch(ctx.ps.cwd()))
1221
+ };
1222
+ const project = await projectFrom(ctx);
1223
+ const boards = __using(__stack, new OpenBoards(ctx, ctx.projects), 0);
1224
+ const { blockedBy, blockedElsewhere } = await blockerFlags({
1225
+ boards,
1226
+ here: ctx.git.root,
1227
+ blockedBy: opts["blocked-by"]
1228
+ });
1229
+ await ctx.journal.record("task add", from, () => taskAdd(ctx, opts.subject, from, {
1230
+ slug: opts.slug,
1231
+ text: opts.text,
1232
+ position: opts.position,
1233
+ files: commaList5(opts.file.join(",")),
1234
+ blockedBy,
1235
+ blockedElsewhere,
1236
+ lane: laneIn(opts.lane) ?? null,
1237
+ assignee,
1238
+ origin,
1239
+ ready,
1240
+ project,
1241
+ boards
1242
+ }), addedFacts);
1243
+ } catch (_catch) {
1244
+ var _err = _catch, _hasErr = 1;
1245
+ } finally {
1246
+ __callDispose(__stack, _err, _hasErr);
1075
1247
  }
1076
- const from = await here(ctx);
1077
- const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
1078
- const origin = ctx.elsewhere === null ? undefined : {
1079
- project: ctx.elsewhere,
1080
- task: ctx.project === undefined ? null : ctx.service.taskFor(await ctx.git.currentBranch(ctx.ps.cwd()))
1081
- };
1082
- const project = await projectFrom(ctx);
1083
- await ctx.journal.record("task add", from, () => taskAdd(ctx, opts.subject, from, {
1084
- slug: opts.slug,
1085
- text: opts.text,
1086
- position: opts.position,
1087
- files: commaList4(opts.file.join(",")),
1088
- blockedBy: taskIds(opts["blocked-by"].join(",")),
1089
- lane: laneIn(opts.lane) ?? null,
1090
- assignee,
1091
- origin,
1092
- ready,
1093
- project
1094
- }), addedFacts);
1095
1248
  });
1096
1249
  }
1097
1250
  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);
1098
1251
  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);
1099
- splatesTask.command("list").use(repository8({ needs: "read" })).description("every queued task, top of the list first, with the task it came from and the started one that took it up").option("json", z12.boolean(), { default: false, description: "emit JSON" }).option("open", z12.boolean(), {
1252
+ splatesTask.command("list").use(repository9({ 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(), {
1100
1253
  default: false,
1101
1254
  description: "only tasks no started one has taken up: the ones free to pick up"
1102
- }).option("ready", z12.boolean(), {
1255
+ }).option("ready", z13.boolean(), {
1103
1256
  default: false,
1104
1257
  description: "only tasks free to start now: none has taken them, and nothing blocking them is still open"
1105
1258
  }).action(async (opts, ctx) => taskList(ctx, {
@@ -1108,77 +1261,96 @@ splatesTask.command("list").use(repository8({ needs: "read" })).description("eve
1108
1261
  ready: opts.ready,
1109
1262
  project: await projectFrom(ctx)
1110
1263
  }));
1111
- splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).option("json", z12.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => taskShow(ctx, opts.id, {
1112
- json: opts.json,
1113
- project: await projectFrom(ctx),
1114
- others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug)
1115
- }));
1116
- splatesTask.command("update").use(repository8({ needs: "tasks" })).description("say what a queued task is in other words, move it up or down the list, or attach and drop files; it keeps its number").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).arg("text", z12.string(), {
1264
+ splatesTask.command("show").use(repository9({ 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) => {
1265
+ let __stack = [];
1266
+ try {
1267
+ const boards = __using(__stack, new OpenBoards(ctx, ctx.projects), 0);
1268
+ await taskShow(ctx, opts.id, {
1269
+ json: opts.json,
1270
+ project: await projectFrom(ctx),
1271
+ others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug),
1272
+ boards,
1273
+ root: ctx.git.root
1274
+ });
1275
+ } catch (_catch) {
1276
+ var _err = _catch, _hasErr = 1;
1277
+ } finally {
1278
+ __callDispose(__stack, _err, _hasErr);
1279
+ }
1280
+ });
1281
+ splatesTask.command("update").use(repository9({ 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(), {
1117
1282
  default: "",
1118
1283
  description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
1119
- }).option("subject", z12.string(), {
1284
+ }).option("subject", z13.string(), {
1120
1285
  default: "",
1121
1286
  description: "what is left to do, in a line; leave out to keep it"
1122
- }).option("slug", z12.string(), {
1287
+ }).option("slug", z13.string(), {
1123
1288
  default: "",
1124
1289
  description: "a new name for the task that takes it (lowercase-with-dashes); leave out to keep it"
1125
1290
  }).option("position", position, {
1126
1291
  description: "where to move it in the list, 1 at the top; the others close up around it"
1127
- }).repeated("file", z12.string(), {
1292
+ }).repeated("file", z13.string(), {
1128
1293
  description: "files to attach, comma separated or the flag repeated"
1129
- }).repeated("remove-file", z12.string(), {
1294
+ }).repeated("remove-file", z13.string(), {
1130
1295
  description: "attached files to drop, comma separated or the flag repeated, by path or stored name"
1131
- }).repeated("blocked-by", z12.string(), {
1132
- description: "numbers of more tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts; refused when that makes a loop"
1133
- }).repeated("remove-blocked-by", z12.string(), {
1134
- description: "numbers of tasks no longer blocking it, comma separated or the flag repeated"
1135
- }).option("lane", z12.string(), {
1296
+ }).repeated("blocked-by", z13.string(), {
1297
+ description: "more tasks blocking it, comma separated or the flag repeated: a number for one here, <project>#<number> for one of another project (`webappwiz#404`); each has to merge before this one starts; refused when that makes a loop"
1298
+ }).repeated("remove-blocked-by", z13.string(), {
1299
+ description: "tasks no longer blocking it, as --blocked-by names them, comma separated or the flag repeated"
1300
+ }).option("lane", z13.string(), {
1136
1301
  default: "",
1137
1302
  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"
1138
- }).option("agent", z12.string(), {
1303
+ }).option("agent", z13.string(), {
1139
1304
  default: "",
1140
1305
  description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first, or `none`"
1141
- }).option("model", z12.string(), {
1306
+ }).option("model", z13.string(), {
1142
1307
  default: "",
1143
1308
  description: "with --agent: the model, by its id or name on the roster"
1144
- }).option("effort", z12.string(), {
1309
+ }).option("effort", z13.string(), {
1145
1310
  default: "",
1146
1311
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
1147
- }).option("ready", z12.boolean(), {
1312
+ }).option("ready", z13.boolean(), {
1148
1313
  default: false,
1149
1314
  description: "mark it groomed, moving it from the backlog to ready: no task already asks it, and what blocks it is linked"
1150
- }).option("manual", z12.boolean(), {
1151
- default: false,
1152
- description: "queue it by hand only: a queued lane passes over it, and it starts once you `splates queue add` it"
1153
- }).option("auto", z12.boolean(), {
1154
- default: false,
1155
- description: "let a queued lane queue it again, undoing --manual"
1156
1315
  }).action(async (opts, ctx) => {
1157
- if (opts.manual && opts.auto) {
1158
- fail8("usage", "pass --manual or --auto, not both", { number: opts.id });
1159
- }
1160
- await refuseGroomer3(ctx.service, "task update", await ctx.tasks.find(opts.id));
1161
- const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
1162
- await recordUpdate(ctx.journal, ctx.tasks, opts.id, () => taskUpdate(ctx, opts.id, {
1163
- subject: opts.subject,
1164
- slug: opts.slug,
1165
- text: opts.text,
1166
- position: opts.position,
1167
- files: commaList4(opts.file.join(",")),
1168
- removeFiles: commaList4(opts["remove-file"].join(",")),
1169
- blockedBy: taskIds(opts["blocked-by"].join(",")),
1170
- removeBlockedBy: taskIds(opts["remove-blocked-by"].join(",")),
1171
- lane: laneIn(opts.lane),
1172
- assignee,
1173
- ready: opts.ready,
1174
- manual: opts.manual ? true : opts.auto ? false : undefined
1175
- }));
1316
+ let __stack = [];
1317
+ try {
1318
+ await refuseGroomer3(ctx.service, "task update", await ctx.tasks.find(opts.id));
1319
+ const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
1320
+ const boards = __using(__stack, new OpenBoards(ctx, ctx.projects), 0);
1321
+ const { blockedBy, blockedElsewhere, unblocked } = await blockerFlags({
1322
+ boards,
1323
+ here: ctx.git.root,
1324
+ blockedBy: opts["blocked-by"],
1325
+ removeBlockedBy: opts["remove-blocked-by"]
1326
+ });
1327
+ await recordUpdate(ctx.journal, ctx.tasks, opts.id, () => taskUpdate(ctx, opts.id, {
1328
+ subject: opts.subject,
1329
+ slug: opts.slug,
1330
+ text: opts.text,
1331
+ position: opts.position,
1332
+ files: commaList5(opts.file.join(",")),
1333
+ removeFiles: commaList5(opts["remove-file"].join(",")),
1334
+ blockedBy,
1335
+ removeBlockedBy: unblocked.ids,
1336
+ blockedElsewhere,
1337
+ removeElsewhere: unblocked.elsewhere,
1338
+ lane: laneIn(opts.lane),
1339
+ assignee,
1340
+ ready: opts.ready,
1341
+ boards
1342
+ }));
1343
+ } catch (_catch) {
1344
+ var _err = _catch, _hasErr = 1;
1345
+ } finally {
1346
+ __callDispose(__stack, _err, _hasErr);
1347
+ }
1176
1348
  });
1177
- splatesTask.command("remove").use(repository8({ needs: "tasks" })).description("drop a queued task that was done some other way or no longer applies").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).action(async (opts, ctx) => {
1349
+ splatesTask.command("remove").use(repository9({ 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) => {
1178
1350
  await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
1179
1351
  await ctx.journal.record("task remove", null, () => taskRemove(ctx, opts.id), (removed) => ({ tasks: tasksNamed2([removed]) }));
1180
1352
  });
1181
- splatesTask.command("wait").use(repository8({ needs: "read" })).description("block until a task leaves the queue, which it does when the task that took it merges: for a task blocked by one another lane is on; ends early when that task waits on a person to answer its questions, and gives up with `timeout`").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).option("timeout-secs", z12.coerce.number(), {
1353
+ splatesTask.command("wait").use(repository9({ 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(), {
1182
1354
  default: DEFAULT_TIMEOUT2.secs,
1183
1355
  description: "how long to wait before giving up"
1184
1356
  }).action(async (opts, ctx) => {
@@ -1192,25 +1364,25 @@ splatesTask.command("wait").use(repository8({ needs: "read" })).description("blo
1192
1364
  });
1193
1365
 
1194
1366
  // tunnel.ts
1195
- import { repository as repository9 } from "@stringsync/splates-server/records";
1367
+ import { repository as repository10 } from "@stringsync/splates-server/records";
1196
1368
  import { savedTunnelOf } from "@stringsync/splates-server/server";
1197
1369
  import {
1198
1370
  API_TOKEN,
1199
1371
  tunnelRemove,
1200
1372
  tunnelSetup
1201
1373
  } from "@stringsync/splates-server/tunnel";
1202
- import { cli as cli12 } from "webappwiz/cmd";
1203
- import { z as z13 } from "zod";
1204
- var splatesTunnel = cli12("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
1205
- splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`make a Cloudflare tunnel to \`splates server\`, the DNS record routing <host> to it, and an Access application letting in only --email, through the API with the token kept as the cred ${API_TOKEN}, then save them for this machine, so a bare \`splates server\` comes up tunneled; run again, it changes only what is not so already. Without that token it requests it on the Creds page of \`splates server\`, with how to make it, and exits \`requested\`: \`splates creds wait ${API_TOKEN}\`, then run it again`).arg("host", z13.string(), {
1374
+ import { cli as cli13 } from "webappwiz/cmd";
1375
+ import { z as z14 } from "zod";
1376
+ var splatesTunnel = cli13("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
1377
+ splatesTunnel.command("setup").use(repository10({ 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(), {
1206
1378
  description: "the public host name, in a zone of the account, as in splates.example.com"
1207
- }).option("email", z13.string(), {
1379
+ }).option("email", z14.string(), {
1208
1380
  default: "",
1209
1381
  description: "comma-separated email addresses Access lets in"
1210
- }).option("name", z13.string(), {
1382
+ }).option("name", z14.string(), {
1211
1383
  default: "splates",
1212
1384
  description: "what the tunnel, the Access application and its policy are called"
1213
- }).option("team", z13.string(), {
1385
+ }).option("team", z14.string(), {
1214
1386
  default: "",
1215
1387
  description: "the Zero Trust team to make, as in <team>.cloudflareaccess.com, when the account has none yet"
1216
1388
  }).action(async (opts, ctx) => ctx.journal.record("tunnel setup", null, async () => tunnelSetup({
@@ -1221,7 +1393,7 @@ splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`m
1221
1393
  log: ctx.log,
1222
1394
  fetch: ctx.fetch
1223
1395
  })));
1224
- splatesTunnel.command("status").description("print the tunnel `splates tunnel setup` saved for this machine, which a bare `splates server` runs, and whether its token is in the system's secret store").option("json", z13.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
1396
+ 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) => {
1225
1397
  const saved = savedTunnelOf(ctx);
1226
1398
  const record = await saved.read();
1227
1399
  const token = record === null ? false : await saved.token() !== undefined;
@@ -1242,7 +1414,7 @@ splatesTunnel.command("status").description("print the tunnel `splates tunnel se
1242
1414
  ].join(`
1243
1415
  `));
1244
1416
  });
1245
- splatesTunnel.command("remove").use(repository9({ needs: "own" })).description(`delete what \`splates tunnel setup\` and \`splates server --slot\` made in Cloudflare by what they saved: each tunnel, the DNS record routing to it, the Access applications and their policy, then forget the saved files and the tunnels' tokens; the API token kept as ${API_TOKEN} stays. Run again after a failure, it finishes what is left`).action(async (_opts, ctx) => ctx.journal.record("tunnel remove", null, async () => tunnelRemove({
1417
+ splatesTunnel.command("remove").use(repository10({ needs: "own" })).description(`delete what \`splates tunnel setup\` and \`splates server --slot\` made in Cloudflare by what they saved: each tunnel, the DNS record routing to it, the Access applications and their policy, then forget the saved files and the tunnels' tokens; the API token kept as ${API_TOKEN} stays. Run again after a failure, it finishes what is left`).action(async (_opts, ctx) => ctx.journal.record("tunnel remove", null, async () => tunnelRemove({
1246
1418
  saved: savedTunnelOf(ctx),
1247
1419
  slots: slotTunnelsOf(ctx),
1248
1420
  creds: ctx.creds,
@@ -1261,20 +1433,20 @@ import {
1261
1433
  removeWorker,
1262
1434
  workerRun
1263
1435
  } from "@stringsync/splates-server/workers";
1264
- import { cli as cli14 } from "webappwiz/cmd";
1436
+ import { cli as cli15 } from "webappwiz/cmd";
1265
1437
  import { CompositeLogger } from "webappwiz/log";
1266
- import { z as z15 } from "zod";
1438
+ import { z as z16 } from "zod";
1267
1439
 
1268
1440
  // waiting.ts
1269
1441
  import { listQueue, moveQueued } from "@stringsync/splates-server/workers";
1270
- import { cli as cli13 } from "webappwiz/cmd";
1271
- import { z as z14 } from "zod";
1442
+ import { cli as cli14 } from "webappwiz/cmd";
1443
+ import { z as z15 } from "zod";
1272
1444
  var DEV = {
1273
1445
  default: "",
1274
1446
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1275
1447
  };
1276
- var splatesWaiting = cli13("queue").description("the agents waiting to start on a worker of the running `splates server`: nothing caps how many agents run, so one waits only while what splates starts on its own (the queue, automations, hooks, the groomer) is paused, or while no worker it can run on is up, first come, first served; none is ever failed for waiting. An agent you start waits only while no worker is up");
1277
- splatesWaiting.command("list").description("list the agents waiting to start, first in line first: position, project, task, conversation, how long each has waited and why (paused, no worker up, or worker draining)").option("dev", z14.string(), DEV).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
1448
+ var splatesWaiting = cli14("queue").description("the agents waiting to start on a worker of the running `splates server`: nothing caps how many agents run, so one waits only while what splates starts on its own (the queue, automations, hooks, the groomer) is paused, or while no worker it can run on is up, first come, first served; none is ever failed for waiting. An agent you start waits only while no worker is up");
1449
+ splatesWaiting.command("list").description("list the agents waiting to start, first in line first: position, project, task, conversation, how long each has waited and why (paused, no worker up, or worker draining)").option("dev", z15.string(), DEV).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
1278
1450
  dev: opts.dev,
1279
1451
  json: opts.json,
1280
1452
  log: ctx.log,
@@ -1282,11 +1454,11 @@ splatesWaiting.command("list").description("list the agents waiting to start, fi
1282
1454
  ps: ctx.ps,
1283
1455
  send: ctx.fetch
1284
1456
  }));
1285
- splatesWaiting.command("move").description("move an agent waiting to start to another place in line, from 1 for the next to start, then list the queue").arg("which", z14.string(), {
1457
+ splatesWaiting.command("move").description("move an agent waiting to start to another place in line, from 1 for the next to start, then list the queue").arg("which", z15.string(), {
1286
1458
  description: "the task it works, or its conversation's id"
1287
- }).arg("position", z14.coerce.number().int(), {
1459
+ }).arg("position", z15.coerce.number().int(), {
1288
1460
  description: "where in line to put it, from 1"
1289
- }).option("dev", z14.string(), DEV).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
1461
+ }).option("dev", z15.string(), DEV).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
1290
1462
  which: opts.which,
1291
1463
  position: opts.position,
1292
1464
  dev: opts.dev,
@@ -1298,20 +1470,20 @@ splatesWaiting.command("move").description("move an agent waiting to start to an
1298
1470
  }));
1299
1471
 
1300
1472
  // worker.ts
1301
- var splatesWorker = cli14("worker").description("this machine’s worker, which runs agents, terminals and process reads for `splates server` over its Unix socket");
1302
- splatesWorker.command("run").description("run this machine’s worker until stopped, connecting over its private Unix socket and reconnecting when the server restarts").option("server", z15.string(), {
1473
+ var splatesWorker = cli15("worker").description("this machine’s worker, which runs agents, terminals and process reads for `splates server` over its Unix socket");
1474
+ 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", z16.string(), {
1303
1475
  default: "",
1304
1476
  description: "the `splates server` to dial: unix:<path>; by default this machine's"
1305
- }).option("coordinator", z15.string(), {
1477
+ }).option("coordinator", z16.string(), {
1306
1478
  default: "",
1307
1479
  description: "the old name for --server, still read for workers installed before it, and passed to a build older than it"
1308
- }).option("slots", z15.coerce.number().int().min(0), {
1480
+ }).option("slots", z16.coerce.number().int().min(0), {
1309
1481
  default: 0,
1310
1482
  description: "ignored: nothing caps how many agents a worker runs; still read, since a `splates server` started before it was dropped passes it"
1311
- }).option("id", z15.literal("local"), {
1483
+ }).option("id", z16.literal("local"), {
1312
1484
  default: "local",
1313
1485
  description: "what the server knows this worker by, kept across restarts so it picks up where it was; by default local"
1314
- }).option("quiet", z15.boolean(), {
1486
+ }).option("quiet", z16.boolean(), {
1315
1487
  default: false,
1316
1488
  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"
1317
1489
  }).action((opts, ctx) => workerRun({
@@ -1321,10 +1493,10 @@ splatesWorker.command("run").description("run this machine’s worker until stop
1321
1493
  ps: ctx.ps,
1322
1494
  fs: ctx.fs
1323
1495
  }));
1324
- splatesWorker.command("list").description("list the workers the running `splates server` has: platform, build, protocol, health with when it was last heard, and tasks, then any it refused for speaking another protocol").option("dev", z15.string(), {
1496
+ splatesWorker.command("list").description("list the workers the running `splates server` has: platform, build, protocol, health with when it was last heard, and tasks, then any it refused for speaking another protocol").option("dev", z16.string(), {
1325
1497
  default: "",
1326
1498
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1327
- }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
1499
+ }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
1328
1500
  dev: opts.dev,
1329
1501
  json: opts.json,
1330
1502
  log: ctx.log,
@@ -1336,12 +1508,12 @@ var dev = {
1336
1508
  default: "",
1337
1509
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1338
1510
  };
1339
- splatesWorker.command("drain").description("take no new agents on a worker, letting those on it run to their end, but for one asking you something in a task, which saves its task and lets its slot go, its question kept for you to answer and carry it on elsewhere; --undo takes new ones again").arg("id", z15.string(), {
1511
+ 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(), {
1340
1512
  description: "the worker, as `splates worker list` names it"
1341
- }).option("undo", z15.boolean(), {
1513
+ }).option("undo", z16.boolean(), {
1342
1514
  default: false,
1343
1515
  description: "take new agents again"
1344
- }).option("dev", z15.string(), dev).action((opts, ctx) => drainWorker({
1516
+ }).option("dev", z16.string(), dev).action((opts, ctx) => drainWorker({
1345
1517
  id: opts.id,
1346
1518
  undo: opts.undo,
1347
1519
  dev: opts.dev,
@@ -1350,12 +1522,12 @@ splatesWorker.command("drain").description("take no new agents on a worker, lett
1350
1522
  ps: ctx.ps,
1351
1523
  send: ctx.fetch
1352
1524
  }));
1353
- splatesWorker.command("remove").description("stop this machine’s worker taking agents, letting its agents run to their end; resume it with `splates worker drain local --undo`").arg("id", z15.string(), {
1525
+ 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", z16.string(), {
1354
1526
  description: "the worker, as `splates worker list` names it"
1355
- }).option("force", z15.boolean(), {
1527
+ }).option("force", z16.boolean(), {
1356
1528
  default: false,
1357
1529
  description: "end the agents on it now rather than wait for them"
1358
- }).option("dev", z15.string(), dev).action((opts, ctx) => removeWorker({
1530
+ }).option("dev", z16.string(), dev).action((opts, ctx) => removeWorker({
1359
1531
  id: opts.id,
1360
1532
  force: opts.force,
1361
1533
  dev: opts.dev,
@@ -1387,15 +1559,15 @@ function projectArgs(argv) {
1387
1559
  }
1388
1560
  return { argv };
1389
1561
  }
1390
- var splates = cli15("splates").use(exits());
1391
- splates.command("init").use(repository10({ creates: true, needs: "own" })).description("make this repository a splates project: set up its records under .git/splates when it has none, install the splates skill of this release with `bunx skills add`, and register it in ~/.splates/projects.json, so `splates server` serves it beside every other; run again, it changes only what is not so already").action((_opts, ctx) => init(ctx));
1562
+ var splates = cli16("splates").use(exits());
1563
+ splates.command("init").use(repository11({ 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));
1392
1564
  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) => {
1393
1565
  const format = await migrateRecords(ctx);
1394
1566
  if (ctx.ps.env(MIGRATING) === undefined) {
1395
1567
  ctx.log.info(`splates's records are in format ${format}`);
1396
1568
  }
1397
1569
  });
1398
- splates.command("update").use(recordsFirst()).use(repository10({ needs: "own" })).description("bring this repository up to the newest splates on npm: run `update` on that release when it is newer than this one, which moves the records under .git/splates to its format, as `splates migrate` does, unless an older `splates server` serves the repository, which it asks to restart onto that release once its agents have checkpointed, migrating them as it starts; then installs its splates skill with `bunx skills add` over the copy there, leaving one that links to your own; commit what changed").action(async (_opts, ctx) => {
1570
+ splates.command("update").use(recordsFirst()).use(repository11({ needs: "own" })).description("bring this repository up to the newest splates on npm: run `update` on that release when it is newer than this one, which moves the records under .git/splates to its format, as `splates migrate` does, unless an older `splates server` serves the repository, which it asks to restart onto that release once its agents have checkpointed, migrating them as it starts; then installs its splates skill with `bunx skills add` over the copy there, leaving one that links to your own; commit what changed").action(async (_opts, ctx) => {
1399
1571
  const code = await update({
1400
1572
  root: ctx.git.root,
1401
1573
  fs: ctx.fs,
@@ -1407,42 +1579,43 @@ splates.command("update").use(recordsFirst()).use(repository10({ needs: "own" })
1407
1579
  ctx.ps.exit(code);
1408
1580
  }
1409
1581
  });
1410
- splates.command("forget").use(repository10({ needs: "own" })).description("take this repository off ~/.splates/projects.json, so `splates server` no longer offers it; its records stay, and `splates init` brings it back").action((_opts, ctx) => forget(ctx));
1411
- splates.command("add").use(repository10({ needs: "tasks" })).description("start a task: a new one, or with --task one from the queue; create branch task/<slug>, a worktree at ../<repo>-splates.noindex/<slug> and a state record").arg("slug", z16.string(), {
1582
+ splates.command("refresh").description("bring the tree you stand in, a task's worktree included, up to this splates: install its splates skill with `bunx skills add` over the copy there and move what an older config holds to where it reads it, leaving the records and any `splates server` alone; what a project runs once a ship's bump task has installed a newer splates, as splates's `afterBump` names; commit what changed").action((_opts, ctx) => refreshHere(ctx));
1583
+ splates.command("forget").use(repository11({ 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));
1584
+ splates.command("add").use(repository11({ 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(), {
1412
1585
  description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
1413
- }).option("base", z16.string(), {
1586
+ }).option("base", z17.string(), {
1414
1587
  default: "",
1415
1588
  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"
1416
- }).option("task", z16.string(), {
1589
+ }).option("task", z17.string(), {
1417
1590
  default: "",
1418
1591
  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"
1419
- }).option("goal", z16.string(), {
1592
+ }).option("goal", z17.string(), {
1420
1593
  default: "",
1421
1594
  description: "one or two lines on what done means, written under `## Goal` in the task's SPLATES.md; required unless --task seeds the Goal"
1422
- }).option("agent", z16.string(), {
1595
+ }).option("agent", z17.string(), {
1423
1596
  default: "",
1424
1597
  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"
1425
- }).option("model", z16.string(), {
1598
+ }).option("model", z17.string(), {
1426
1599
  default: "",
1427
1600
  description: "with --agent: the model it works on, by its id or name on the roster (default: the agent's own)"
1428
- }).option("effort", z16.string(), {
1601
+ }).option("effort", z17.string(), {
1429
1602
  default: "",
1430
1603
  description: "with --agent: the thinking level it works at, one the agent offers (default: the agent's own)"
1431
- }).option("prompt", z16.string(), {
1604
+ }).option("prompt", z17.string(), {
1432
1605
  default: "",
1433
1606
  description: "with --agent: what to tell the agent beyond claiming the task"
1434
- }).option("held", z16.string(), {
1607
+ }).option("held", z17.string(), {
1435
1608
  default: "",
1436
1609
  description: "with --agent: the conversation `splates server`'s queue held for the queued task, which takes the task rather than a new one"
1437
1610
  }).action(async (opts, ctx) => {
1438
1611
  await refuseGroomer4(ctx.service, "add");
1439
1612
  if (opts.agent === "" && (opts.model !== "" || opts.effort !== "" || opts.prompt !== "" || opts.held !== "")) {
1440
- fail9("usage", "--model, --effort, --prompt and --held go with --agent", {
1613
+ fail10("usage", "--model, --effort, --prompt and --held go with --agent", {
1441
1614
  task: opts.slug
1442
1615
  });
1443
1616
  }
1444
1617
  if (opts.goal.trim() === "" && opts.task === "") {
1445
- fail9("usage", `say what done means: \`splates add ${opts.slug} --goal "<one or two lines>"\`, or start queued tasks with --task`, { task: opts.slug });
1618
+ fail10("usage", `say what done means: \`splates add ${opts.slug} --goal "<one or two lines>"\`, or start queued tasks with --task`, { task: opts.slug });
1446
1619
  }
1447
1620
  const dispatch = opts.agent === "" ? undefined : {
1448
1621
  agent: opts.agent,
@@ -1476,11 +1649,11 @@ splates.command("add").use(repository10({ needs: "tasks" })).description("start
1476
1649
  await serveProject(ctx);
1477
1650
  }
1478
1651
  });
1479
- splates.command("claim").use(repository10({ needs: "own" })).description("resume an existing task: take ownership of its worktree and print its path, status and any half-finished rebase; refuses while another agent holds the lease, but takes a stale one silently, so `splates show` first if the tree may not be abandoned").arg("task", z16.string(), { description: "task name" }).action(async (opts, ctx) => {
1652
+ splates.command("claim").use(repository11({ 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) => {
1480
1653
  await ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task), (claimed) => claimed);
1481
1654
  await follow(ctx, opts.task);
1482
1655
  });
1483
- splates.command("merge").use(repository10({ needs: "own" })).description("land this worktree's branch on its base (trunk unless created with --base): rebase onto it, run tests on the rebased code, fast-forward it in whichever worktree has it checked out, then discard the worktree, branch and record (linear history, never a merge commit, no flag to skip tests); requires committed work, refusing a dirty worktree").option("message", z16.string(), {
1656
+ splates.command("merge").use(repository11({ 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(), {
1484
1657
  default: "",
1485
1658
  description: "squash the task's commits into one with this message before landing, saying what the base gains in at least 5 characters"
1486
1659
  }).action(async (opts, ctx) => {
@@ -1495,39 +1668,75 @@ splates.command("merge").use(repository10({ needs: "own" })).description("land t
1495
1668
  await follow(ctx, null, { from: task });
1496
1669
  }
1497
1670
  });
1498
- splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and board tasks; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z16.string(), { description: "task name" }).option("force", z16.boolean(), {
1671
+ splates.command("remove").use(repository11({ 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", z17.string(), { description: "task name" }).option("force", z17.boolean(), {
1499
1672
  default: false,
1500
1673
  description: "discard even when another agent holds the lease"
1501
- }).option("reason", z16.optional(z16.enum(REMOVE_REASONS)), {
1502
- description: "why, for the Stats page: nothing (nothing to change), moot (no longer needed), superseded (done some other way), redo (better redone against current trunk), blocked (set aside until what it waits on lands, so back on the list)"
1674
+ }).option("reason", z17.optional(z17.enum(REMOVE_REASONS)), {
1675
+ 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, which takes --why and at least one blocker)"
1676
+ }).option("why", z17.string(), {
1677
+ default: "",
1678
+ description: "with --reason blocked: what you found that stops the work, in a sentence or two, for the person and whoever takes it next"
1679
+ }).repeated("blocked-by", z17.string(), {
1680
+ description: "with --reason blocked: the tasks it waits on to merge, comma separated or the flag repeated: a number for one here, <project>#<number> for one of another project"
1681
+ }).repeated("ask", z17.string(), {
1682
+ description: "with --reason blocked: a question only the person can answer, in the skill's question format; repeat the flag for each"
1683
+ }).repeated("waiting-on", z17.string(), {
1684
+ description: "with --reason blocked: something outside splates it waits on, such as a key or an upstream release; repeat the flag for each"
1685
+ }).option("detail", z17.string(), {
1686
+ default: "",
1687
+ description: "with --reason blocked: the task's detail rewritten for whoever takes it next: why it stopped, what you found, the steps done and left, and where any pushed work is"
1503
1688
  }).action(async (opts, ctx) => {
1504
- await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force, reason: opts.reason }), ({ released }) => ({ why: opts.reason ?? null, released }));
1505
- await follow(ctx, null, { from: opts.task });
1689
+ let __stack = [];
1690
+ try {
1691
+ const boards = __using(__stack, new OpenBoards2(ctx, ctx.projects), 0);
1692
+ const { blockedBy, blockedElsewhere } = await blockerFlags({
1693
+ boards,
1694
+ here: ctx.git.root,
1695
+ blockedBy: opts["blocked-by"]
1696
+ });
1697
+ await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, {
1698
+ force: opts.force,
1699
+ reason: opts.reason,
1700
+ deferral: {
1701
+ why: opts.why,
1702
+ blockedBy,
1703
+ blockedElsewhere,
1704
+ questions: opts.ask,
1705
+ outside: opts["waiting-on"],
1706
+ detail: opts.detail === "" ? undefined : opts.detail
1707
+ }
1708
+ }), ({ released }) => ({ why: opts.reason ?? null, released }));
1709
+ await follow(ctx, null, { from: opts.task });
1710
+ } catch (_catch) {
1711
+ var _err = _catch, _hasErr = 1;
1712
+ } finally {
1713
+ __callDispose(__stack, _err, _hasErr);
1714
+ }
1506
1715
  });
1507
- splates.command("list").use(repository10({ needs: "read" })).description("list every started task: its number, name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age; `splates task list` lists the queue").option("json", z16.boolean(), { default: false, description: "emit JSON" }).option("files", z16.boolean(), {
1716
+ splates.command("list").use(repository11({ 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(), {
1508
1717
  default: false,
1509
1718
  description: "add each task's Goal and changed files (committed or not), to check for overlap before starting"
1510
1719
  }).action((opts, ctx) => list(ctx, { json: opts.json, files: opts.files }));
1511
- splates.command("show").use(repository10({ needs: "read" })).description("read one task without touching it: everything `list` shows for it, plus the SPLATES.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree; for a task no longer live, the newest finished one of that name: how it ended, its base, the commit it landed as, its conversation and the SPLATES.md it ended on").arg("task", z16.string(), { description: "task name" }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
1512
- splates.command("wait").use(repository10({ needs: "read" })).description("block until a task stops moving: waiting on a person to answer its questions, or gone (merged or removed), or broken; takes no lease, and gives up with `timeout` rather than waiting forever").arg("task", z16.string(), { description: "task name" }).option("timeout-secs", z16.coerce.number(), {
1720
+ splates.command("show").use(repository11({ 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 }));
1721
+ splates.command("wait").use(repository11({ 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(), {
1513
1722
  default: DEFAULT_TIMEOUT3.secs,
1514
1723
  description: "how long to wait before giving up"
1515
1724
  }).action((opts, ctx) => {
1516
1725
  const timeout = Duration3.secs(opts["timeout-secs"]);
1517
1726
  return ctx.journal.waiting("wait", opts.task, { until: untilAfter3(timeout) }, () => wait(ctx, opts.task, { timeout }));
1518
1727
  });
1519
- splates.command("log").use(repository10({ needs: "read" })).description("show what has been done here recently: one line per add, claim, merge, remove, retry, queued task and more, with how it ended; outlives the tasks themselves").option("count", z16.coerce.number(), {
1728
+ splates.command("log").use(repository11({ 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(), {
1520
1729
  default: DEFAULT_COUNT,
1521
1730
  description: "how many entries to show"
1522
- }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
1731
+ }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
1523
1732
  splates.mount("server", splatesServer);
1524
- splates.command("ports").description("list the ports tasks serve, found by the running `splates server` as an editor finds ports to forward: each port a process in a task's worktree, or under its agent, listens on, with its link through the ports host `splates server --ports-host` saved, behind Cloudflare Access, and its link on this machine; give the user both").arg("task", z16.string(), {
1733
+ 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(), {
1525
1734
  default: "",
1526
1735
  description: "only this task's ports; every task's by default"
1527
- }).option("dev", z16.string(), {
1736
+ }).option("dev", z17.string(), {
1528
1737
  default: "",
1529
1738
  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"
1530
- }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
1739
+ }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
1531
1740
  task: opts.task,
1532
1741
  dev: opts.dev,
1533
1742
  json: opts.json,
@@ -1535,50 +1744,50 @@ splates.command("ports").description("list the ports tasks serve, found by the r
1535
1744
  ps: ctx.ps,
1536
1745
  send: ctx.fetch
1537
1746
  }));
1538
- splates.command("agents").description("print the roster of agents splates can start, kept for every project in ~/.splates/agents.json: each agent's launch command and models, with what each model is good at and what it costs, and the thinking levels `--effort` takes, as the agent offered them when last started, for choosing whom to delegate to; writes the defaults (Claude, Codex) there first when there is none, and `splates server` starts conversations with those it lists").option("json", z16.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => agents({
1747
+ 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({
1539
1748
  roster: rosterOf2(ctx),
1540
1749
  offered: await offeredOf(ctx).all(),
1541
1750
  log: ctx.log
1542
1751
  }, { json: opts.json }));
1543
- splates.command("setup").description("set splates up on this machine, one step at a time: with no step, each area's steps, checked from real state, and the one to do next; with a step, why it matters, the one thing the user does and what the agent does. Skips are kept in ~/.splates/setup.json, so a skipped step stops asking").arg("step", z16.string(), {
1752
+ 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(), {
1544
1753
  default: "",
1545
1754
  description: "a step's id; omit for every area's progress"
1546
- }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).option("skip", z16.boolean(), {
1755
+ }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).option("skip", z17.boolean(), {
1547
1756
  default: false,
1548
1757
  description: "with a step: stop asking for it"
1549
- }).option("unskip", z16.boolean(), {
1758
+ }).option("unskip", z17.boolean(), {
1550
1759
  default: false,
1551
1760
  description: "with a step: ask for it again"
1552
- }).option("undo", z16.boolean(), {
1761
+ }).option("undo", z17.boolean(), {
1553
1762
  default: false,
1554
1763
  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"
1555
1764
  }).action((opts, ctx) => setup(ctx, opts));
1556
- splates.command("path").use(repository10({ needs: "read" })).description("print a task's worktree path, or the main tree with no task; names another agent's tree without taking its lease").arg("task", z16.string(), {
1765
+ splates.command("path").use(repository11({ 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(), {
1557
1766
  default: "",
1558
1767
  description: "task name; omit for the main tree"
1559
1768
  }).action((opts, ctx) => path(ctx, opts.task || undefined));
1560
- splates.command("checkpoint").use(repository10({ needs: "own" })).description("save this task to the central remote (`remote` in splates.config.ts) before a stop: commit what is uncommitted as a `WIP: checkpoint` commit, to squash before merging, and push the branch as `splates/<task>`, so a worker that dies loses nothing; with no remote configured, leaves the tree as it is").action(async (_opts, ctx) => {
1769
+ splates.command("checkpoint").use(repository11({ 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) => {
1561
1770
  await checkpoint(ctx, ctx.ps.cwd());
1562
1771
  });
1563
- splates.command("attach").use(repository10({ needs: "own" })).description("store screenshots and recordings for this task's questions to show: prints the path each was stored at, under .git/splates, which the `splates server` page shows inline; from a conversation (its $SPLATES_CONVERSATION, or --conversation) they stay with it past the task, otherwise they go when the task merges or is removed; those the task's final SPLATES.md shows are copied to keep with its finished record. A task holds at most attachmentCapacity bytes; with no files, lists what it holds. A file the user will come back for on its own is an artifact instead (`splates artifact add`)").arg("files", z16.string(), {
1772
+ splates.command("attach").use(repository11({ 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(), {
1564
1773
  default: "",
1565
1774
  description: "files to store, comma separated"
1566
- }).option("task", z16.string(), {
1775
+ }).option("task", z17.string(), {
1567
1776
  default: "",
1568
1777
  description: "task name, when run outside its worktree"
1569
- }).option("remove", z16.string(), {
1778
+ }).option("remove", z17.string(), {
1570
1779
  default: "",
1571
1780
  description: "files it holds to drop first, comma separated, by path or stored name"
1572
- }).option("conversation", z16.string(), {
1781
+ }).option("conversation", z17.string(), {
1573
1782
  default: "",
1574
1783
  description: "conversation to keep the files with, when $SPLATES_CONVERSATION is not set"
1575
- }).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList5(opts.files), ctx.ps.cwd(), {
1784
+ }).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList6(opts.files), ctx.ps.cwd(), {
1576
1785
  task: opts.task || undefined,
1577
- remove: commaList5(opts.remove),
1786
+ remove: commaList6(opts.remove),
1578
1787
  conversation: opts.conversation || undefined
1579
- }), (stored) => ({ stored, removed: commaList5(opts.remove) })));
1580
- splates.command("retry").use(repository10({ needs: "own" })).description("give a task that spent its merge attempts another mergeRetryCount of them; the way out of `budget_exhausted` that is not remove and redo, for a person to run once they have seen the tree").arg("task", z16.string(), { description: "task name" }).action((opts, ctx) => recordRetry(ctx, opts.task));
1581
- splates.mount("artifact", splatesArtifact).mount("task", splatesTask).mount("lane", splatesLane).mount("projects", splatesProjects).mount("creds", splatesCreds).mount("tunnel", splatesTunnel).mount("slots", splatesSlots).mount("worker", splatesWorker).mount("queue", splatesQueue).mount("hook", splatesHook).mount("replica", splatesReplica).mount("evals", splatesEvals);
1788
+ }), (stored) => ({ stored, removed: commaList6(opts.remove) })));
1789
+ splates.command("retry").use(repository11({ 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));
1790
+ splates.mount("artifact", splatesArtifact).mount("task", splatesTask).mount("lane", splatesLane).mount("ship", splatesShip).mount("projects", splatesProjects).mount("creds", splatesCreds).mount("tunnel", splatesTunnel).mount("slots", splatesSlots).mount("worker", splatesWorker).mount("queue", splatesQueue).mount("hook", splatesHook).mount("replica", splatesReplica).mount("evals", splatesEvals);
1582
1791
 
1583
1792
  // index.ts
1584
1793
  if (basename2(process.argv[1] ?? "") === LEGACY) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stringsync/splates",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "description": "Runs several AI coding agents on one repository at once, each in its own git worktree",
5
5
  "license": "MIT",
6
6
  "author": "stringsync LLC",
@@ -17,8 +17,8 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@stringsync/splates-server": "^0.0.58",
21
- "webappwiz": "0.0.50",
20
+ "@stringsync/splates-server": "^0.0.60",
21
+ "webappwiz": "0.0.54",
22
22
  "zod": "^4.4.3"
23
23
  },
24
24
  "main": "./index.js",
package/ship.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { SplatesDeps } from "@stringsync/splates-server/server";
2
+ /** `splates ship [project]` prints the Ships page link for the person to open. */
3
+ export declare const splatesShip: import("webappwiz/cmd").Cli<SplatesDeps, SplatesDeps>;