@stringsync/splates 0.0.58 → 0.0.59

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,15 +96,18 @@ import {
59
96
  MIGRATING,
60
97
  MIGRATION_LIMIT,
61
98
  migrateRecords,
99
+ OpenBoards as OpenBoards2,
62
100
  repository as repository10
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
70
109
  } from "@stringsync/splates-server/server";
110
+ import { ship } from "@stringsync/splates-server/ship";
71
111
  import { addFactTasks } from "@stringsync/splates-server/stats";
72
112
  import {
73
113
  add,
@@ -244,9 +284,87 @@ splatesArtifact.command("remove").use(repository({ needs: "own" })).description(
244
284
  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
285
  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
286
 
287
+ // blockers.ts
288
+ import { commaList as commaList3 } from "@stringsync/splates-server/comma-list";
289
+ import { fail as fail3 } from "@stringsync/splates-server/exit";
290
+ async function blockerFlags({
291
+ boards,
292
+ here,
293
+ blockedBy = [],
294
+ removeBlockedBy = []
295
+ }) {
296
+ const ids = [];
297
+ const blockedElsewhere = [];
298
+ for (const named of await namedIn(boards, here, blockedBy)) {
299
+ if (named.project === null) {
300
+ ids.push(named.id);
301
+ continue;
302
+ }
303
+ const found = await boards.find(named.project, named.id);
304
+ if (found === null) {
305
+ fail3("not_found", `the tasks of ${named.written} cannot be read: is ${named.slug} set up with \`splates init\`?`, { blockedBy: named.written });
306
+ }
307
+ if (found.status === "merged" || found.status === "removed") {
308
+ 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 });
309
+ }
310
+ blockedElsewhere.push({
311
+ project: named.project,
312
+ id: named.id,
313
+ subject: found.subject
314
+ });
315
+ }
316
+ const unblocked = { ids: [], elsewhere: [] };
317
+ for (const named of await namedIn(boards, here, removeBlockedBy)) {
318
+ if (named.project === null) {
319
+ unblocked.ids.push(named.id);
320
+ } else {
321
+ unblocked.elsewhere.push({ project: named.project, id: named.id });
322
+ }
323
+ }
324
+ return { blockedBy: ids, blockedElsewhere, unblocked };
325
+ }
326
+ function id(digits) {
327
+ const parsed = Number(digits);
328
+ if (!Number.isInteger(parsed) || parsed <= 0) {
329
+ fail3("usage", `'${digits}' is not a task number`, { number: digits });
330
+ }
331
+ return parsed;
332
+ }
333
+ async function namedIn(boards, here, raw) {
334
+ const named = [];
335
+ for (const value of commaList3(raw.join(","))) {
336
+ const local = /^#?(\d+)$/.exec(value);
337
+ if (local !== null) {
338
+ named.push({
339
+ project: null,
340
+ slug: "",
341
+ id: id(local[1]),
342
+ written: value
343
+ });
344
+ continue;
345
+ }
346
+ const other = /^#?([\w.-]+)(?:\s+task)?\s*#(\d+)$/i.exec(value);
347
+ if (other === null) {
348
+ fail3("usage", `'${value}' is not a task: pass its number, or <project>#<number> for one of another project`, { blockedBy: value });
349
+ }
350
+ const slug = other[1];
351
+ const root = await boards.root(slug);
352
+ if (root === null) {
353
+ fail3("not_found", `no splates project '${slug}' on this machine: \`splates projects\` lists them`, { blockedBy: value });
354
+ }
355
+ named.push({
356
+ project: root === here ? null : root,
357
+ slug,
358
+ id: id(other[2]),
359
+ written: `${slug} #${other[2]}`
360
+ });
361
+ }
362
+ return named;
363
+ }
364
+
247
365
  // creds.ts
248
366
  import { DEFAULT_TIMEOUT, PROVIDERS } from "@stringsync/splates-server/creds";
