@stringsync/splates 0.0.51 → 0.0.52

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.
Files changed (2) hide show
  1. package/index.js +68 -47
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  MIGRATING,
26
26
  MIGRATION_LIMIT,
27
27
  migrateRecords,
28
- repository as repository9
28
+ repository as repository10
29
29
  } from "@stringsync/splates-server/records";
30
30
  import {
31
31
  newest,
@@ -358,8 +358,8 @@ import { fail as fail4 } from "@stringsync/splates-server/exit";
358
358
  import {
359
359
  HookStore,
360
360
  hookAdd,
361
- hookAudit,
362
361
  hookEdit,
362
+ hookGardener,
363
363
  hookGroomer,
364
364
  hookList,
365
365
  hookPause,
@@ -484,7 +484,7 @@ splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description(
484
484
  }
485
485
  await hookGroomer({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null);
486
486
  });
487
- splatesHook.command("audit").use(repository3({ needs: "tasks" })).description("turn on the notes audit, built-in automation `notes-audit`, or set the agent it runs as and its schedule, as the Automations page does: on each run, an agent in the main tree reads each project note no open task names by its path, checks it against the code and the other notes, and defers a task for each one gone stale or contradicting another, never fixing one itself. Off by default, since each run reads code for every note on your tokens; it runs every 7d until you set `--every` or `--cron`. `--off` turns it off").option("agent", z5.string(), {
487
+ splatesHook.command("gardener").use(repository3({ needs: "tasks" })).description("turn on the notes gardener, built-in automation `notes-gardener`, or set the agent it runs as and its schedule, as the Automations page does: on each run, an agent reads each project note no open task names by its path, checks it against the code and the other notes, and drafts the fixes in one task, `notes-garden`: rewriting what went stale, merging notes that say one thing, splitting one that says several, settling a contradiction the code settles and sharpening descriptions, then asks you to review the change rather than merge it, asking about any contradiction the code does not settle. No run starts while `notes-garden` is open. Off by default, since each run reads code for every note on your tokens; it runs every 7d until you set `--every` or `--cron`. `--off` turns it off").option("agent", z5.string(), {
488
488
  default: "",
489
489
  description: "the agent it runs as, as `splates agents` names it"
490
490
  }).option("model", z5.string(), {
@@ -507,7 +507,7 @@ splatesHook.command("audit").use(repository3({ needs: "tasks" })).description("t
507
507
  if (opts.off === (pick != null)) {
508
508
  fail4("usage", "say how it runs with --agent, or turn it off with --off", {});
509
509
  }
510
- await hookAudit({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null, {
510
+ await hookGardener({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null, {
511
511
  every: opts.every,
512
512
  cron: opts.cron
513
513
  });
@@ -674,6 +674,7 @@ splatesQueue.command("move").use(repository6({ needs: "tasks" })).description("m
674
674
  import {
675
675
  BoxShell,
676
676
  CoordinatorBox as CoordinatorBox2,
677
+ coordinatorMoved,
677
678
  deployKey,
678
679
  machineMove,
679
680
  neededCreds
@@ -685,7 +686,6 @@ import {
685
686
  RUNNING,
686
687
  serveDev
687
688
  } from "@stringsync/splates-server/server";
688
- import { coordinatorMoved } from "@stringsync/splates-server/setup";
689
689
  import { LOCAL_SLOTS } from "@stringsync/splates-server/workers";
690
690
  import { cli as cli9 } from "webappwiz/cmd";
691
691
  import { z as z10 } from "zod";
@@ -694,26 +694,35 @@ import { z as z10 } from "zod";
694
694
  import { resolve as resolve2 } from "node:path";
695
695
  import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
696
696
  import {
697
+ ACCESS_ENVIRONMENT,
698
+ accessEnvironmentExists,
697
699
  CoordinatorBox,
700
+ pulumiOrg,
698
701
  writeInfraConfig
699
702
  } from "@stringsync/splates-server/coordinator";
700
703
  import { fail as fail5 } from "@stringsync/splates-server/exit";
701
- import { infraInit } from "@stringsync/splates-server/infra/init";
704
+ import { infraConfigure } from "@stringsync/splates-server/infra/configure";
705
+ import { Projects } from "@stringsync/splates-server/projects";
706
+ import { repository as repository7 } from "@stringsync/splates-server/records";
707
+ import { savedTunnelOf } from "@stringsync/splates-server/server";
708
+ import { FsStorage as FsStorage2 } from "@stringsync/splates-server/storage";
702
709
  import { cli as cli8 } from "webappwiz/cmd";
703
710
  import { z as z9 } from "zod";
704
711
  var splatesInfra = cli8("infra").description("the Pulumi project that makes everything the server's box needs in AWS, Cloudflare and GitHub, which `splates server deploy` then installs splates on");
705
- splatesInfra.command("init").description("write a Pulumi project in <dir>, its program a dozen lines using the components of @stringsync/splates-server/infra, and the stack's config from the flags; run again, it writes only what is not so already and keeps values you changed. It prints each id and secret left to set with `pulumi config set`. It records the directory in ~/.splates/coordinator/infra.json, where `splates server deploy` finds the stack. Then `bun add @stringsync/splates-server @pulumi/pulumi @pulumi/aws @pulumi/awsx @pulumi/cloudflare @pulumi/github @pulumi/tls @pulumi/random` there and `pulumi up`").arg("dir", z9.string(), { description: "where the Pulumi project goes" }).option("stack", z9.string(), { default: "prod", description: "the stack" }).option("host", z9.string(), {
706
- description: "the public host name, as in splates.example.com"
712
+ splatesInfra.command("init").use(repository7({ needs: "own" })).description("write a Pulumi project in <dir>, its program a dozen lines using the components of @stringsync/splates-server/infra, and the stack's config from the flags; run again, it writes only what is not so already and keeps values you changed. It fills the stack's config from what splates keeps (the projects, the saved tunnel, the Cloudflare ids, and each secret from the creds store), requesting on the Creds page any cred not kept and exiting `requested`; run again once saved. When the Pulumi ESC environment `coordinator-access` makes (splates/aws) is there, the stack imports it. It records the directory in ~/.splates/coordinator/infra.json, where `splates server deploy` finds the stack. Then `bun add @stringsync/splates-server @pulumi/pulumi @pulumi/aws @pulumi/awsx @pulumi/cloudflare @pulumi/github @pulumi/tls @pulumi/random` there and `pulumi up`").arg("dir", z9.string(), { description: "where the Pulumi project goes" }).option("stack", z9.string(), { default: "prod", description: "the stack" }).option("host", z9.string(), {
713
+ default: "",
714
+ description: "the public host name, as in splates.example.com; the saved tunnel's by default"
707
715
  }).option("email", z9.string(), {
708
- description: "the email address Access lets in"
716
+ default: "",
717
+ description: "the email address Access lets in; the tunnel's Access application's by default"
709
718
  }).option("region", z9.string(), {
710
719
  default: "us-east-1",
711
720
  description: "the AWS region"
712
721
  }).option("projects", z9.string(), {
713
722
  default: "",
714
- description: "comma-separated slug=owner/repo, each a project the box pushes trunk of"
723
+ description: "comma-separated slug=owner/repo, each a project the box pushes trunk of; every project registered here with a GitHub origin by default"
715
724
  }).action(async (opts, ctx) => {
716
- const projects = commaList4(opts.projects).map((entry) => {
725
+ const listed = commaList4(opts.projects).map((entry) => {
717
726
  const [slug, repository] = entry.split("=");
718
727
  if (!slug || !repository?.includes("/")) {
719
728
  fail5("usage", `--projects wants slug=owner/repo, not ${entry}`, {
@@ -722,16 +731,28 @@ splatesInfra.command("init").description("write a Pulumi project in <dir>, its p
722
731
  }
723
732
  return { slug, repository };
724
733
  });
725
- const { written, todo } = await infraInit({
734
+ const org = await pulumiOrg(ctx.ps);
735
+ const home = new CoordinatorBox({ ps: ctx.ps, fs: ctx.fs }).home;
736
+ const { written, todo } = await infraConfigure({
726
737
  dir: opts.dir,
727
738
  stack: opts.stack,
728
739
  host: opts.host,
729
740
  email: opts.email,
730
741
  region: opts.region,
731
- projects,
732
- fs: ctx.fs
742
+ projects: listed.length === 0 ? null : listed,
743
+ registered: async () => (await Projects.in(new FsStorage2(home, ctx.fs), ctx.fs).list()).filter((project) => !project.gone),
744
+ creds: ctx.creds,
745
+ saved: savedTunnelOf(ctx),
746
+ ...org !== null && await accessEnvironmentExists(ctx.ps, org) ? { environment: ACCESS_ENVIRONMENT } : {},
747
+ fs: ctx.fs,
748
+ ps: ctx.ps,
749
+ log: ctx.log,
750
+ fetch: ctx.fetch
751
+ });
752
+ await writeInfraConfig(ctx.fs, home, {
753
+ dir: resolve2(opts.dir),
754
+ stack: opts.stack
733
755
  });
734
- await writeInfraConfig(ctx.fs, new CoordinatorBox({ ps: ctx.ps, fs: ctx.fs }).home, { dir: resolve2(opts.dir), stack: opts.stack });
735
756
  ctx.log.info(written.length === 0 ? "already so: nothing written" : `wrote ${written.join(", ")}`);
736
757
  for (const command of todo) {
737
758
  ctx.log.info(`still to run in ${opts.dir}: ${command}`);
@@ -886,7 +907,7 @@ import { dirname } from "node:path";
886
907
  import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
887
908
  import { fail as fail7 } from "@stringsync/splates-server/exit";
888
909
  import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
889
- import { Projects } from "@stringsync/splates-server/projects";
910
+ import { Projects as Projects2 } from "@stringsync/splates-server/projects";
890
911
  import {
891
912
  SETUP_AREAS,
892
913
  Setup,
@@ -947,7 +968,7 @@ async function setup(deps, opts) {
947
968
  }
948
969
  async function setupOf(deps) {
949
970
  const { fs, ps, secrets = SYSTEM_STORES3, fetch } = deps;
950
- const root = deps.project === undefined ? await repoRoot(ps) : (await Projects.ofMachine(deps).named(deps.project)).root;
971
+ const root = deps.project === undefined ? await repoRoot(ps) : (await Projects2.ofMachine(deps).named(deps.project)).root;
951
972
  return new Setup({
952
973
  fs,
953
974
  ps,
@@ -1093,7 +1114,7 @@ import {
1093
1114
  import { commaList as commaList5 } from "@stringsync/splates-server/comma-list";
1094
1115
  import { refuseGroomer as refuseGroomer3 } from "@stringsync/splates-server/conversations";
1095
1116
  import { fail as fail8 } from "@stringsync/splates-server/exit";
1096
- import { repository as repository7 } from "@stringsync/splates-server/records";
1117
+ import { repository as repository8 } from "@stringsync/splates-server/records";
1097
1118
  import {
1098
1119
  DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
1099
1120
  taskWait,
@@ -1110,7 +1131,7 @@ async function projectFrom(ctx) {
1110
1131
  return (await ctx.projects.list()).find((each) => each.root === ctx.git.root)?.slug;
1111
1132
  }
1112
1133
  function adding(name, description, ready) {
1113
- splatesTask.command(name).use(repository7({ needs: "queue" })).description(description).arg("subject", z12.string(), {
1134
+ splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z12.string(), {
1114
1135
  description: "what is left to do, in a line"
1115
1136
  }).arg("text", z12.string(), {
1116
1137
  default: "",
@@ -1164,7 +1185,7 @@ function adding(name, description, ready) {
1164
1185
  }
1165
1186
  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);
1166
1187
  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);
1167
- splatesTask.command("list").use(repository7({ 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(), {
1188
+ 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(), {
1168
1189
  default: false,
1169
1190
  description: "only tasks no started one has taken up: the ones free to pick up"
1170
1191
  }).option("ready", z12.boolean(), {
@@ -1176,12 +1197,12 @@ splatesTask.command("list").use(repository7({ needs: "read" })).description("eve
1176
1197
  ready: opts.ready,
1177
1198
  project: await projectFrom(ctx)
1178
1199
  }));
1179
- splatesTask.command("show").use(repository7({ 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, {
1200
+ 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, {
1180
1201
  json: opts.json,
1181
1202
  project: await projectFrom(ctx),
1182
1203
  others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug)
1183
1204
  }));
1184
- splatesTask.command("update").use(repository7({ 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(), {
1205
+ 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(), {
1185
1206
  default: "",
1186
1207
  description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
1187
1208
  }).option("subject", z12.string(), {
@@ -1246,11 +1267,11 @@ splatesTask.command("update").use(repository7({ needs: "tasks" })).description("
1246
1267
  manual: opts.manual ? true : opts.auto ? false : undefined
1247
1268
  }));
1248
1269
  });
1249
- splatesTask.command("remove").use(repository7({ 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) => {
1270
+ 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) => {
1250
1271
  await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
1251
1272
  await ctx.journal.record("task remove", null, () => taskRemove(ctx, opts.id), (removed) => ({ tasks: tasksNamed2([removed]) }));
1252
1273
  });
1253
- splatesTask.command("wait").use(repository7({ 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(), {
1274
+ 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(), {
1254
1275
  default: DEFAULT_TIMEOUT2.secs,
1255
1276
  description: "how long to wait before giving up"
1256
1277
  }).action(async (opts, ctx) => {
@@ -1264,8 +1285,8 @@ splatesTask.command("wait").use(repository7({ needs: "read" })).description("blo
1264
1285
  });
1265
1286
 
1266
1287
  // tunnel.ts
1267
- import { repository as repository8 } from "@stringsync/splates-server/records";
1268
- import { savedTunnelOf } from "@stringsync/splates-server/server";
1288
+ import { repository as repository9 } from "@stringsync/splates-server/records";
1289
+ import { savedTunnelOf as savedTunnelOf2 } from "@stringsync/splates-server/server";
1269
1290
  import {
1270
1291
  API_TOKEN,
1271
1292
  tunnelRemove,
@@ -1274,7 +1295,7 @@ import {
1274
1295
  import { cli as cli12 } from "webappwiz/cmd";
1275
1296
  import { z as z13 } from "zod";
1276
1297
  var splatesTunnel = cli12("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
1277
- splatesTunnel.command("setup").use(repository8({ 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(), {
1298
+ 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(), {
1278
1299
  description: "the public host name, in a zone of the account, as in splates.example.com"
1279
1300
  }).option("email", z13.string(), {
1280
1301
  default: "",
@@ -1289,12 +1310,12 @@ splatesTunnel.command("setup").use(repository8({ needs: "own" })).description(`m
1289
1310
  ...opts,
1290
1311
  creds: ctx.creds,
1291
1312
  task: await here(ctx),
1292
- saved: savedTunnelOf(ctx),
1313
+ saved: savedTunnelOf2(ctx),
1293
1314
  log: ctx.log,
1294
1315
  fetch: ctx.fetch
1295
1316
  })));
1296
1317
  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) => {
1297
- const saved = savedTunnelOf(ctx);
1318
+ const saved = savedTunnelOf2(ctx);
1298
1319
  const record = await saved.read();
1299
1320
  const token = record === null ? false : await saved.token() !== undefined;
1300
1321
  if (opts.json) {
@@ -1314,8 +1335,8 @@ splatesTunnel.command("status").description("print the tunnel `splates tunnel se
1314
1335
  ].join(`
1315
1336
  `));
1316
1337
  });
1317
- splatesTunnel.command("remove").use(repository8({ 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({
1318
- saved: savedTunnelOf(ctx),
1338
+ 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({
1339
+ saved: savedTunnelOf2(ctx),
1319
1340
  slots: slotTunnelsOf(ctx),
1320
1341
  creds: ctx.creds,
1321
1342
  task: await here(ctx),
@@ -1546,15 +1567,15 @@ function projectArgs(argv) {
1546
1567
  return { argv };
1547
1568
  }
1548
1569
  var splates = cli15("splates").use(exits());
1549
- splates.command("notes").use(repository9({ needs: "read" })).description("print each project note's frontmatter description, one per line; notesDir in splates.config.ts defaults to notes; an absent directory prints nothing").action((_opts, ctx) => notes(ctx));
1550
- splates.command("init").use(repository9({ 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));
1570
+ splates.command("notes").use(repository10({ needs: "read" })).description("print each project note's frontmatter description, one per line; notesDir in splates.config.ts defaults to notes; an absent directory prints nothing").action((_opts, ctx) => notes(ctx));
1571
+ 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));
1551
1572
  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) => {
1552
1573
  const format = await migrateRecords(ctx);
1553
1574
  if (ctx.ps.env(MIGRATING) === undefined) {
1554
1575
  ctx.log.info(`splates's records are in format ${format}`);
1555
1576
  }
1556
1577
  });
1557
- splates.command("update").use(recordsFirst()).use(repository9({ 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) => {
1578
+ 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) => {
1558
1579
  const code = await update({
1559
1580
  root: ctx.git.root,
1560
1581
  fs: ctx.fs,
@@ -1574,8 +1595,8 @@ splates.command("heavy").description("run a heavy command (a full test run, an i
1574
1595
  ctx.ps.exit(code);
1575
1596
  }
1576
1597
  });
1577
- splates.command("forget").use(repository9({ 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));
1578
- splates.command("add").use(repository9({ 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(), {
1598
+ 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));
1599
+ 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(), {
1579
1600
  description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
1580
1601
  }).option("base", z16.string(), {
1581
1602
  default: "",
@@ -1643,11 +1664,11 @@ splates.command("add").use(repository9({ needs: "tasks" })).description("start a
1643
1664
  await serveProject(ctx);
1644
1665
  }
1645
1666
  });
1646
- splates.command("claim").use(repository9({ 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) => {
1667
+ 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) => {
1647
1668
  await ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task), (claimed) => claimed);
1648
1669
  await follow(ctx, opts.task);
1649
1670
  });
1650
- splates.command("merge").use(repository9({ 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(), {
1671
+ 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(), {
1651
1672
  default: "",
1652
1673
  description: "squash the task's commits into one with this message before landing, saying what the base gains in at least 5 characters"
1653
1674
  }).action(async (opts, ctx) => {
@@ -1660,7 +1681,7 @@ splates.command("merge").use(repository9({ needs: "own" })).description("land th
1660
1681
  await follow(ctx, null, { from: task });
1661
1682
  }
1662
1683
  });
1663
- splates.command("remove").use(repository9({ needs: "tasks" })).description("discard a task: worktree, branch and state file; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z16.string(), { description: "task name" }).option("force", z16.boolean(), {
1684
+ splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and state file; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z16.string(), { description: "task name" }).option("force", z16.boolean(), {
1664
1685
  default: false,
1665
1686
  description: "discard even when another agent holds the lease"
1666
1687
  }).option("reason", z16.optional(z16.enum(REMOVE_REASONS)), {
@@ -1669,19 +1690,19 @@ splates.command("remove").use(repository9({ needs: "tasks" })).description("disc
1669
1690
  await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force, reason: opts.reason }), ({ released }) => ({ why: opts.reason ?? null, released }));
1670
1691
  await follow(ctx, null, { from: opts.task });
1671
1692
  });
1672
- splates.command("list").use(repository9({ 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(), {
1693
+ 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(), {
1673
1694
  default: false,
1674
1695
  description: "add each task's Goal and changed files (committed or not), to check for overlap before starting"
1675
1696
  }).action((opts, ctx) => list(ctx, { json: opts.json, files: opts.files }));
1676
- splates.command("show").use(repository9({ 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 }));
1677
- splates.command("wait").use(repository9({ 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(), {
1697
+ 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 }));
1698
+ 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(), {
1678
1699
  default: DEFAULT_TIMEOUT3.secs,
1679
1700
  description: "how long to wait before giving up"
1680
1701
  }).action((opts, ctx) => {
1681
1702
  const timeout = Duration3.secs(opts["timeout-secs"]);
1682
1703
  return ctx.journal.waiting("wait", opts.task, { until: untilAfter3(timeout) }, () => wait(ctx, opts.task, { timeout }));
1683
1704
  });
1684
- splates.command("log").use(repository9({ 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(), {
1705
+ 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(), {
1685
1706
  default: DEFAULT_COUNT,
1686
1707
  description: "how many entries to show"
1687
1708
  }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
@@ -1719,14 +1740,14 @@ splates.command("setup").description("set splates up on this machine, one step a
1719
1740
  default: false,
1720
1741
  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"
1721
1742
  }).action((opts, ctx) => setup(ctx, opts));
1722
- splates.command("path").use(repository9({ 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(), {
1743
+ 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(), {
1723
1744
  default: "",
1724
1745
  description: "task name; omit for the main tree"
1725
1746
  }).action((opts, ctx) => path(ctx, opts.task || undefined));
1726
- splates.command("checkpoint").use(repository9({ 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) => {
1747
+ 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) => {
1727
1748
  await checkpoint(ctx, ctx.ps.cwd());
1728
1749
  });
1729
- splates.command("attach").use(repository9({ 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(), {
1750
+ 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(), {
1730
1751
  default: "",
1731
1752
  description: "files to store, comma separated"
1732
1753
  }).option("task", z16.string(), {
@@ -1743,7 +1764,7 @@ splates.command("attach").use(repository9({ needs: "own" })).description("store
1743
1764
  remove: commaList6(opts.remove),
1744
1765
  conversation: opts.conversation || undefined
1745
1766
  }), (stored) => ({ stored, removed: commaList6(opts.remove) })));
1746
- splates.command("retry").use(repository9({ 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));
1767
+ 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));
1747
1768
  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("budget", splatesBudget);
1748
1769
 
1749
1770
  // index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stringsync/splates",
3
- "version": "0.0.51",
3
+ "version": "0.0.52",
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,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@stringsync/splates-server": "^0.0.51",
20
+ "@stringsync/splates-server": "^0.0.52",
21
21
  "webappwiz": "0.0.47",
22
22
  "zod": "^4.4.3"
23
23
  },