249
- import { fail as fail3 } from "@stringsync/splates-server/exit";
367
+ import { fail as fail4 } from "@stringsync/splates-server/exit";
250
368
  import { repository as repository2 } from "@stringsync/splates-server/records";
251
369
  import { untilAfter } from "@stringsync/splates-server/tasks";
252
370
  import { cli as cli2 } from "webappwiz/cmd";
@@ -296,7 +414,7 @@ splatesCreds.command("wait").use(repository2({ needs: "own" })).description("blo
296
414
  await ctx.journal.waiting("creds wait", null, { name: opts.name, until: untilAfter(timeout) }, async () => {
297
415
  const ended = await ctx.creds.wait(opts.name, { timeout });
298
416
  if (ended === "dismissed") {
299
- fail3("dismissed", `${opts.name} was dismissed: carry on without it, or ask the person why`, { name: opts.name });
417
+ fail4("dismissed", `${opts.name} was dismissed: carry on without it, or ask the person why`, { name: opts.name });
300
418
  }
301
419
  });
302
420
  ctx.log.info(`${color.green("saved")} ${opts.name}`);
@@ -328,7 +446,7 @@ import {
328
446
  inspectBatch,
329
447
  writeImported
330
448
  } from "@stringsync/splates-server/evals";
331
- import { fail as fail4 } from "@stringsync/splates-server/exit";
449
+ import { fail as fail5 } from "@stringsync/splates-server/exit";
332
450
  import { cli as cli3 } from "webappwiz/cmd";
333
451
  import { color as color2 } from "webappwiz/log";
334
452
  import { z as z4 } from "zod";
@@ -344,7 +462,7 @@ splatesEvals.command("import").description("turn a framework's results into a sp
344
462
  const file = resolve3(ctx.ps.cwd(), opts.file);
345
463
  const dir = resolve3(ctx.ps.cwd(), opts["batch-dir"]);
346
464
  if (!await ctx.fs.exists(file)) {
347
- fail4("not_found", `no file at ${file}`, { file });
465
+ fail5("not_found", `no file at ${file}`, { file });
348
466
  }
349
467
  const imported = inspectBatch(await inspectLog(ctx, file), opts["pass-at"]);
350
468
  await writeImported(dir, imported);
@@ -358,12 +476,12 @@ async function inspectLog(ctx, file) {
358
476
  return log;
359
477
  }
360
478
  } catch {}
361
- return fail4("usage", `${file} is no Inspect eval log`, { file });
479
+ return fail5("usage", `${file} is no Inspect eval log`, { file });
362
480
  }
363
481
  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"));
482
+ 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
483
  if (exitCode !== 0) {
366
- fail4("usage", `inspect log dump ${file} failed: ${stderr.trim()}`, {
484
+ fail5("usage", `inspect log dump ${file} failed: ${stderr.trim()}`, {
367
485
  file
368
486
  });
369
487
  }
@@ -371,8 +489,8 @@ async function dumped(ctx, file) {
371
489
  }
372
490
 
373
491
  // hook.ts
374
- import { commaList as commaList3 } from "@stringsync/splates-server/comma-list";
375
- import { fail as fail5 } from "@stringsync/splates-server/exit";
492
+ import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
493
+ import { fail as fail6 } from "@stringsync/splates-server/exit";
376
494
  import {
377
495
  HookStore,
378
496
  hookAdd,
@@ -425,13 +543,13 @@ splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run
425
543
  let hook;
426
544
  if (opts.run !== "") {
427
545
  if (pick != null || opts.prompt !== "") {
428
- fail5("usage", "--run goes alone: a shell command runs no agent", {});
546
+ fail6("usage", "--run goes alone: a shell command runs no agent", {});
429
547
  }
430
548
  hook = { kind: "run", command: opts.run };
431
549
  } else if (pick == null) {
432
- fail5("usage", "say what runs: --agent with --prompt, or --run", {});
550
+ fail6("usage", "say what runs: --agent with --prompt, or --run", {});
433
551
  } else if (opts.prompt === "") {
434
- fail5("usage", "an agent hook needs --prompt: what to tell it", {});
552
+ fail6("usage", "an agent hook needs --prompt: what to tell it", {});
435
553
  } else {
436
554
  hook = { kind: "agent", prompt: opts.prompt, ...pick };
437
555
  }
@@ -471,7 +589,7 @@ splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("ch
471
589
  const { id, agent, model, effort, ...change } = opts;
472
590
  const pick = await assigneeIn(ctx, agent, model, effort);
473
591
  if (pick === null) {
474
- fail5("usage", "an agent automation needs an agent: name one, or --run", {});
592
+ fail6("usage", "an agent automation needs an agent: name one, or --run", {});
475
593
  }
476
594
  await hookEdit({ hooks: storeOf(ctx), log: ctx.log }, id, {
477
595
  ...change,
@@ -501,24 +619,24 @@ splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description(
501
619
  }).action(async (opts, ctx) => {
502
620
  const pick = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
503
621
  if (opts.off === (pick != null)) {
504
- fail5("usage", "say how it runs with --agent, or turn it off with --off", {});
622
+ fail6("usage", "say how it runs with --agent, or turn it off with --off", {});
505
623
  }
506
624
  await hookGroomer({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null);
507
625
  });
508
626
  function triggerOf(opts) {
509
627
  const given = [opts.event, opts.every, opts.cron].filter((each) => each !== undefined && each !== "");
510
628
  if (given.length !== 1) {
511
- fail5("usage", "say what sets it off: a task event, --every or --cron, one of them", {});
629
+ fail6("usage", "say what sets it off: a task event, --every or --cron, one of them", {});
512
630
  }
513
631
  if (opts.event === undefined || opts.event === "") {
514
632
  if (opts.stage !== "" || opts.lane !== "") {
515
- fail5("usage", "--stage and --lane narrow a task event, not a schedule", {});
633
+ fail6("usage", "--stage and --lane narrow a task event, not a schedule", {});
516
634
  }
517
635
  return opts.every !== "" ? { every: opts.every } : { cron: opts.cron };
518
636
  }
519
637
  const filter = {};
520
638
  if (opts.stage !== "") {
521
- filter.stages = stagesNamed(commaList3(opts.stage));
639
+ filter.stages = stagesNamed(commaList4(opts.stage));
522
640
  }
523
641
  if (opts.lane !== "") {
524
642
  filter.lanes = lanesNamed(opts.lane);
@@ -672,7 +790,7 @@ for (const command of ["pause", "resume"]) {
672
790
  import { dirname as dirname2 } from "node:path";
673
791
  import { loadConfig } from "@stringsync/splates-server/config";
674
792
  import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
675
- import { fail as fail6 } from "@stringsync/splates-server/exit";
793
+ import { fail as fail7 } from "@stringsync/splates-server/exit";
676
794
  import { Projects } from "@stringsync/splates-server/projects";
677
795
  import {
678
796
  FORMAT,
@@ -714,7 +832,7 @@ splatesReplica.command("restore").description("with `splates server` stopped, re
714
832
  ]);
715
833
  const config = await loadConfig(root, { fs: ctx.fs });
716
834
  if (config.replica === null) {
717
- fail6("usage", "no replica: name one as `replica` in splates.config.ts first");
835
+ fail7("usage", "no replica: name one as `replica` in splates.config.ts first");
718
836
  }
719
837
  const replica = openReplica(config.replica, {
720
838
  root,
@@ -741,7 +859,7 @@ splatesReplica.command("restore").description("with `splates server` stopped, re
741
859
  ].join(`
742
860
  `));
743
861
  } catch (error) {
744
- fail6("usage", error instanceof Error ? error.message : String(error));
862
+ fail7("usage", error instanceof Error ? error.message : String(error));
745
863
  }
746
864
  });
747
865
  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 +880,7 @@ async function mainTree(ctx) {
762
880
  "--git-common-dir"
763
881
  ]);
764
882
  if (exitCode !== 0) {
765
- fail6("usage", `not a git repository: ${ctx.ps.cwd()}`);
883
+ fail7("usage", `not a git repository: ${ctx.ps.cwd()}`);
766
884
  }
767
885
  return dirname2(stdout.trim());
768
886
  }
@@ -807,7 +925,7 @@ splatesServer.command("run").description("serve every splates project on this ma
807
925
  // setup.ts
808
926
  import { dirname as dirname3 } from "node:path";
809
927
  import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
810
- import { fail as fail7 } from "@stringsync/splates-server/exit";
928
+ import { fail as fail8 } from "@stringsync/splates-server/exit";
811
929
  import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
812
930
  import { Projects as Projects2 } from "@stringsync/splates-server/projects";
813
931
  import {
@@ -823,15 +941,15 @@ import { fetchWorkers } from "@stringsync/splates-server/workers";
823
941
  import { color as color3 } from "webappwiz/log";
824
942
  async function setup(deps, opts) {
825
943
  if (opts.step === "" && (opts.skip || opts.unskip || opts.undo)) {
826
- fail7("usage", "--skip and --unskip name a step, --undo an area", {});
944
+ fail8("usage", "--skip and --unskip name a step, --undo an area", {});
827
945
  }
828
946
  if (opts.undo && (opts.skip || opts.unskip)) {
829
- fail7("usage", "--undo names an area, not a step to skip", {
947
+ fail8("usage", "--undo names an area, not a step to skip", {
830
948
  area: opts.step
831
949
  });
832
950
  }
833
951
  if (opts.skip && opts.unskip) {
834
- fail7("usage", "--skip or --unskip, not both", { step: opts.step });
952
+ fail8("usage", "--skip or --unskip, not both", { step: opts.step });
835
953
  }
836
954
  const machine = await setupOf(deps);
837
955
  const { log } = deps;
@@ -1024,10 +1142,10 @@ import {
1024
1142
  tasksNamed as tasksNamed2,
1025
1143
  taskUpdate
1026
1144
  } from "@stringsync/splates-server/board";
1027
- import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
1145
+ import { commaList as commaList5 } from "@stringsync/splates-server/comma-list";
1028
1146
  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";
1147
+ import { fail as fail9 } from "@stringsync/splates-server/exit";
1148
+ import { OpenBoards, repository as repository8 } from "@stringsync/splates-server/records";
1031
1149
  import {
1032
1150
  DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
1033
1151
  taskWait,
@@ -1056,7 +1174,7 @@ function adding(name, description, ready) {
1056
1174
  }).repeated("file", z12.string(), {
1057
1175
  description: "files to attach, comma separated or the flag repeated: copied under .git/splates/tasks/<n>/"
1058
1176
  }).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"
1177
+ 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"
1060
1178
  }).option("lane", z12.string(), {
1061
1179
  default: "",
1062
1180
  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)"
@@ -1070,28 +1188,43 @@ function adding(name, description, ready) {
1070
1188
  default: "",
1071
1189
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
1072
1190
  }).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 });
1191
+ let __stack = [];
1192
+ try {
1193
+ if (ctx.elsewhere !== null && opts.lane !== "") {
1194
+ 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 });
1195
+ }
1196
+ const from = await here(ctx);
1197
+ const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
1198
+ const origin = ctx.elsewhere === null ? undefined : {
1199
+ project: ctx.elsewhere,
1200
+ task: ctx.project === undefined ? null : ctx.service.taskFor(await ctx.git.currentBranch(ctx.ps.cwd()))
1201
+ };
1202
+ const project = await projectFrom(ctx);
1203
+ const boards = __using(__stack, new OpenBoards(ctx, ctx.projects), 0);
1204
+ const { blockedBy, blockedElsewhere } = await blockerFlags({
1205
+ boards,
1206
+ here: ctx.git.root,
1207
+ blockedBy: opts["blocked-by"]
1208
+ });
1209
+ await ctx.journal.record("task add", from, () => taskAdd(ctx, opts.subject, from, {
1210
+ slug: opts.slug,
1211
+ text: opts.text,
1212
+ position: opts.position,
1213
+ files: commaList5(opts.file.join(",")),
1214
+ blockedBy,
1215
+ blockedElsewhere,
1216
+ lane: laneIn(opts.lane) ?? null,
1217
+ assignee,
1218
+ origin,
1219
+ ready,
1220
+ project,
1221
+ boards
1222
+ }), addedFacts);
1223
+ } catch (_catch) {
1224
+ var _err = _catch, _hasErr = 1;
1225
+ } finally {
1226
+ __callDispose(__stack, _err, _hasErr);
1075
1227
  }
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
1228
  });
1096
1229
  }
1097
1230
  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);
@@ -1108,11 +1241,23 @@ splatesTask.command("list").use(repository8({ needs: "read" })).description("eve
1108
1241
  ready: opts.ready,
1109
1242
  project: await projectFrom(ctx)
1110
1243
  }));
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
- }));
1244
+ 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) => {
1245
+ let __stack = [];
1246
+ try {
1247
+ const boards = __using(__stack, new OpenBoards(ctx, ctx.projects), 0);
1248
+ await taskShow(ctx, opts.id, {
1249
+ json: opts.json,
1250
+ project: await projectFrom(ctx),
1251
+ others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug),
1252
+ boards,
1253
+ root: ctx.git.root
1254
+ });
1255
+ } catch (_catch) {
1256
+ var _err = _catch, _hasErr = 1;
1257
+ } finally {
1258
+ __callDispose(__stack, _err, _hasErr);
1259
+ }
1260
+ });
1116
1261
  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(), {
1117
1262
  default: "",
1118
1263
  description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
@@ -1129,9 +1274,9 @@ splatesTask.command("update").use(repository8({ needs: "tasks" })).description("
1129
1274
  }).repeated("remove-file", z12.string(), {
1130
1275
  description: "attached files to drop, comma separated or the flag repeated, by path or stored name"
1131
1276
  }).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"
1277
+ 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"
1133
1278
  }).repeated("remove-blocked-by", z12.string(), {
1134
- description: "numbers of tasks no longer blocking it, comma separated or the flag repeated"
1279
+ description: "tasks no longer blocking it, as --blocked-by names them, comma separated or the flag repeated"
1135
1280
  }).option("lane", z12.string(), {
1136
1281
  default: "",
1137
1282
  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"
@@ -1154,25 +1299,42 @@ splatesTask.command("update").use(repository8({ needs: "tasks" })).description("
1154
1299
  default: false,
1155
1300
  description: "let a queued lane queue it again, undoing --manual"
1156
1301
  }).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
- }));
1302
+ let __stack = [];
1303
+ try {
1304
+ if (opts.manual && opts.auto) {
1305
+ fail9("usage", "pass --manual or --auto, not both", { number: opts.id });
1306
+ }
1307
+ await refuseGroomer3(ctx.service, "task update", await ctx.tasks.find(opts.id));
1308
+ const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
1309
+ const boards = __using(__stack, new OpenBoards(ctx, ctx.projects), 0);
1310
+ const { blockedBy, blockedElsewhere, unblocked } = await blockerFlags({
1311
+ boards,
1312
+ here: ctx.git.root,
1313
+ blockedBy: opts["blocked-by"],
1314
+ removeBlockedBy: opts["remove-blocked-by"]
1315
+ });
1316
+ await recordUpdate(ctx.journal, ctx.tasks, opts.id, () => taskUpdate(ctx, opts.id, {
1317
+ subject: opts.subject,
1318
+ slug: opts.slug,
1319
+ text: opts.text,
1320
+ position: opts.position,
1321
+ files: commaList5(opts.file.join(",")),
1322
+ removeFiles: commaList5(opts["remove-file"].join(",")),
1323
+ blockedBy,
1324
+ removeBlockedBy: unblocked.ids,
1325
+ blockedElsewhere,
1326
+ removeElsewhere: unblocked.elsewhere,
1327
+ lane: laneIn(opts.lane),
1328
+ assignee,
1329
+ ready: opts.ready,
1330
+ manual: opts.manual ? true : opts.auto ? false : undefined,
1331
+ boards
1332
+ }));
1333
+ } catch (_catch) {
1334
+ var _err = _catch, _hasErr = 1;
1335
+ } finally {
1336
+ __callDispose(__stack, _err, _hasErr);
1337
+ }
1176
1338
  });
1177
1339
  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) => {
1178
1340
  await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
@@ -1407,6 +1569,7 @@ splates.command("update").use(recordsFirst()).use(repository10({ needs: "own" })
1407
1569
  ctx.ps.exit(code);
1408
1570
  }
1409
1571
  });
1572
+ 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));
1410
1573
  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
1574
  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(), {
1412
1575
  description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
@@ -1437,12 +1600,12 @@ splates.command("add").use(repository10({ needs: "tasks" })).description("start
1437
1600
  }).action(async (opts, ctx) => {
1438
1601
  await refuseGroomer4(ctx.service, "add");
1439
1602
  if (opts.agent === "" && (opts.model !== "" || opts.effort !== "" || opts.prompt !== "" || opts.held !== "")) {
1440
- fail9("usage", "--model, --effort, --prompt and --held go with --agent", {
1603
+ fail10("usage", "--model, --effort, --prompt and --held go with --agent", {
1441
1604
  task: opts.slug
1442
1605
  });
1443
1606
  }
1444
1607
  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 });
1608
+ 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
1609
  }
1447
1610
  const dispatch = opts.agent === "" ? undefined : {
1448
1611
  agent: opts.agent,
@@ -1499,10 +1662,46 @@ splates.command("remove").use(repository10({ needs: "tasks" })).description("dis
1499
1662
  default: false,
1500
1663
  description: "discard even when another agent holds the lease"
1501
1664
  }).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)"
1665
+ 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)"
1666
+ }).option("why", z16.string(), {
1667
+ default: "",
1668
+ description: "with --reason blocked: what you found that stops the work, in a sentence or two, for the person and whoever takes it next"
1669
+ }).repeated("blocked-by", z16.string(), {
1670
+ 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"
1671
+ }).repeated("ask", z16.string(), {
1672
+ description: "with --reason blocked: a question only the person can answer, in the skill's question format; repeat the flag for each"
1673
+ }).repeated("waiting-on", z16.string(), {
1674
+ description: "with --reason blocked: something outside splates it waits on, such as a key or an upstream release; repeat the flag for each"
1675
+ }).option("detail", z16.string(), {
1676
+ default: "",
1677
+ 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
1678
  }).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 });
1679
+ let __stack = [];
1680
+ try {
1681
+ const boards = __using(__stack, new OpenBoards2(ctx, ctx.projects), 0);
1682
+ const { blockedBy, blockedElsewhere } = await blockerFlags({
1683
+ boards,
1684
+ here: ctx.git.root,
1685
+ blockedBy: opts["blocked-by"]
1686
+ });
1687
+ await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, {
1688
+ force: opts.force,
1689
+ reason: opts.reason,
1690
+ deferral: {
1691
+ why: opts.why,
1692
+ blockedBy,
1693
+ blockedElsewhere,
1694
+ questions: opts.ask,
1695
+ outside: opts["waiting-on"],
1696
+ detail: opts.detail === "" ? undefined : opts.detail
1697
+ }
1698
+ }), ({ released }) => ({ why: opts.reason ?? null, released }));
1699
+ await follow(ctx, null, { from: opts.task });
1700
+ } catch (_catch) {
1701
+ var _err = _catch, _hasErr = 1;
1702
+ } finally {
1703
+ __callDispose(__stack, _err, _hasErr);
1704
+ }
1506
1705
  });
1507
1706
  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(), {
1508
1707
  default: false,
@@ -1572,12 +1771,42 @@ splates.command("attach").use(repository10({ needs: "own" })).description("store
1572
1771
  }).option("conversation", z16.string(), {
1573
1772
  default: "",
1574
1773
  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(), {
1774
+ }).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList6(opts.files), ctx.ps.cwd(), {
1576
1775
  task: opts.task || undefined,
1577
- remove: commaList5(opts.remove),
1776
+ remove: commaList6(opts.remove),
1578
1777
  conversation: opts.conversation || undefined
1579
- }), (stored) => ({ stored, removed: commaList5(opts.remove) })));
1778
+ }), (stored) => ({ stored, removed: commaList6(opts.remove) })));
1580
1779
  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));
1780
+ splates.command("ship").use(repository10({ needs: "read" })).description("ship a project and its cascade: `splates server` runs its `ship` command from splates.config.ts on its machine, under your own npm sign-in, signing in with `npm login --auth-type=web` first when npm knows no one, and shows npm's sign-in and 2FA links on the page for any device; this prints that ship's output until it ends. The server then carries the release through every registered project that depends on it, in dependency order: a bump task queued in each once all it takes from are shipped, and, once that merges, each public one with a `ship` command and dependents of its own ships in turn. A bump that asks you, or is removed, stops the cascade at that project until it merges or you retry or drop it, as does a failed ship. While a cascade is under way, this prints where it stands instead of starting another, or changes one of its pending publishes with `--bump`. `--plan` prints the cascade and runs nothing. The person's alone to start, refused an agent, which asks with a run-command card").arg("project", z16.string(), {
1781
+ default: "",
1782
+ description: "the registered project's slug; this one by default"
1783
+ }).option("plan", z16.boolean(), {
1784
+ default: false,
1785
+ description: "print the plan without running anything"
1786
+ }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).option("dev", z16.string(), {
1787
+ default: "",
1788
+ description: "the `splates server` to run the ship on, a URL such as http://localhost:4310; by default the one from SPLATES_DEV, else this machine's on its default port"
1789
+ }).option("bump", z16.optional(z16.enum(["patch", "minor", "major"])), {
1790
+ description: "how far to move the starting project's version, patch by default; while its cascade runs, change this pending publish instead"
1791
+ }).option("drop", z16.string(), {
1792
+ default: "",
1793
+ description: "take this registered project out of the cascade under way, so those after it go on without its new version"
1794
+ }).option("retry", z16.string(), {
1795
+ default: "",
1796
+ description: "run this registered project's part of the cascade under way again: its ship again when that failed, or a new bump task when its last was removed"
1797
+ }).option("abandon", z16.boolean(), {
1798
+ default: false,
1799
+ description: "end the cascade under way: what shipped stays published, a running ship is stopped, its open bump tasks are removed, and history files it as abandoned"
1800
+ }).action((opts, ctx) => ship(ctx, {
1801
+ project: opts.project || undefined,
1802
+ plan: opts.plan,
1803
+ json: opts.json,
1804
+ dev: opts.dev,
1805
+ bump: opts.bump,
1806
+ drop: opts.drop || undefined,
1807
+ retry: opts.retry || undefined,
1808
+ abandon: opts.abandon
1809
+ }));
1581
1810
  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);
1582
1811
 
1583
1812
  // index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stringsync/splates",
3
- "version": "0.0.58",
3
+ "version": "0.0.59",
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.59",
21
+ "webappwiz": "0.0.53",
22
22
  "zod": "^4.4.3"
23
23
  },
24
24
  "main": "./index.js",