@stringsync/splates 0.0.47 → 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.
- package/README.md +55 -10
- package/flags.d.ts +4 -4
- package/hook.d.ts +3 -0
- package/index.js +949 -508
- package/infra.d.ts +3 -0
- package/lane.d.ts +1 -1
- package/notes.d.ts +4 -0
- package/package.json +3 -3
- package/queue.d.ts +1 -1
- package/server.d.ts +7 -0
- package/setup.d.ts +2 -0
- package/task.d.ts +3 -0
- package/waiting.d.ts +3 -0
- package/coordinator.d.ts +0 -3
- package/main.d.ts +0 -6
- package/todo.d.ts +0 -3
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
import { basename as basename2 } from "
|
|
4
|
+
// index.ts
|
|
5
|
+
import { basename as basename2 } from "path";
|
|
6
6
|
import {
|
|
7
7
|
LEGACY,
|
|
8
8
|
legacyWarnings,
|
|
@@ -12,49 +12,49 @@ import { assets } from "@stringsync/splates-server/server";
|
|
|
12
12
|
import { NodeFs } from "webappwiz/system";
|
|
13
13
|
|
|
14
14
|
// splates.ts
|
|
15
|
-
import { DEFAULT_PORT as DEFAULT_PORT2 } from "@stringsync/splates-server";
|
|
16
15
|
import { agents } from "@stringsync/splates-server/agents";
|
|
17
16
|
import { attach } from "@stringsync/splates-server/artifacts";
|
|
18
|
-
import { commaList as
|
|
19
|
-
import {
|
|
17
|
+
import { commaList as commaList6 } from "@stringsync/splates-server/comma-list";
|
|
18
|
+
import { refuseGroomer as refuseGroomer4 } from "@stringsync/splates-server/conversations";
|
|
19
|
+
import { exits, fail as fail9 } from "@stringsync/splates-server/exit";
|
|
20
20
|
import { DEFAULT_COUNT, log as showLog } from "@stringsync/splates-server/log";
|
|
21
21
|
import { merge } from "@stringsync/splates-server/merge";
|
|
22
22
|
import { portsLs } from "@stringsync/splates-server/ports";
|
|
23
23
|
import { forget, init } from "@stringsync/splates-server/projects";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
MIGRATING,
|
|
26
|
+
MIGRATION_LIMIT,
|
|
27
|
+
migrateRecords,
|
|
28
|
+
repository as repository10
|
|
29
|
+
} from "@stringsync/splates-server/records";
|
|
25
30
|
import {
|
|
26
31
|
newest,
|
|
32
|
+
recordsFirst,
|
|
27
33
|
rosterOf as rosterOf2,
|
|
28
34
|
runHeavy,
|
|
29
35
|
seeded as seeded2,
|
|
30
|
-
serveDev,
|
|
31
36
|
update
|
|
32
37
|
} from "@stringsync/splates-server/server";
|
|
33
|
-
import {
|
|
38
|
+
import { addFactTasks } from "@stringsync/splates-server/stats";
|
|
34
39
|
import {
|
|
35
40
|
add,
|
|
36
41
|
checkpoint,
|
|
37
42
|
claim,
|
|
38
43
|
DEFAULT_TIMEOUT as DEFAULT_TIMEOUT3,
|
|
39
|
-
escalate,
|
|
40
44
|
follow,
|
|
41
|
-
handoff,
|
|
42
45
|
list,
|
|
43
46
|
path,
|
|
44
47
|
REMOVE_REASONS,
|
|
45
48
|
recordRetry,
|
|
46
49
|
remove,
|
|
47
|
-
reportToCaller,
|
|
48
50
|
serveProject,
|
|
49
51
|
show,
|
|
50
|
-
tell,
|
|
51
52
|
untilAfter as untilAfter3,
|
|
52
53
|
wait
|
|
53
54
|
} from "@stringsync/splates-server/tasks";
|
|
54
|
-
import {
|
|
55
|
-
import { cli as cli12 } from "webappwiz/cmd";
|
|
55
|
+
import { cli as cli15 } from "webappwiz/cmd";
|
|
56
56
|
import { Duration as Duration3 } from "webappwiz/time";
|
|
57
|
-
import { z as
|
|
57
|
+
import { z as z16 } from "zod";
|
|
58
58
|
|
|
59
59
|
// artifact.ts
|
|
60
60
|
import { basename, resolve } from "node:path";
|
|
@@ -71,6 +71,7 @@ import { cli } from "webappwiz/cmd";
|
|
|
71
71
|
import { z as z2 } from "zod";
|
|
72
72
|
|
|
73
73
|
// flags.ts
|
|
74
|
+
import { BACKLOG_LANE } from "@stringsync/splates-server/board";
|
|
74
75
|
import { commaList } from "@stringsync/splates-server/comma-list";
|
|
75
76
|
import { SYSTEM_STORES } from "@stringsync/splates-server/creds";
|
|
76
77
|
import { fail } from "@stringsync/splates-server/exit";
|
|
@@ -82,6 +83,7 @@ import {
|
|
|
82
83
|
} from "@stringsync/splates-server/models";
|
|
83
84
|
import { SLOT_POOL } from "@stringsync/splates-server/ports";
|
|
84
85
|
import { rosterOf, seeded } from "@stringsync/splates-server/server";
|
|
86
|
+
import { FsStorage } from "@stringsync/splates-server/storage";
|
|
85
87
|
import { SavedTunnel } from "@stringsync/splates-server/tunnel";
|
|
86
88
|
import { z } from "zod";
|
|
87
89
|
var position = z.coerce.number().int().positive().optional();
|
|
@@ -112,17 +114,14 @@ async function effortIn(deps, agent, effort, data = {}) {
|
|
|
112
114
|
return { effort: pickEffort(offered, agent, effort, data) };
|
|
113
115
|
}
|
|
114
116
|
function offeredOf({ ps, fs }) {
|
|
115
|
-
return new Offered(
|
|
117
|
+
return new Offered(new FsStorage(splatesHome(ps), fs).document("offered.json"));
|
|
116
118
|
}
|
|
117
119
|
function slotTunnelsOf({
|
|
118
120
|
ps,
|
|
119
121
|
fs,
|
|
120
122
|
secrets = SYSTEM_STORES
|
|
121
123
|
}) {
|
|
122
|
-
return SLOT_POOL.map(({ word }) => new
|
|
123
|
-
fs,
|
|
124
|
-
store: secrets.device
|
|
125
|
-
}));
|
|
124
|
+
return SLOT_POOL.map(({ word }) => SavedTunnel.in(new FsStorage(`${splatesHome(ps)}/slots/${word}`, fs), secrets.device));
|
|
126
125
|
}
|
|
127
126
|
function laneIn(raw) {
|
|
128
127
|
const value = raw.trim();
|
|
@@ -135,19 +134,22 @@ function laneIn(raw) {
|
|
|
135
134
|
if (value === "none") {
|
|
136
135
|
return null;
|
|
137
136
|
}
|
|
137
|
+
if (value === "backlog") {
|
|
138
|
+
return BACKLOG_LANE;
|
|
139
|
+
}
|
|
138
140
|
const id = Number(value);
|
|
139
|
-
if (!Number.isInteger(id) || id
|
|
141
|
+
if (!Number.isInteger(id) || id < 0) {
|
|
140
142
|
fail("usage", `'${value}' is not a lane: pass its number, new or none`, {
|
|
141
143
|
lane: value
|
|
142
144
|
});
|
|
143
145
|
}
|
|
144
146
|
return id;
|
|
145
147
|
}
|
|
146
|
-
function
|
|
148
|
+
function taskIds(raw) {
|
|
147
149
|
return commaList(raw).map((value) => {
|
|
148
150
|
const id = Number(value);
|
|
149
151
|
if (!Number.isInteger(id) || id <= 0) {
|
|
150
|
-
fail("usage", `'${value}' is not a
|
|
152
|
+
fail("usage", `'${value}' is not a task number`, { number: value });
|
|
151
153
|
}
|
|
152
154
|
return id;
|
|
153
155
|
});
|
|
@@ -162,7 +164,7 @@ async function here({
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
// artifact.ts
|
|
165
|
-
var splatesArtifact = cli("artifact").description("files your conversation made as its outputs, such as a screenshot, a recording or an HTML page: kept by name for the life of the conversation, whatever task it was made in, and listed on the `splates server` page beside its thread. A file that shows a question
|
|
167
|
+
var splatesArtifact = cli("artifact").description("files your conversation made as its outputs, such as a screenshot, a recording or an HTML page: kept by name for the life of the conversation, whatever task it was made in, and listed on the `splates server` page beside its thread. A file that shows a question or a demo step is no artifact: `splates attach` it. The conversation is $SPLATES_CONVERSATION, or --conversation <id> from an agent outside `splates server`");
|
|
166
168
|
var conversation = [
|
|
167
169
|
"conversation",
|
|
168
170
|
z2.string(),
|
|
@@ -232,6 +234,7 @@ splatesBudget.command("show").description("show each limit: what made workers sp
|
|
|
232
234
|
dev: opts.dev,
|
|
233
235
|
json: opts.json,
|
|
234
236
|
log: ctx.log,
|
|
237
|
+
fs: ctx.fs,
|
|
235
238
|
ps: ctx.ps,
|
|
236
239
|
send: ctx.fetch
|
|
237
240
|
}));
|
|
@@ -243,6 +246,7 @@ splatesBudget.command("set").description("set a limit, in dollars (dollars an ho
|
|
|
243
246
|
dev: opts.dev,
|
|
244
247
|
json: opts.json,
|
|
245
248
|
log: ctx.log,
|
|
249
|
+
fs: ctx.fs,
|
|
246
250
|
ps: ctx.ps,
|
|
247
251
|
send: ctx.fetch
|
|
248
252
|
}));
|
|
@@ -256,6 +260,7 @@ splatesBudget.command("raise").description("add dollars to a limit for a while,
|
|
|
256
260
|
dev: opts.dev,
|
|
257
261
|
json: opts.json,
|
|
258
262
|
log: ctx.log,
|
|
263
|
+
fs: ctx.fs,
|
|
259
264
|
ps: ctx.ps,
|
|
260
265
|
send: ctx.fetch
|
|
261
266
|
}));
|
|
@@ -264,143 +269,56 @@ splatesBudget.command("end").description("end a raise now, by the number `show`
|
|
|
264
269
|
dev: opts.dev,
|
|
265
270
|
json: opts.json,
|
|
266
271
|
log: ctx.log,
|
|
272
|
+
fs: ctx.fs,
|
|
267
273
|
ps: ctx.ps,
|
|
268
274
|
send: ctx.fetch
|
|
269
275
|
}));
|
|
270
276
|
|
|
271
|
-
// coordinator.ts
|
|
272
|
-
import {
|
|
273
|
-
CoordinatorBox,
|
|
274
|
-
keepCarriedCreds,
|
|
275
|
-
machineMove
|
|
276
|
-
} from "@stringsync/splates-server/coordinator";
|
|
277
|
-
import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
|
|
278
|
-
import { fail as fail3 } from "@stringsync/splates-server/exit";
|
|
279
|
-
import {
|
|
280
|
-
DEFAULT_PORT,
|
|
281
|
-
RUNNING
|
|
282
|
-
} from "@stringsync/splates-server/server";
|
|
283
|
-
import { cli as cli3 } from "webappwiz/cmd";
|
|
284
|
-
import { z as z4 } from "zod";
|
|
285
|
-
var splatesCoordinator = cli3("coordinator").description("the always-on box in your own Hetzner Cloud account that `splates server` moves onto, made through Pulumi with the token and state the Cloud workers setup readied: a CPX21 in Ashburn with Hetzner's daily backups, reached by SSH alone");
|
|
286
|
-
splatesCoordinator.command("up").description("make the coordinator's box, or bring it to its program, with splates of this release installed for the user splates and an SSH key splates makes under ~/.splates/coordinator; run again, it changes only what is not so already. The box serves nothing until `splates coordinator move`").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
287
|
-
ctx.log.info("making the coordinator's box; its first boot takes minutes");
|
|
288
|
-
print(ctx, await box(ctx).up(), opts.json);
|
|
289
|
-
});
|
|
290
|
-
splatesCoordinator.command("status").description("print the coordinator's box as its Pulumi stack has it").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
291
|
-
const standing = await box(ctx).status();
|
|
292
|
-
if (standing === null) {
|
|
293
|
-
ctx.log.info(opts.json ? "null" : "no coordinator's box: `splates coordinator up` makes one");
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
print(ctx, standing, opts.json);
|
|
297
|
-
});
|
|
298
|
-
splatesCoordinator.command("remove").description("destroy the coordinator's box and its backups, lifting the protection that keeps it from deletion first, and forget its stack; its SSH key stays. Anything only on the box is lost").action(async (_opts, ctx) => {
|
|
299
|
-
await box(ctx).remove();
|
|
300
|
-
ctx.log.info("removed the coordinator's box");
|
|
301
|
-
});
|
|
302
|
-
splatesCoordinator.command("move").description("move the coordinator onto its box, over SSH. First it checks what the move needs, brings the box's splates to this release, and gives each project a clone there with a deploy key of its own; until GitHub lets each key push trunk, it prints each key with where to add it, with write access, and exits `blocked`: add them and run it again. Then it asks every agent of this machine's `splates server` to checkpoint and stops it, carries the coordinator's records and creds to the box, starts `splates server` there for good under systemd, with the saved tunnel, and joins this machine to it as a worker, kept up by a LaunchAgent, where each task carries on in its worktree here. This machine's records go aside, kept, and its `splates` reaches the box from then on").option("dry-run", z4.boolean(), {
|
|
303
|
-
default: false,
|
|
304
|
-
description: "stop once the box is ready, before this machine's server stops or anything here changes"
|
|
305
|
-
}).option("dev", z4.string(), {
|
|
306
|
-
default: `http://localhost:${DEFAULT_PORT}`,
|
|
307
|
-
description: "this machine's `splates server`, which the move stops"
|
|
308
|
-
}).action(async (opts, ctx) => {
|
|
309
|
-
const moved = await machineMove({
|
|
310
|
-
ps: ctx.ps,
|
|
311
|
-
fs: ctx.fs,
|
|
312
|
-
log: ctx.log,
|
|
313
|
-
version: RUNNING,
|
|
314
|
-
dev: opts.dev,
|
|
315
|
-
stores: ctx.secrets ?? SYSTEM_STORES2,
|
|
316
|
-
...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
|
|
317
|
-
}).run({ dryRun: opts["dry-run"] });
|
|
318
|
-
if (moved.status === "keys") {
|
|
319
|
-
fail3("blocked", [
|
|
320
|
-
'GitHub does not let these deploy keys push yet. Add each to its repository with "Allow write access" ticked, then move again:',
|
|
321
|
-
...moved.keys.flatMap(({ slug, key, add }) => [
|
|
322
|
-
"",
|
|
323
|
-
` ${slug}${add === null ? "" : `: ${add}`}`,
|
|
324
|
-
` ${key}`
|
|
325
|
-
])
|
|
326
|
-
].join(`
|
|
327
|
-
`), { keys: moved.keys });
|
|
328
|
-
}
|
|
329
|
-
if (moved.status === "ready") {
|
|
330
|
-
ctx.log.info(`the box is ready to coordinate ${moved.projects.join(", ")} through ${moved.host}: move again without --dry-run`);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
ctx.log.info([
|
|
334
|
-
`the coordinator runs on the box: https://${moved.host}`,
|
|
335
|
-
`this machine is its worker ${ctx.ps.hostname}, kept up by ${moved.worker}`,
|
|
336
|
-
`its own records are kept aside in ${moved.aside} and each project's .git/splates.moved`
|
|
337
|
-
].join(`
|
|
338
|
-
`));
|
|
339
|
-
});
|
|
340
|
-
splatesCoordinator.command("keep-creds").description("keep the creds `splates coordinator move` sends on stdin in this machine's stores, as the box does when the coordinator moves onto it").action(async (_opts, ctx) => {
|
|
341
|
-
const kept = await keepCarriedCreds({
|
|
342
|
-
stores: ctx.secrets ?? SYSTEM_STORES2,
|
|
343
|
-
sent: await Bun.stdin.text()
|
|
344
|
-
});
|
|
345
|
-
ctx.log.info(`kept ${kept} creds`);
|
|
346
|
-
});
|
|
347
|
-
function box(ctx) {
|
|
348
|
-
return new CoordinatorBox({ version: RUNNING, ps: ctx.ps, fs: ctx.fs });
|
|
349
|
-
}
|
|
350
|
-
function print(ctx, standing, json) {
|
|
351
|
-
if (json) {
|
|
352
|
-
ctx.log.info(JSON.stringify(standing));
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
ctx.log.info([
|
|
356
|
-
`address: ${standing.ipv4}`,
|
|
357
|
-
`server: ${standing.serverId} (${standing.serverType} in ${standing.location})`,
|
|
358
|
-
`ssh: ssh -i ${standing.key} root@${standing.ipv4}`
|
|
359
|
-
].join(`
|
|
360
|
-
`));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
277
|
// creds.ts
|
|
364
|
-
import { DEFAULT_TIMEOUT } from "@stringsync/splates-server/creds";
|
|
365
|
-
import { fail as
|
|
278
|
+
import { DEFAULT_TIMEOUT, PROVIDERS } from "@stringsync/splates-server/creds";
|
|
279
|
+
import { fail as fail3 } from "@stringsync/splates-server/exit";
|
|
366
280
|
import { repository as repository2 } from "@stringsync/splates-server/records";
|
|
367
281
|
import { untilAfter } from "@stringsync/splates-server/tasks";
|
|
368
|
-
import { cli as
|
|
282
|
+
import { cli as cli3 } from "webappwiz/cmd";
|
|
369
283
|
import { color } from "webappwiz/log";
|
|
370
284
|
import { Duration } from "webappwiz/time";
|
|
371
|
-
import { z as
|
|
372
|
-
var splatesCreds =
|
|
373
|
-
splatesCreds.command("request").use(repository2({ needs: "own" })).description("ask a person for a cred, saying why it is needed and what it will be used for, with markdown instructions for getting it; returns at once, and does nothing when the store already keeps it unless --replace; run from a worktree, it records the task asking").arg("name",
|
|
285
|
+
import { z as z4 } from "zod";
|
|
286
|
+
var splatesCreds = cli3("creds").description("creds an agent needs a person for, such as an API token: request one saying what it is for, with instructions for getting it, which the Creds page of `splates server` shows for the person to paste, then wait for it; the value goes into the system's store (the macOS Keychain, Windows Credential Manager, a Linux secret service), never to the agent, and code reads it through webappwiz/creds");
|
|
287
|
+
splatesCreds.command("request").use(repository2({ needs: "own" })).description("ask a person for a cred, saying why it is needed and what it will be used for, with markdown instructions for getting it; returns at once, and does nothing when the store already keeps it unless --replace; run from a worktree, it records the task asking").arg("name", z4.string(), {
|
|
374
288
|
description: "what it is kept as, an environment variable's name"
|
|
375
|
-
}).option("purpose",
|
|
289
|
+
}).option("purpose", z4.string(), {
|
|
376
290
|
default: "",
|
|
377
291
|
description: "why it is needed and what it will be used for, in a sentence the person reads before getting it"
|
|
378
|
-
}).option("instructions",
|
|
292
|
+
}).option("instructions", z4.string(), {
|
|
379
293
|
default: "",
|
|
380
294
|
description: "how to get it, in markdown: where to go, what to pick, what to paste"
|
|
381
|
-
}).option("device",
|
|
295
|
+
}).option("device", z4.boolean(), {
|
|
382
296
|
default: false,
|
|
383
297
|
description: "keep it for every project on this device, as for a person's own account, rather than this project alone"
|
|
384
|
-
}).option("
|
|
298
|
+
}).option("provider", z4.optional(z4.enum(PROVIDERS)), {
|
|
299
|
+
description: "who keeps it: `system`, the machine's own store, or `ssm`, AWS Parameter Store, where that is offered"
|
|
300
|
+
}).option("replace", z4.boolean(), {
|
|
385
301
|
default: false,
|
|
386
302
|
description: "ask again although one is kept, since the kept one does not work; --purpose and --instructions left off are those of the last request"
|
|
387
303
|
}).action(async (opts, ctx) => {
|
|
388
304
|
const scope = opts.device ? "device" : "project";
|
|
305
|
+
const provider = opts.provider ?? "system";
|
|
389
306
|
const request = await ctx.journal.record("creds request", null, async () => ctx.creds.request(opts.name, {
|
|
390
307
|
scope,
|
|
308
|
+
provider,
|
|
391
309
|
purpose: opts.purpose,
|
|
392
310
|
instructions: opts.instructions,
|
|
393
311
|
task: await here(ctx),
|
|
394
312
|
replace: opts.replace
|
|
395
313
|
}), (asked) => ({ name: opts.name, asked: asked !== null }));
|
|
396
314
|
if (request === null) {
|
|
397
|
-
ctx.log.info(`${color.green("kept")} ${opts.name} in ${ctx.creds.label(scope)}: nothing to ask`);
|
|
315
|
+
ctx.log.info(`${color.green("kept")} ${opts.name} in ${ctx.creds.label(scope, provider)}: nothing to ask`);
|
|
398
316
|
return;
|
|
399
317
|
}
|
|
400
318
|
ctx.log.info(`${color.yellow("requested")} ${opts.name} ${color.dim(request.purpose)}`);
|
|
401
319
|
ctx.log.error(`It shows on the Creds page of \`splates server\`. \`splates creds wait ${opts.name}\` until it is saved.`);
|
|
402
320
|
});
|
|
403
|
-
splatesCreds.command("wait").use(repository2({ needs: "own" })).description("block until a person saves the cred requested, or dismisses the request (`dismissed`); gives up with `timeout` rather than waiting forever").arg("name",
|
|
321
|
+
splatesCreds.command("wait").use(repository2({ needs: "own" })).description("block until a person saves the cred requested, or dismisses the request (`dismissed`); gives up with `timeout` rather than waiting forever").arg("name", z4.string(), { description: "the cred requested" }).option("timeout-secs", z4.coerce.number(), {
|
|
404
322
|
default: DEFAULT_TIMEOUT.secs,
|
|
405
323
|
description: "how long to wait before giving up"
|
|
406
324
|
}).action(async (opts, ctx) => {
|
|
@@ -408,12 +326,12 @@ splatesCreds.command("wait").use(repository2({ needs: "own" })).description("blo
|
|
|
408
326
|
await ctx.journal.waiting("creds wait", null, { name: opts.name, until: untilAfter(timeout) }, async () => {
|
|
409
327
|
const ended = await ctx.creds.wait(opts.name, { timeout });
|
|
410
328
|
if (ended === "dismissed") {
|
|
411
|
-
|
|
329
|
+
fail3("dismissed", `${opts.name} was dismissed: carry on without it, or ask the person why`, { name: opts.name });
|
|
412
330
|
}
|
|
413
331
|
});
|
|
414
332
|
ctx.log.info(`${color.green("saved")} ${opts.name}`);
|
|
415
333
|
});
|
|
416
|
-
splatesCreds.command("list").use(repository2({ needs: "read" })).description("the creds requested and still open, with what each is for, and every cred the project's store and the device's keep, by name; never a value").option("json",
|
|
334
|
+
splatesCreds.command("list").use(repository2({ needs: "read" })).description("the creds requested and still open, with what each is for, and every cred the project's store and the device's keep, by name; never a value").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
417
335
|
const open = (await ctx.creds.requests()).filter((each) => each.status === "open");
|
|
418
336
|
const kept = await ctx.creds.kept();
|
|
419
337
|
if (opts.json) {
|
|
@@ -427,15 +345,202 @@ splatesCreds.command("list").use(repository2({ needs: "read" })).description("th
|
|
|
427
345
|
}
|
|
428
346
|
}
|
|
429
347
|
for (const each of kept) {
|
|
430
|
-
ctx.log.info(`${color.green("kept")} ${each.name} ${color.dim(each.scope)}${each.savedBy === null ? "" : color.dim(` saved by ${each.savedBy}`)}`);
|
|
348
|
+
ctx.log.info(`${color.green("kept")} ${each.name} ${color.dim(`${each.provider} ${each.scope}`)}${each.savedBy === null ? "" : color.dim(` saved by ${each.savedBy}`)}`);
|
|
431
349
|
}
|
|
432
350
|
if (open.length === 0 && kept.length === 0) {
|
|
433
351
|
ctx.log.info(color.dim("no creds requested or kept"));
|
|
434
352
|
}
|
|
435
353
|
});
|
|
436
354
|
|
|
355
|
+
// hook.ts
|
|
356
|
+
import { commaList as commaList3 } from "@stringsync/splates-server/comma-list";
|
|
357
|
+
import { fail as fail4 } from "@stringsync/splates-server/exit";
|
|
358
|
+
import {
|
|
359
|
+
HookStore,
|
|
360
|
+
hookAdd,
|
|
361
|
+
hookEdit,
|
|
362
|
+
hookGardener,
|
|
363
|
+
hookGroomer,
|
|
364
|
+
hookList,
|
|
365
|
+
hookPause,
|
|
366
|
+
hookRemove,
|
|
367
|
+
lanesNamed,
|
|
368
|
+
stagesNamed,
|
|
369
|
+
TASK_EVENTS
|
|
370
|
+
} from "@stringsync/splates-server/hooks";
|
|
371
|
+
import { repository as repository3 } from "@stringsync/splates-server/records";
|
|
372
|
+
import { cli as cli4 } from "webappwiz/cmd";
|
|
373
|
+
import { z as z5 } from "zod";
|
|
374
|
+
var splatesHook = cli4("hook").description(`automations: what runs on a task event (${TASK_EVENTS.join(", ")}), as \`splates server\` sees the journal take it, or on a schedule it keeps: an agent told of it; or a shell command. Kept for you in this project's .git/splates, never checked in, since an agent hook spends your tokens; splates.config.ts's \`on\` holds the shell commands a project checks in. None set, nothing runs. The page's Automations tab lists them with when each last ran and for what task`);
|
|
375
|
+
splatesHook.command("list").use(repository3({ needs: "read" })).description("every automation set here, by number, with its trigger").option("json", z5.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => hookList({ hooks: storeOf(ctx), log: ctx.log }, { json: opts.json }));
|
|
376
|
+
splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run something on a task event, or on a schedule with `--every` or `--cron` in place of the event: `--agent <a> --prompt <words>` starts that agent in the main tree, told the event as `[SPLATES EVENT <event> #N]`, or a schedule as `[SPLATES SCHEDULE <number>]`, then your words; `--run <command>` runs it via `sh -c` in the main tree, with $SPLATES_EVENT (`schedule` for one), $SPLATES_TASKS and $SPLATES_TASK set. A schedule that came due while `splates server` was down runs once as it starts").arg("event", z5.string().optional(), {
|
|
377
|
+
description: "the task event, as task.created; none for a schedule"
|
|
378
|
+
}).option("stage", z5.string(), {
|
|
379
|
+
default: "",
|
|
380
|
+
description: "only for tasks in these stages, comma separated: backlog, ready, queued, running"
|
|
381
|
+
}).option("lane", z5.string(), {
|
|
382
|
+
default: "",
|
|
383
|
+
description: "only for tasks in these lanes, by number, comma separated"
|
|
384
|
+
}).option("every", z5.string(), {
|
|
385
|
+
default: "",
|
|
386
|
+
description: "run every so often, as 30m, 2h or 1d"
|
|
387
|
+
}).option("cron", z5.string(), {
|
|
388
|
+
default: "",
|
|
389
|
+
description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
|
|
390
|
+
}).option("agent", z5.string(), {
|
|
391
|
+
default: "",
|
|
392
|
+
description: "the agent to run, as `splates agents` names it"
|
|
393
|
+
}).option("model", z5.string(), {
|
|
394
|
+
default: "",
|
|
395
|
+
description: "its model (default: the agent's own)"
|
|
396
|
+
}).option("effort", z5.string(), {
|
|
397
|
+
default: "",
|
|
398
|
+
description: "its thinking level (default: the agent's own)"
|
|
399
|
+
}).option("prompt", z5.string(), {
|
|
400
|
+
default: "",
|
|
401
|
+
description: "what the agent is told after the event"
|
|
402
|
+
}).option("run", z5.string(), {
|
|
403
|
+
default: "",
|
|
404
|
+
description: "a shell command to run"
|
|
405
|
+
}).action(async (opts, ctx) => {
|
|
406
|
+
const pick = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
|
|
407
|
+
let hook;
|
|
408
|
+
if (opts.run !== "") {
|
|
409
|
+
if (pick != null || opts.prompt !== "") {
|
|
410
|
+
fail4("usage", "--run goes alone: a shell command runs no agent", {});
|
|
411
|
+
}
|
|
412
|
+
hook = { kind: "run", command: opts.run };
|
|
413
|
+
} else if (pick == null) {
|
|
414
|
+
fail4("usage", "say what runs: --agent with --prompt, or --run", {});
|
|
415
|
+
} else if (opts.prompt === "") {
|
|
416
|
+
fail4("usage", "an agent hook needs --prompt: what to tell it", {});
|
|
417
|
+
} else {
|
|
418
|
+
hook = { kind: "agent", prompt: opts.prompt, ...pick };
|
|
419
|
+
}
|
|
420
|
+
await hookAdd({ hooks: storeOf(ctx), log: ctx.log }, triggerOf(opts), hook);
|
|
421
|
+
});
|
|
422
|
+
splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("change one of your automations, by its number in `splates hook list`, keeping its number, its pause and its last runs. Each flag changes only what it names, as `splates hook add` takes it: `--event`, `--every` or `--cron` sets what sets it off anew, `--stage` or `--lane` alone narrows its event again (`any` for no narrowing), `--prompt` or `--agent` changes what an agent is told or which agent, and `--run` makes it a shell command. The groomer changes through `splates hook groomer`").arg("id", z5.string(), { description: "its number in `splates hook list`" }).option("event", z5.string(), {
|
|
423
|
+
default: "",
|
|
424
|
+
description: "a task event to run on, as task.created, in place of what sets it off now"
|
|
425
|
+
}).option("stage", z5.string(), {
|
|
426
|
+
default: "",
|
|
427
|
+
description: "only for tasks in these stages, comma separated, or `any`: backlog, ready, queued, running"
|
|
428
|
+
}).option("lane", z5.string(), {
|
|
429
|
+
default: "",
|
|
430
|
+
description: "only for tasks in these lanes, by number, comma separated, or `any`"
|
|
431
|
+
}).option("every", z5.string(), {
|
|
432
|
+
default: "",
|
|
433
|
+
description: "run every so often, as 30m, 2h or 1d"
|
|
434
|
+
}).option("cron", z5.string(), {
|
|
435
|
+
default: "",
|
|
436
|
+
description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
|
|
437
|
+
}).option("agent", z5.string(), {
|
|
438
|
+
default: "",
|
|
439
|
+
description: "the agent to run, as `splates agents` names it"
|
|
440
|
+
}).option("model", z5.string(), {
|
|
441
|
+
default: "",
|
|
442
|
+
description: "its model, with --agent (default: the agent's own)"
|
|
443
|
+
}).option("effort", z5.string(), {
|
|
444
|
+
default: "",
|
|
445
|
+
description: "its thinking level, with --agent (default: the agent's own)"
|
|
446
|
+
}).option("prompt", z5.string(), {
|
|
447
|
+
default: "",
|
|
448
|
+
description: "what the agent is told after the event"
|
|
449
|
+
}).option("run", z5.string(), {
|
|
450
|
+
default: "",
|
|
451
|
+
description: "a shell command to run in place of an agent"
|
|
452
|
+
}).action(async (opts, ctx) => {
|
|
453
|
+
const { id, agent, model, effort, ...change } = opts;
|
|
454
|
+
const pick = await assigneeIn(ctx, agent, model, effort);
|
|
455
|
+
if (pick === null) {
|
|
456
|
+
fail4("usage", "an agent automation needs an agent: name one, or --run", {});
|
|
457
|
+
}
|
|
458
|
+
await hookEdit({ hooks: storeOf(ctx), log: ctx.log }, id, {
|
|
459
|
+
...change,
|
|
460
|
+
...pick === undefined ? {} : { pick }
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
splatesHook.command("remove").use(repository3({ needs: "tasks" })).description("take away an automation, by its number in `splates hook list`, or every one on an event").arg("id", z5.string(), {
|
|
464
|
+
description: "its number, or a task event, as task.created"
|
|
465
|
+
}).action((opts, ctx) => hookRemove({ hooks: storeOf(ctx), log: ctx.log }, opts.id));
|
|
466
|
+
splatesHook.command("pause").use(repository3({ needs: "tasks" })).description("stop an automation running, keeping it to resume").arg("id", z5.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, true));
|
|
467
|
+
splatesHook.command("resume").use(repository3({ needs: "tasks" })).description("let a paused automation run again").arg("id", z5.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, false));
|
|
468
|
+
splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description("turn on the project's groomer, built-in automation `groomer`, or set the agent it runs as, as the Automations page does: on each task deferred to the backlog, and the backlog as `splates server` starts, a conversation of its own per run, archived as the run ends, organizes each task in order and marks it ready, never doing it. `--off` turns it off, retiring its conversations").option("agent", z5.string(), {
|
|
469
|
+
default: "",
|
|
470
|
+
description: "the agent it runs as, as `splates agents` names it"
|
|
471
|
+
}).option("model", z5.string(), {
|
|
472
|
+
default: "",
|
|
473
|
+
description: "its model (default: the agent's own)"
|
|
474
|
+
}).option("effort", z5.string(), {
|
|
475
|
+
default: "",
|
|
476
|
+
description: "its thinking level (default: the agent's own)"
|
|
477
|
+
}).option("off", z5.boolean(), {
|
|
478
|
+
default: false,
|
|
479
|
+
description: "turn it off"
|
|
480
|
+
}).action(async (opts, ctx) => {
|
|
481
|
+
const pick = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
|
|
482
|
+
if (opts.off === (pick != null)) {
|
|
483
|
+
fail4("usage", "say how it runs with --agent, or turn it off with --off", {});
|
|
484
|
+
}
|
|
485
|
+
await hookGroomer({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null);
|
|
486
|
+
});
|
|
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
|
+
default: "",
|
|
489
|
+
description: "the agent it runs as, as `splates agents` names it"
|
|
490
|
+
}).option("model", z5.string(), {
|
|
491
|
+
default: "",
|
|
492
|
+
description: "its model (default: the agent's own)"
|
|
493
|
+
}).option("effort", z5.string(), {
|
|
494
|
+
default: "",
|
|
495
|
+
description: "its thinking level (default: the agent's own)"
|
|
496
|
+
}).option("every", z5.string(), {
|
|
497
|
+
default: "",
|
|
498
|
+
description: "run every so often, as 1d (default: as it was, or 7d)"
|
|
499
|
+
}).option("cron", z5.string(), {
|
|
500
|
+
default: "",
|
|
501
|
+
description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1"`
|
|
502
|
+
}).option("off", z5.boolean(), {
|
|
503
|
+
default: false,
|
|
504
|
+
description: "turn it off"
|
|
505
|
+
}).action(async (opts, ctx) => {
|
|
506
|
+
const pick = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
|
|
507
|
+
if (opts.off === (pick != null)) {
|
|
508
|
+
fail4("usage", "say how it runs with --agent, or turn it off with --off", {});
|
|
509
|
+
}
|
|
510
|
+
await hookGardener({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null, {
|
|
511
|
+
every: opts.every,
|
|
512
|
+
cron: opts.cron
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
function triggerOf(opts) {
|
|
516
|
+
const given = [opts.event, opts.every, opts.cron].filter((each) => each !== undefined && each !== "");
|
|
517
|
+
if (given.length !== 1) {
|
|
518
|
+
fail4("usage", "say what sets it off: a task event, --every or --cron, one of them", {});
|
|
519
|
+
}
|
|
520
|
+
if (opts.event === undefined || opts.event === "") {
|
|
521
|
+
if (opts.stage !== "" || opts.lane !== "") {
|
|
522
|
+
fail4("usage", "--stage and --lane narrow a task event, not a schedule", {});
|
|
523
|
+
}
|
|
524
|
+
return opts.every !== "" ? { every: opts.every } : { cron: opts.cron };
|
|
525
|
+
}
|
|
526
|
+
const filter = {};
|
|
527
|
+
if (opts.stage !== "") {
|
|
528
|
+
filter.stages = stagesNamed(commaList3(opts.stage));
|
|
529
|
+
}
|
|
530
|
+
if (opts.lane !== "") {
|
|
531
|
+
filter.lanes = lanesNamed(opts.lane);
|
|
532
|
+
}
|
|
533
|
+
const event = opts.event;
|
|
534
|
+
return Object.keys(filter).length === 0 ? { event } : { event, filter };
|
|
535
|
+
}
|
|
536
|
+
function storeOf(ctx) {
|
|
537
|
+
return new HookStore(ctx.storage);
|
|
538
|
+
}
|
|
539
|
+
|
|
437
540
|
// lane.ts
|
|
438
541
|
import {
|
|
542
|
+
APPROVAL,
|
|
543
|
+
BACKLOG_LANE as BACKLOG_LANE2,
|
|
439
544
|
laneAdd,
|
|
440
545
|
laneList,
|
|
441
546
|
laneRemove,
|
|
@@ -444,22 +549,27 @@ import {
|
|
|
444
549
|
laneUpdated,
|
|
445
550
|
recordLaneRemove
|
|
446
551
|
} from "@stringsync/splates-server/board";
|
|
552
|
+
import { refuseGroomer } from "@stringsync/splates-server/conversations";
|
|
447
553
|
import { UNTRIAGED } from "@stringsync/splates-server/plans";
|
|
448
|
-
import {
|
|
554
|
+
import { refuseAgentQueue } from "@stringsync/splates-server/queue";
|
|
555
|
+
import { repository as repository4 } from "@stringsync/splates-server/records";
|
|
449
556
|
import { cli as cli5 } from "webappwiz/cmd";
|
|
450
557
|
import { z as z6 } from "zod";
|
|
451
|
-
var
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
splatesLane.command("
|
|
458
|
-
|
|
558
|
+
var laneNumber = z6.union([
|
|
559
|
+
z6.literal("backlog").transform(() => BACKLOG_LANE2),
|
|
560
|
+
z6.coerce.number().int().nonnegative()
|
|
561
|
+
]);
|
|
562
|
+
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");
|
|
563
|
+
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 }));
|
|
564
|
+
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], {
|
|
565
|
+
error: "pass a lane's number, backlog, or untriaged"
|
|
566
|
+
}), { description: "lane number, `backlog`, or `untriaged`" }).option("json", z6.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneShow(ctx, opts.lane, { json: opts.json }));
|
|
567
|
+
splatesLane.command("add").use(repository4({ needs: "tasks" })).description("start an empty lane, after the others").arg("name", z6.string(), {
|
|
568
|
+
description: "what its tasks are for, like `Licensing`"
|
|
459
569
|
}).action(async (opts, ctx) => {
|
|
460
570
|
await ctx.journal.record("lane add", null, () => laneAdd(ctx, opts.name), (lane) => ({ lane: lane.id, name: lane.name }));
|
|
461
571
|
});
|
|
462
|
-
splatesLane.command("update").use(
|
|
572
|
+
splatesLane.command("update").use(repository4({ needs: "tasks" })).description("rename a lane, or move it among the others").arg("lane", laneNumber, {
|
|
463
573
|
description: "lane number"
|
|
464
574
|
}).option("name", z6.string().optional(), {
|
|
465
575
|
description: "what it is called from now on"
|
|
@@ -469,9 +579,46 @@ splatesLane.command("update").use(repository3({ needs: "tasks" })).description("
|
|
|
469
579
|
const change = { name: opts.name, position: opts.position };
|
|
470
580
|
await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
|
|
471
581
|
});
|
|
472
|
-
splatesLane.command("
|
|
582
|
+
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, {
|
|
583
|
+
description: "lane number"
|
|
584
|
+
}).action(async (opts, ctx) => {
|
|
585
|
+
refuseAgentQueue(ctx.service, "lane queue");
|
|
586
|
+
await refuseGroomer(ctx.service, "lane queue");
|
|
587
|
+
const change = { queued: true };
|
|
588
|
+
await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
|
|
589
|
+
});
|
|
590
|
+
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, {
|
|
591
|
+
description: "lane number"
|
|
592
|
+
}).action(async (opts, ctx) => {
|
|
593
|
+
refuseAgentQueue(ctx.service, "lane approve");
|
|
594
|
+
const change = { approve: APPROVAL };
|
|
595
|
+
await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
|
|
596
|
+
});
|
|
597
|
+
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, {
|
|
598
|
+
description: "lane number"
|
|
599
|
+
}).action(async (opts, ctx) => {
|
|
600
|
+
refuseAgentQueue(ctx.service, "lane unqueue");
|
|
601
|
+
const change = { queued: false };
|
|
602
|
+
await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
|
|
603
|
+
});
|
|
604
|
+
splatesLane.command("remove").use(repository4({ needs: "tasks" })).description("remove a lane and its tasks, kept to restore; a lane whose last task merges goes on its own; refused from an agent, which asks with a permission card").arg("lane", laneNumber, {
|
|
473
605
|
description: "lane number"
|
|
474
|
-
}).action((opts, ctx) =>
|
|
606
|
+
}).action((opts, ctx) => {
|
|
607
|
+
refuseAgentQueue(ctx.service, "lane remove");
|
|
608
|
+
return recordLaneRemove(ctx.journal, ctx.tasks, opts.lane, () => laneRemove(ctx, opts.lane));
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// notes.ts
|
|
612
|
+
import { loadConfig } from "@stringsync/splates-server/config";
|
|
613
|
+
import { Notes } from "@stringsync/splates-server/notes";
|
|
614
|
+
async function notes(ctx) {
|
|
615
|
+
const root = ctx.project === undefined ? (await ctx.git.run(ctx.ps.cwd(), "rev-parse", "--show-toplevel")).stdout.trim() : ctx.git.root;
|
|
616
|
+
const config = await loadConfig(root, { fs: ctx.fs, git: ctx.git });
|
|
617
|
+
const found = await new Notes(ctx.fs).list(root, config);
|
|
618
|
+
for (const note of found) {
|
|
619
|
+
ctx.log.info(note.description);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
475
622
|
|
|
476
623
|
// projects.ts
|
|
477
624
|
import {
|
|
@@ -479,50 +626,288 @@ import {
|
|
|
479
626
|
listReaches,
|
|
480
627
|
REACHES
|
|
481
628
|
} from "@stringsync/splates-server/projects";
|
|
482
|
-
import { repository as
|
|
629
|
+
import { repository as repository5 } from "@stringsync/splates-server/records";
|
|
483
630
|
import { cli as cli6 } from "webappwiz/cmd";
|
|
484
631
|
import { z as z7 } from "zod";
|
|
485
|
-
var splatesProjects = cli6("projects").description("the other splates projects on this machine, and what agents of each may do in the others through splates: `read` their tasks
|
|
486
|
-
splatesProjects.command("list").use(
|
|
487
|
-
splatesProjects.command("grant").use(
|
|
488
|
-
description: "read,
|
|
632
|
+
var splatesProjects = cli6("projects").description("the other splates projects on this machine, and what agents of each may do in the others through splates: `read` their tasks and lanes; add tasks to their untriaged lane with `queue`, as every project may until granted otherwise; or run their queue, lanes and hooks with `tasks`. Any repo command runs on another project as `splates --project <slug> <command>`, refused where it needs more than that project granted").fallback("list");
|
|
633
|
+
splatesProjects.command("list").use(repository5({ needs: "read" })).description("print each other project: its slug, its root, what it may do here, and what this one may do there").option("json", z7.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listReaches(ctx, { json: opts.json }));
|
|
634
|
+
splatesProjects.command("grant").use(repository5({ needs: "own" })).description("let agents of another project do as much as `read`, `queue` or `tasks` here, each including those before it; the user's call, so an agent runs it only when the user asks it to").arg("project", z7.string(), { description: "the other project's slug" }).arg("reach", z7.enum(REACHES), {
|
|
635
|
+
description: "read, queue or tasks"
|
|
489
636
|
}).action((opts, ctx) => grantReach(ctx, opts.project, opts.reach));
|
|
490
637
|
|
|
491
638
|
// queue.ts
|
|
492
|
-
import {
|
|
639
|
+
import { tasksNamed } from "@stringsync/splates-server/board";
|
|
640
|
+
import { refuseGroomer as refuseGroomer2 } from "@stringsync/splates-server/conversations";
|
|
641
|
+
import {
|
|
642
|
+
queueAdd,
|
|
643
|
+
queueList,
|
|
644
|
+
queueMove,
|
|
645
|
+
queueRemove,
|
|
646
|
+
refuseAgentQueue as refuseAgentQueue2
|
|
647
|
+
} from "@stringsync/splates-server/queue";
|
|
648
|
+
import { repository as repository6 } from "@stringsync/splates-server/records";
|
|
493
649
|
import { cli as cli7 } from "webappwiz/cmd";
|
|
494
650
|
import { z as z8 } from "zod";
|
|
495
|
-
var
|
|
651
|
+
var splatesQueue = cli7("queue").description("the queue: the tasks a worker of `splates server` starts as soon as it has a slot free, each after every task it is blocked by, otherwise in the order you set. Only you queue a task, from the backlog or groomed, and an agent asks you to with a queue card; one that names no model runs on the roster's first agent");
|
|
652
|
+
splatesQueue.command("list").use(repository6({ needs: "read" })).description("the queue in the order workers start it, with each task's model and what it waits on: the open tasks it is blocked by, or those removed without merging").option("json", z8.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => queueList(ctx, { json: opts.json }));
|
|
653
|
+
splatesQueue.command("add").use(repository6({ needs: "tasks" })).description("queue tasks for a worker to start once nothing they are blocked by is open; refused for one already under way").rest("ids", z8.coerce.number().int().positive(), {
|
|
654
|
+
description: "task numbers"
|
|
655
|
+
}).action(async (opts, ctx) => {
|
|
656
|
+
refuseAgentQueue2(ctx.service, "queue add");
|
|
657
|
+
await refuseGroomer2(ctx.service, "queue add");
|
|
658
|
+
await ctx.journal.record("queue add", await here(ctx), () => queueAdd(ctx, opts.ids), (queued) => ({ tasks: tasksNamed(queued) }));
|
|
659
|
+
});
|
|
660
|
+
splatesQueue.command("remove").use(repository6({ needs: "tasks" })).description("take tasks out of the queue, back to ready when they name a model, else the backlog; the tasks stay").rest("ids", z8.coerce.number().int().positive(), {
|
|
661
|
+
description: "task numbers"
|
|
662
|
+
}).action(async (opts, ctx) => {
|
|
663
|
+
refuseAgentQueue2(ctx.service, "queue remove");
|
|
664
|
+
await ctx.journal.record("queue remove", await here(ctx), () => queueRemove(ctx, opts.ids), (removed) => ({ tasks: tasksNamed(removed) }));
|
|
665
|
+
});
|
|
666
|
+
splatesQueue.command("move").use(repository6({ needs: "tasks" })).description("move a queued task to another place in the queue, from 1 for the next to start; what it is blocked by still goes first. Then lists the queue").arg("id", z8.coerce.number().int().positive(), { description: "task number" }).arg("position", z8.coerce.number().int().positive(), {
|
|
667
|
+
description: "where in the queue to put it, from 1"
|
|
668
|
+
}).action(async (opts, ctx) => {
|
|
669
|
+
refuseAgentQueue2(ctx.service, "queue move");
|
|
670
|
+
return queueMove(ctx, opts.id, opts.position);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// server.ts
|
|
674
|
+
import {
|
|
675
|
+
BoxShell,
|
|
676
|
+
CoordinatorBox as CoordinatorBox2,
|
|
677
|
+
coordinatorMoved,
|
|
678
|
+
deployKey,
|
|
679
|
+
machineMove,
|
|
680
|
+
neededCreds
|
|
681
|
+
} from "@stringsync/splates-server/coordinator";
|
|
682
|
+
import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
|
|
683
|
+
import { fail as fail6 } from "@stringsync/splates-server/exit";
|
|
684
|
+
import {
|
|
685
|
+
DEFAULT_PORT,
|
|
686
|
+
RUNNING,
|
|
687
|
+
serveDev
|
|
688
|
+
} from "@stringsync/splates-server/server";
|
|
689
|
+
import { LOCAL_SLOTS } from "@stringsync/splates-server/workers";
|
|
690
|
+
import { cli as cli9 } from "webappwiz/cmd";
|
|
691
|
+
import { z as z10 } from "zod";
|
|
692
|
+
|
|
693
|
+
// infra.ts
|
|
694
|
+
import { resolve as resolve2 } from "node:path";
|
|
695
|
+
import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
|
|
696
|
+
import {
|
|
697
|
+
ACCESS_ENVIRONMENT,
|
|
698
|
+
accessEnvironmentExists,
|
|
699
|
+
CoordinatorBox,
|
|
700
|
+
pulumiOrg,
|
|
701
|
+
writeInfraConfig
|
|
702
|
+
} from "@stringsync/splates-server/coordinator";
|
|
703
|
+
import { fail as fail5 } from "@stringsync/splates-server/exit";
|
|
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";
|
|
709
|
+
import { cli as cli8 } from "webappwiz/cmd";
|
|
710
|
+
import { z as z9 } from "zod";
|
|
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");
|
|
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(), {
|
|
496
713
|
default: "",
|
|
497
|
-
description: "the
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
714
|
+
description: "the public host name, as in splates.example.com; the saved tunnel's by default"
|
|
715
|
+
}).option("email", z9.string(), {
|
|
716
|
+
default: "",
|
|
717
|
+
description: "the email address Access lets in; the tunnel's Access application's by default"
|
|
718
|
+
}).option("region", z9.string(), {
|
|
719
|
+
default: "us-east-1",
|
|
720
|
+
description: "the AWS region"
|
|
721
|
+
}).option("projects", z9.string(), {
|
|
722
|
+
default: "",
|
|
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"
|
|
724
|
+
}).action(async (opts, ctx) => {
|
|
725
|
+
const listed = commaList4(opts.projects).map((entry) => {
|
|
726
|
+
const [slug, repository] = entry.split("=");
|
|
727
|
+
if (!slug || !repository?.includes("/")) {
|
|
728
|
+
fail5("usage", `--projects wants slug=owner/repo, not ${entry}`, {
|
|
729
|
+
entry
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
return { slug, repository };
|
|
733
|
+
});
|
|
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({
|
|
737
|
+
dir: opts.dir,
|
|
738
|
+
stack: opts.stack,
|
|
739
|
+
host: opts.host,
|
|
740
|
+
email: opts.email,
|
|
741
|
+
region: opts.region,
|
|
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
|
|
755
|
+
});
|
|
756
|
+
ctx.log.info(written.length === 0 ? "already so: nothing written" : `wrote ${written.join(", ")}`);
|
|
757
|
+
for (const command of todo) {
|
|
758
|
+
ctx.log.info(`still to run in ${opts.dir}: ${command}`);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// server.ts
|
|
763
|
+
var splatesServer = cli9("server").description("the splates server: run it on this machine, or deploy it to an always-on box in your own AWS account, which the Pulumi project `splates server infra init` writes makes, reached by SSH alone").fallback("run");
|
|
764
|
+
splatesServer.command("run").description("serve every splates project on this machine as a web page, from wherever it is run: the page picks the project it shows, adds one by its path, running `splates init` there, and shows its queued and started tasks, where queued tasks can be added, updated, reordered or removed; it brings each project's records forward as it opens it. A server that crashes starts again after 2s, then 4s, 8s and 16s, and is left down after five crashes in a row that each came within a minute of its start").option("port", z10.coerce.number(), {
|
|
765
|
+
default: DEFAULT_PORT,
|
|
766
|
+
description: "port to listen on, or the next open one above it"
|
|
767
|
+
}).option("allow-hosts", z10.string(), {
|
|
768
|
+
default: "",
|
|
769
|
+
description: "comma-separated host names the page may also be reached by, like a tunnel's; put Cloudflare Access in front of them with --access-team and --access-aud"
|
|
770
|
+
}).option("access-team", z10.string(), {
|
|
771
|
+
default: "",
|
|
772
|
+
description: "the Cloudflare Access team in front of --allow-hosts, as in <team>.cloudflareaccess.com; with --access-aud, those hosts answer only to a verified Access token, and may then use conversations and terminals"
|
|
773
|
+
}).option("access-aud", z10.string(), {
|
|
774
|
+
default: "",
|
|
775
|
+
description: "the AUD tag of that Access application"
|
|
776
|
+
}).option("tunnel", z10.string(), {
|
|
777
|
+
default: "",
|
|
778
|
+
description: "the public host name of a tunnel to run, with cloudflared or the tunnel provider named by splates.config.ts in the repository it is run in, if any, which is allowed as a host too; refused without --access-team and --access-aud, so a tunnel never exposes the page unauthenticated. With none of the tunnel and Access flags, the tunnel `splates tunnel setup` saved runs, if any"
|
|
779
|
+
}).option("tunnel-name", z10.string(), {
|
|
780
|
+
default: "",
|
|
781
|
+
description: "the named tunnel cloudflared runs for --tunnel, by default the one its own config names; not with a tunnel provider of splates.config.ts"
|
|
782
|
+
}).option("ports-host", z10.string(), {
|
|
783
|
+
default: "",
|
|
784
|
+
description: "the wildcard base host to serve task ports under, saved for this machine so a bare `splates server` serves it too: each port a task's processes listen on opens at https://<task>-<port>.<base>, to whoever Cloudflare Access lets in, and is refused without Access; routing *.<base> here, its DNS and TLS, are yours. `none` forgets it, leaving each task port forwarded to a port on this machine's loopback alone"
|
|
785
|
+
}).option("worker-slots", z10.coerce.number().int().min(1), {
|
|
786
|
+
default: LOCAL_SLOTS,
|
|
787
|
+
description: "how many agents that other agents started (with `splates add --agent`, or a lane's orchestrator handing out tasks) this machine's worker runs at once; past them, each new or resumed one waits in the queue, first come, first served, which `splates worker queue` shows and reorders. An agent you start runs at once on any worker up, taking no slot. Read as the server starts its worker, which a restart from the page leaves running"
|
|
788
|
+
}).option("keepalive", z10.boolean(), {
|
|
789
|
+
default: false,
|
|
790
|
+
description: "keep a machine left to serve awake while the server runs: on macOS with `caffeinate`, on power or idle, though closing a laptop's lid still sleeps it; on Linux with a systemd-inhibit lock on sleep and idle, which the server serves on without, saying so, where systemd is missing or refuses it. Refused on other platforms"
|
|
791
|
+
}).option("slot", z10.boolean(), {
|
|
792
|
+
default: false,
|
|
793
|
+
description: "serve this checkout's splates at https://<word>.<zone>, a slot of its own on the zone `splates tunnel setup` uses, behind the same Access policy, so several run side by side and a phone opens each by name: a task's worktree keeps its word, as fox, until the task merges or is removed, which stops its server; run again, it stops the tree's last server first. Refused in a splates repository's main tree, which every agent shares. A slot keeps its own projects in ~/.splates/slots/<word>, apart from yours, starting with a throwaway project there; not with the tunnel and Access flags"
|
|
794
|
+
}).action((opts, ctx) => serveDev(ctx, opts));
|
|
795
|
+
splatesServer.mount("infra", splatesInfra);
|
|
796
|
+
splatesServer.command("deploy").description("deploy the server to the box its Pulumi stack made (`splates server infra init`, then `pulumi up`), over SSH, with splates of this release installed for the user splates. The SSH key and the host key come from the stack and the instance's console. It checks what the deploy needs, brings the box's splates to this release, and exits `blocked`, naming each, until the box's Parameter Store holds the creds the stack should have put there; then gives each project a clone there with its deploy key from Parameter Store. Then it asks every agent of this machine's `splates server` to checkpoint and stops it, carries the server's records (no creds: the box reads its own through its instance role) to the box, starts `splates server` there for good under systemd, and joins this machine to it as a worker, kept up by a LaunchAgent, where each task carries on in its worktree here. This machine's records go aside, kept, and its `splates` reaches the box from then on. Run again once the server runs on the box, it brings the box's splates to this release and restarts it there").option("dry-run", z10.boolean(), {
|
|
797
|
+
default: false,
|
|
798
|
+
description: "stop once the box is ready, before this machine's server stops or anything here changes; once the server runs on the box, change nothing"
|
|
799
|
+
}).option("dev", z10.string(), {
|
|
800
|
+
default: `http://localhost:${DEFAULT_PORT}`,
|
|
801
|
+
description: "this machine's `splates server`, which the deploy stops"
|
|
802
|
+
}).action(async (opts, ctx) => {
|
|
803
|
+
const moved = await machineMove({
|
|
804
|
+
ps: ctx.ps,
|
|
805
|
+
fs: ctx.fs,
|
|
806
|
+
log: ctx.log,
|
|
807
|
+
version: RUNNING,
|
|
808
|
+
dev: opts.dev,
|
|
809
|
+
stores: ctx.secrets ?? SYSTEM_STORES2,
|
|
810
|
+
...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
|
|
811
|
+
}).run({ dryRun: opts["dry-run"] });
|
|
812
|
+
if (moved.status === "blocked") {
|
|
813
|
+
fail6("blocked", [
|
|
814
|
+
"The box's Parameter Store lacks these. Set each (the Pulumi stack makes them: `pulumi up`), then deploy again:",
|
|
815
|
+
...moved.needs.map((name) => ` ${name}`)
|
|
816
|
+
].join(`
|
|
817
|
+
`), { needs: moved.needs });
|
|
818
|
+
}
|
|
819
|
+
if (moved.status === "deployed") {
|
|
820
|
+
ctx.log.info(`the server runs on the box at ${moved.ipv4}: deploy again without --dry-run to bring it to ${moved.version}`);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (moved.status === "updated") {
|
|
824
|
+
ctx.log.info(`the server on the box at ${moved.ipv4} runs ${moved.version} now`);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (moved.status === "ready") {
|
|
828
|
+
ctx.log.info(`the box is ready to serve ${moved.projects.join(", ")} through ${moved.host}: deploy again without --dry-run`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
ctx.log.info([
|
|
832
|
+
`the server runs on the box: https://${moved.host}`,
|
|
833
|
+
`this machine is its worker ${ctx.ps.hostname}, kept up by ${moved.worker}`,
|
|
834
|
+
`its own records are kept aside in ${moved.aside} and each project's .git/splates.moved`
|
|
835
|
+
].join(`
|
|
836
|
+
`));
|
|
837
|
+
});
|
|
838
|
+
splatesServer.command("status").description("print where the server runs, here or deployed to its box, and the box as its cloud has it").option("json", z10.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
839
|
+
const deployed = await coordinatorMoved({
|
|
840
|
+
fs: ctx.fs,
|
|
841
|
+
ps: ctx.ps,
|
|
842
|
+
home: box(ctx).home
|
|
843
|
+
});
|
|
844
|
+
const standing = await box(ctx).status();
|
|
845
|
+
if (opts.json) {
|
|
846
|
+
ctx.log.info(JSON.stringify({ deployed, box: standing }));
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
ctx.log.info([
|
|
850
|
+
`runs: ${deployed ? "on the box" : "on this machine"}`,
|
|
851
|
+
...standing === null ? [
|
|
852
|
+
"box: none: `splates server infra init` writes the Pulumi project, `pulumi up` there makes it"
|
|
853
|
+
] : [
|
|
854
|
+
`address: ${standing.ipv4}`,
|
|
855
|
+
`instance: ${standing.instanceId} (role ${standing.roleName})`,
|
|
856
|
+
`host: ${standing.host}`,
|
|
857
|
+
"ssh: splates server ssh"
|
|
858
|
+
]
|
|
859
|
+
].join(`
|
|
860
|
+
`));
|
|
861
|
+
});
|
|
862
|
+
splatesServer.command("ssh").description("open a root shell on the box over SSH, with the key its Pulumi stack made, or run the command given there").rest("command", z10.string(), {
|
|
863
|
+
description: "what to run on the box; by default a shell"
|
|
864
|
+
}).passThroughOptions().action(async (opts, ctx) => {
|
|
865
|
+
const standing = await box(ctx).status();
|
|
866
|
+
if (standing === null) {
|
|
867
|
+
fail6("not_found", "no box: `splates server infra init` writes the Pulumi project, `pulumi up` there makes it");
|
|
868
|
+
}
|
|
869
|
+
const { exitCode } = await ctx.ps.spawn(shell(ctx, standing).login(opts.command));
|
|
870
|
+
ctx.ps.exit(exitCode);
|
|
871
|
+
});
|
|
872
|
+
splatesServer.command("destroy").description("say how to destroy the box: its Pulumi stack made it, so the stack unmakes it").action(async (_opts, ctx) => {
|
|
873
|
+
const infra = await box(ctx).infra();
|
|
874
|
+
fail6("usage", infra === null ? "no infra project here: run `pulumi destroy` in the one `splates server infra init` wrote" : `run \`pulumi destroy\` in ${infra.dir}, after lifting the termination protection (\`disableApiTermination\`) on the instance. Anything only on the box is lost`);
|
|
875
|
+
});
|
|
876
|
+
splatesServer.command("needs").description("kept for `splates server deploy`, which runs it on the box: print the name of each cred this box's store lacks of those the server needs, one per line").rest("roots", z10.string(), {
|
|
877
|
+
description: "each project's clone on this machine"
|
|
878
|
+
}).action(async (opts, ctx) => {
|
|
879
|
+
const missing = await neededCreds(ctx.secrets ?? SYSTEM_STORES2, opts.roots);
|
|
880
|
+
for (const name of missing) {
|
|
881
|
+
ctx.log.info(name);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
splatesServer.command("deploy-key").description("kept for `splates server deploy`, which runs it on the box: print the deploy key of the project whose clone is <root>").arg("root", z10.string(), {
|
|
885
|
+
description: "the project's clone on this machine"
|
|
886
|
+
}).action(async (opts, ctx) => {
|
|
887
|
+
const key = await deployKey(ctx.secrets ?? SYSTEM_STORES2, opts.root);
|
|
888
|
+
if (key === undefined) {
|
|
889
|
+
fail6("not_found", `no deploy key kept for ${opts.root}`);
|
|
890
|
+
}
|
|
891
|
+
ctx.log.info(key);
|
|
892
|
+
});
|
|
893
|
+
function box(ctx) {
|
|
894
|
+
return new CoordinatorBox2({ ps: ctx.ps, fs: ctx.fs });
|
|
895
|
+
}
|
|
896
|
+
function shell(ctx, standing) {
|
|
897
|
+
return new BoxShell({
|
|
898
|
+
ps: ctx.ps,
|
|
899
|
+
ipv4: standing.ipv4,
|
|
900
|
+
key: standing.key,
|
|
901
|
+
knownHosts: `${box(ctx).home}/coordinator/known_hosts`
|
|
902
|
+
});
|
|
903
|
+
}
|
|
520
904
|
|
|
521
905
|
// setup.ts
|
|
522
906
|
import { dirname } from "node:path";
|
|
523
907
|
import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
|
|
908
|
+
import { fail as fail7 } from "@stringsync/splates-server/exit";
|
|
524
909
|
import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
|
|
525
|
-
import { Projects } from "@stringsync/splates-server/projects";
|
|
910
|
+
import { Projects as Projects2 } from "@stringsync/splates-server/projects";
|
|
526
911
|
import {
|
|
527
912
|
SETUP_AREAS,
|
|
528
913
|
Setup,
|
|
@@ -535,8 +920,28 @@ import {
|
|
|
535
920
|
} from "@stringsync/splates-server/workers";
|
|
536
921
|
import { color as color2 } from "webappwiz/log";
|
|
537
922
|
async function setup(deps, opts) {
|
|
923
|
+
if (opts.step === "" && (opts.skip || opts.unskip || opts.undo)) {
|
|
924
|
+
fail7("usage", "--skip and --unskip name a step, --undo an area", {});
|
|
925
|
+
}
|
|
926
|
+
if (opts.undo && (opts.skip || opts.unskip)) {
|
|
927
|
+
fail7("usage", "--undo names an area, not a step to skip", {
|
|
928
|
+
area: opts.step
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (opts.skip && opts.unskip) {
|
|
932
|
+
fail7("usage", "--skip or --unskip, not both", { step: opts.step });
|
|
933
|
+
}
|
|
538
934
|
const machine = await setupOf(deps);
|
|
539
935
|
const { log } = deps;
|
|
936
|
+
if (opts.undo) {
|
|
937
|
+
const plan = await machine.undoPlan(opts.step);
|
|
938
|
+
if (opts.json) {
|
|
939
|
+
log.info(JSON.stringify(plan, null, 2));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
printUndo(log, plan);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
540
945
|
if (opts.step !== "") {
|
|
541
946
|
machine.step(opts.step);
|
|
542
947
|
if (opts.skip) {
|
|
@@ -563,7 +968,7 @@ async function setup(deps, opts) {
|
|
|
563
968
|
}
|
|
564
969
|
async function setupOf(deps) {
|
|
565
970
|
const { fs, ps, secrets = SYSTEM_STORES3, fetch } = deps;
|
|
566
|
-
const root = deps.project === undefined ? await repoRoot(ps) : (await
|
|
971
|
+
const root = deps.project === undefined ? await repoRoot(ps) : (await Projects2.ofMachine(deps).named(deps.project)).root;
|
|
567
972
|
return new Setup({
|
|
568
973
|
fs,
|
|
569
974
|
ps,
|
|
@@ -633,13 +1038,37 @@ function printStep(log, step) {
|
|
|
633
1038
|
log.info(`${color2.red("problem:")} ${step.problem}`);
|
|
634
1039
|
}
|
|
635
1040
|
}
|
|
1041
|
+
function printUndo(log, plan) {
|
|
1042
|
+
log.info(`${color2.bold(`Undo ${plan.title}`)} ${color2.dim(plan.area)}`);
|
|
1043
|
+
if (plan.refused !== null) {
|
|
1044
|
+
log.info(`${color2.red("refused:")} ${plan.refused}`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
for (const step of plan.steps) {
|
|
1048
|
+
const started = step.done ? "" : ` ${color2.yellow("started")}`;
|
|
1049
|
+
log.info(` ${step.title} ${color2.dim(step.id)}${started}`);
|
|
1050
|
+
for (const line of step.removes) {
|
|
1051
|
+
log.info(` removes ${line}`);
|
|
1052
|
+
}
|
|
1053
|
+
for (const line of step.leaves) {
|
|
1054
|
+
log.info(` leaves ${line}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
for (const cred of plan.creds) {
|
|
1058
|
+
log.info(cred.keptFor === null ? ` forgets ${cred.name}` : ` keeps ${cred.name} ${color2.dim(`${cred.keptFor} still uses it`)}`);
|
|
1059
|
+
}
|
|
1060
|
+
if (plan.confirm !== null) {
|
|
1061
|
+
log.info(` the page asks for ${plan.confirm} to be typed`);
|
|
1062
|
+
}
|
|
1063
|
+
log.info(color2.dim("the Setup page's Undo does it, after the user confirms"));
|
|
1064
|
+
}
|
|
636
1065
|
|
|
637
1066
|
// slots.ts
|
|
638
1067
|
import { Slots } from "@stringsync/splates-server/ports";
|
|
639
|
-
import { cli as
|
|
640
|
-
import { z as
|
|
641
|
-
var splatesSlots =
|
|
642
|
-
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",
|
|
1068
|
+
import { cli as cli10 } from "webappwiz/cmd";
|
|
1069
|
+
import { z as z11 } from "zod";
|
|
1070
|
+
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");
|
|
1071
|
+
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) => {
|
|
643
1072
|
const held = await Slots.ofMachine(ctx).status();
|
|
644
1073
|
if (opts.json) {
|
|
645
1074
|
ctx.log.info(JSON.stringify(held.map(({ slot, tree, pid, orphaned }) => ({
|
|
@@ -671,190 +1100,222 @@ function heldLine({ slot, tree, pid, orphaned }) {
|
|
|
671
1100
|
return `${slot.word} ${slot.emoji} ${tree} ${server}${orphaned ? " orphaned: `splates slots prune` frees it" : ""}`;
|
|
672
1101
|
}
|
|
673
1102
|
|
|
674
|
-
//
|
|
1103
|
+
// task.ts
|
|
675
1104
|
import {
|
|
676
1105
|
addedFacts,
|
|
677
1106
|
recordUpdate,
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
todoTake,
|
|
685
|
-
todoUpdate
|
|
1107
|
+
taskAdd,
|
|
1108
|
+
taskList,
|
|
1109
|
+
taskRemove,
|
|
1110
|
+
taskShow,
|
|
1111
|
+
tasksNamed as tasksNamed2,
|
|
1112
|
+
taskUpdate
|
|
686
1113
|
} from "@stringsync/splates-server/board";
|
|
687
|
-
import { commaList as
|
|
688
|
-
import {
|
|
689
|
-
import {
|
|
1114
|
+
import { commaList as commaList5 } from "@stringsync/splates-server/comma-list";
|
|
1115
|
+
import { refuseGroomer as refuseGroomer3 } from "@stringsync/splates-server/conversations";
|
|
1116
|
+
import { fail as fail8 } from "@stringsync/splates-server/exit";
|
|
1117
|
+
import { repository as repository8 } from "@stringsync/splates-server/records";
|
|
690
1118
|
import {
|
|
691
1119
|
DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
|
|
692
|
-
|
|
1120
|
+
taskWait,
|
|
693
1121
|
untilAfter as untilAfter2
|
|
694
1122
|
} from "@stringsync/splates-server/tasks";
|
|
695
|
-
import { cli as
|
|
1123
|
+
import { cli as cli11 } from "webappwiz/cmd";
|
|
696
1124
|
import { Duration as Duration2 } from "webappwiz/time";
|
|
697
|
-
import { z as
|
|
698
|
-
var
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}).option("position", position, {
|
|
703
|
-
description: "where it goes in the list, 1 at the top, pushing those from there down (default: the bottom)"
|
|
704
|
-
}).option("file", z10.string(), {
|
|
705
|
-
default: "",
|
|
706
|
-
description: "files to attach, comma separated: copied under .git/splates/todos/<id>/"
|
|
707
|
-
}).option("blocked-by", z10.string(), {
|
|
708
|
-
default: "",
|
|
709
|
-
description: "ids of the todos blocking it, comma separated: each has to merge before this one starts"
|
|
710
|
-
}).option("lane", z10.string(), {
|
|
711
|
-
default: "",
|
|
712
|
-
description: "the lane it goes in, at the bottom: a lane's number, or `new` for one of its own (default: none, untriaged)"
|
|
713
|
-
}).option("agent", z10.string(), {
|
|
714
|
-
default: "",
|
|
715
|
-
description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first"
|
|
716
|
-
}).option("model", z10.string(), {
|
|
717
|
-
default: "",
|
|
718
|
-
description: "with --agent: the model, by its id or name on the roster"
|
|
719
|
-
}).option("effort", z10.string(), {
|
|
720
|
-
default: "",
|
|
721
|
-
description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
|
|
722
|
-
}).action(async (opts, ctx) => {
|
|
723
|
-
if (ctx.elsewhere !== null && opts.lane !== "") {
|
|
724
|
-
fail5("not_granted", "a todo from another project goes in its untriaged lane, for whoever orchestrates it there to place: leave --lane off", { subject: opts.subject });
|
|
1125
|
+
import { z as z12 } from "zod";
|
|
1126
|
+
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");
|
|
1127
|
+
async function projectFrom(ctx) {
|
|
1128
|
+
if (ctx.elsewhere === null) {
|
|
1129
|
+
return;
|
|
725
1130
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
})
|
|
742
|
-
|
|
1131
|
+
return (await ctx.projects.list()).find((each) => each.root === ctx.git.root)?.slug;
|
|
1132
|
+
}
|
|
1133
|
+
function adding(name, description, ready) {
|
|
1134
|
+
splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z12.string(), {
|
|
1135
|
+
description: "what is left to do, in a line"
|
|
1136
|
+
}).arg("text", z12.string(), {
|
|
1137
|
+
default: "",
|
|
1138
|
+
description: "whatever more there is to say about it, in markdown"
|
|
1139
|
+
}).option("slug", z12.string(), {
|
|
1140
|
+
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"
|
|
1141
|
+
}).option("position", position, {
|
|
1142
|
+
description: "where it goes in the list, 1 at the top, pushing those from there down (default: the bottom)"
|
|
1143
|
+
}).option("file", z12.string(), {
|
|
1144
|
+
default: "",
|
|
1145
|
+
description: "files to attach, comma separated: copied under .git/splates/tasks/<n>/"
|
|
1146
|
+
}).option("blocked-by", z12.string(), {
|
|
1147
|
+
default: "",
|
|
1148
|
+
description: "numbers of the tasks blocking it, comma separated: each has to merge before this one starts"
|
|
1149
|
+
}).option("lane", z12.string(), {
|
|
1150
|
+
default: "",
|
|
1151
|
+
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)"
|
|
1152
|
+
}).option("agent", z12.string(), {
|
|
1153
|
+
default: "",
|
|
1154
|
+
description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first"
|
|
1155
|
+
}).option("model", z12.string(), {
|
|
1156
|
+
default: "",
|
|
1157
|
+
description: "with --agent: the model, by its id or name on the roster"
|
|
1158
|
+
}).option("effort", z12.string(), {
|
|
1159
|
+
default: "",
|
|
1160
|
+
description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
|
|
1161
|
+
}).action(async (opts, ctx) => {
|
|
1162
|
+
if (ctx.elsewhere !== null && opts.lane !== "") {
|
|
1163
|
+
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 });
|
|
1164
|
+
}
|
|
1165
|
+
const from = await here(ctx);
|
|
1166
|
+
const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
|
|
1167
|
+
const origin = ctx.elsewhere === null ? undefined : {
|
|
1168
|
+
project: ctx.elsewhere,
|
|
1169
|
+
task: ctx.project === undefined ? null : ctx.service.taskFor(await ctx.git.currentBranch(ctx.ps.cwd()))
|
|
1170
|
+
};
|
|
1171
|
+
const project = await projectFrom(ctx);
|
|
1172
|
+
await ctx.journal.record("task add", from, () => taskAdd(ctx, opts.subject, from, {
|
|
1173
|
+
slug: opts.slug,
|
|
1174
|
+
text: opts.text,
|
|
1175
|
+
position: opts.position,
|
|
1176
|
+
files: commaList5(opts.file),
|
|
1177
|
+
blockedBy: taskIds(opts["blocked-by"]),
|
|
1178
|
+
lane: laneIn(opts.lane) ?? null,
|
|
1179
|
+
assignee,
|
|
1180
|
+
origin,
|
|
1181
|
+
ready,
|
|
1182
|
+
project
|
|
1183
|
+
}), addedFacts);
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
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);
|
|
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);
|
|
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(), {
|
|
743
1189
|
default: false,
|
|
744
|
-
description: "only
|
|
745
|
-
}).option("ready",
|
|
1190
|
+
description: "only tasks no started one has taken up: the ones free to pick up"
|
|
1191
|
+
}).option("ready", z12.boolean(), {
|
|
746
1192
|
default: false,
|
|
747
|
-
description: "only
|
|
748
|
-
}).action((opts, ctx) =>
|
|
1193
|
+
description: "only tasks free to start now: none has taken them, and nothing blocking them is still open"
|
|
1194
|
+
}).action(async (opts, ctx) => taskList(ctx, {
|
|
749
1195
|
json: opts.json,
|
|
750
1196
|
open: opts.open,
|
|
751
|
-
ready: opts.ready
|
|
1197
|
+
ready: opts.ready,
|
|
1198
|
+
project: await projectFrom(ctx)
|
|
1199
|
+
}));
|
|
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, {
|
|
1201
|
+
json: opts.json,
|
|
1202
|
+
project: await projectFrom(ctx),
|
|
1203
|
+
others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug)
|
|
752
1204
|
}));
|
|
753
|
-
|
|
754
|
-
splatesTodo.command("update").use(repository5({ needs: "tasks" })).description("say what a todo is in other words, move it up or down the list, or attach and drop files; it keeps its id").arg("id", z10.coerce.number().int().positive(), { description: "todo id" }).arg("text", z10.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(), {
|
|
755
1206
|
default: "",
|
|
756
1207
|
description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
|
|
757
|
-
}).option("subject",
|
|
1208
|
+
}).option("subject", z12.string(), {
|
|
758
1209
|
default: "",
|
|
759
1210
|
description: "what is left to do, in a line; leave out to keep it"
|
|
1211
|
+
}).option("slug", z12.string(), {
|
|
1212
|
+
default: "",
|
|
1213
|
+
description: "a new name for the task that takes it (lowercase-with-dashes); leave out to keep it"
|
|
760
1214
|
}).option("position", position, {
|
|
761
1215
|
description: "where to move it in the list, 1 at the top; the others close up around it"
|
|
762
|
-
}).option("file",
|
|
1216
|
+
}).option("file", z12.string(), {
|
|
763
1217
|
default: "",
|
|
764
1218
|
description: "files to attach, comma separated"
|
|
765
|
-
}).option("remove-file",
|
|
1219
|
+
}).option("remove-file", z12.string(), {
|
|
766
1220
|
default: "",
|
|
767
1221
|
description: "attached files to drop, comma separated, by path or stored name"
|
|
768
|
-
}).option("blocked-by",
|
|
1222
|
+
}).option("blocked-by", z12.string(), {
|
|
769
1223
|
default: "",
|
|
770
|
-
description: "
|
|
771
|
-
}).option("remove-blocked-by",
|
|
1224
|
+
description: "numbers of more tasks blocking it, comma separated: each has to merge before this one starts; refused when that makes a loop"
|
|
1225
|
+
}).option("remove-blocked-by", z12.string(), {
|
|
772
1226
|
default: "",
|
|
773
|
-
description: "
|
|
774
|
-
}).option("lane",
|
|
1227
|
+
description: "numbers of tasks no longer blocking it, comma separated"
|
|
1228
|
+
}).option("lane", z12.string(), {
|
|
775
1229
|
default: "",
|
|
776
|
-
description: "move it to a lane, at the bottom: a lane's number, `new` for one of its own, or `none`
|
|
777
|
-
}).option("agent",
|
|
1230
|
+
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"
|
|
1231
|
+
}).option("agent", z12.string(), {
|
|
778
1232
|
default: "",
|
|
779
1233
|
description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first, or `none`"
|
|
780
|
-
}).option("model",
|
|
1234
|
+
}).option("model", z12.string(), {
|
|
781
1235
|
default: "",
|
|
782
1236
|
description: "with --agent: the model, by its id or name on the roster"
|
|
783
|
-
}).option("effort",
|
|
1237
|
+
}).option("effort", z12.string(), {
|
|
784
1238
|
default: "",
|
|
785
1239
|
description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
|
|
1240
|
+
}).option("ready", z12.boolean(), {
|
|
1241
|
+
default: false,
|
|
1242
|
+
description: "mark it groomed, moving it from the backlog to ready: no task already asks it, and what blocks it is linked"
|
|
1243
|
+
}).option("manual", z12.boolean(), {
|
|
1244
|
+
default: false,
|
|
1245
|
+
description: "queue it by hand only: a queued lane passes over it, and it starts once you `splates queue add` it"
|
|
1246
|
+
}).option("auto", z12.boolean(), {
|
|
1247
|
+
default: false,
|
|
1248
|
+
description: "let a queued lane queue it again, undoing --manual"
|
|
786
1249
|
}).action(async (opts, ctx) => {
|
|
1250
|
+
if (opts.manual && opts.auto) {
|
|
1251
|
+
fail8("usage", "pass --manual or --auto, not both", { number: opts.id });
|
|
1252
|
+
}
|
|
1253
|
+
await refuseGroomer3(ctx.service, "task update", await ctx.tasks.find(opts.id));
|
|
787
1254
|
const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
|
|
788
|
-
await recordUpdate(ctx.journal, ctx.
|
|
1255
|
+
await recordUpdate(ctx.journal, ctx.tasks, opts.id, () => taskUpdate(ctx, opts.id, {
|
|
789
1256
|
subject: opts.subject,
|
|
1257
|
+
slug: opts.slug,
|
|
790
1258
|
text: opts.text,
|
|
791
1259
|
position: opts.position,
|
|
792
|
-
files:
|
|
793
|
-
removeFiles:
|
|
794
|
-
blockedBy:
|
|
795
|
-
removeBlockedBy:
|
|
1260
|
+
files: commaList5(opts.file),
|
|
1261
|
+
removeFiles: commaList5(opts["remove-file"]),
|
|
1262
|
+
blockedBy: taskIds(opts["blocked-by"]),
|
|
1263
|
+
removeBlockedBy: taskIds(opts["remove-blocked-by"]),
|
|
796
1264
|
lane: laneIn(opts.lane),
|
|
797
|
-
assignee
|
|
1265
|
+
assignee,
|
|
1266
|
+
ready: opts.ready,
|
|
1267
|
+
manual: opts.manual ? true : opts.auto ? false : undefined
|
|
798
1268
|
}));
|
|
799
1269
|
});
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const task = await here(ctx);
|
|
804
|
-
await ctx.journal.record("todo take", task, () => todoTake(ctx, opts.ids, task), (taken) => ({ todos: todosNamed(taken) }));
|
|
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) => {
|
|
1271
|
+
await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
|
|
1272
|
+
await ctx.journal.record("task remove", null, () => taskRemove(ctx, opts.id), (removed) => ({ tasks: tasksNamed2([removed]) }));
|
|
805
1273
|
});
|
|
806
|
-
|
|
807
|
-
description: "todo ids"
|
|
808
|
-
}).action(async (opts, ctx) => {
|
|
809
|
-
const task = await here(ctx);
|
|
810
|
-
await ctx.journal.record("todo release", task, () => todoRelease(ctx, opts.ids, task), (released) => ({ todos: todosNamed(released) }));
|
|
811
|
-
});
|
|
812
|
-
splatesTodo.command("remove").use(repository5({ needs: "tasks" })).description("drop a todo that was done some other way or no longer applies").arg("id", z10.coerce.number().int().positive(), { description: "todo id" }).action((opts, ctx) => ctx.journal.record("todo remove", null, () => todoRemove(ctx, opts.id), (removed) => ({ todos: todosNamed([removed]) })));
|
|
813
|
-
splatesTodo.command("wait").use(repository5({ needs: "read" })).description("block until a todo leaves the list, which it does when the task that took it merges: for a todo blocked by one another lane is on; ends early when that task is escalated, and gives up with `timeout`").arg("id", z10.coerce.number().int().positive(), { description: "todo id" }).option("timeout-secs", z10.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(), {
|
|
814
1275
|
default: DEFAULT_TIMEOUT2.secs,
|
|
815
1276
|
description: "how long to wait before giving up"
|
|
816
1277
|
}).action(async (opts, ctx) => {
|
|
817
1278
|
const timeout = Duration2.secs(opts["timeout-secs"]);
|
|
818
|
-
const
|
|
819
|
-
await ctx.journal.waiting("
|
|
820
|
-
|
|
821
|
-
subject:
|
|
1279
|
+
const task = (await ctx.tasks.all()).find((each) => each.id === opts.id);
|
|
1280
|
+
await ctx.journal.waiting("task wait", null, {
|
|
1281
|
+
number: opts.id,
|
|
1282
|
+
subject: task?.subject ?? null,
|
|
822
1283
|
until: untilAfter2(timeout)
|
|
823
|
-
}, () =>
|
|
1284
|
+
}, () => taskWait(ctx, opts.id, { timeout }));
|
|
824
1285
|
});
|
|
825
1286
|
|
|
826
1287
|
// tunnel.ts
|
|
827
|
-
import { repository as
|
|
828
|
-
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";
|
|
829
1290
|
import {
|
|
830
1291
|
API_TOKEN,
|
|
831
1292
|
tunnelRemove,
|
|
832
1293
|
tunnelSetup
|
|
833
1294
|
} from "@stringsync/splates-server/tunnel";
|
|
834
|
-
import { cli as
|
|
835
|
-
import { z as
|
|
836
|
-
var splatesTunnel =
|
|
837
|
-
splatesTunnel.command("setup").use(
|
|
1295
|
+
import { cli as cli12 } from "webappwiz/cmd";
|
|
1296
|
+
import { z as z13 } from "zod";
|
|
1297
|
+
var splatesTunnel = cli12("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
|
|
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(), {
|
|
838
1299
|
description: "the public host name, in a zone of the account, as in splates.example.com"
|
|
839
|
-
}).option("email",
|
|
1300
|
+
}).option("email", z13.string(), {
|
|
840
1301
|
default: "",
|
|
841
1302
|
description: "comma-separated email addresses Access lets in"
|
|
842
|
-
}).option("name",
|
|
1303
|
+
}).option("name", z13.string(), {
|
|
843
1304
|
default: "splates",
|
|
844
1305
|
description: "what the tunnel, the Access application and its policy are called"
|
|
845
|
-
}).option("team",
|
|
1306
|
+
}).option("team", z13.string(), {
|
|
846
1307
|
default: "",
|
|
847
1308
|
description: "the Zero Trust team to make, as in <team>.cloudflareaccess.com, when the account has none yet"
|
|
848
1309
|
}).action(async (opts, ctx) => ctx.journal.record("tunnel setup", null, async () => tunnelSetup({
|
|
849
1310
|
...opts,
|
|
850
1311
|
creds: ctx.creds,
|
|
851
1312
|
task: await here(ctx),
|
|
852
|
-
saved:
|
|
1313
|
+
saved: savedTunnelOf2(ctx),
|
|
853
1314
|
log: ctx.log,
|
|
854
1315
|
fetch: ctx.fetch
|
|
855
1316
|
})));
|
|
856
|
-
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",
|
|
857
|
-
const saved =
|
|
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) => {
|
|
1318
|
+
const saved = savedTunnelOf2(ctx);
|
|
858
1319
|
const record = await saved.read();
|
|
859
1320
|
const token = record === null ? false : await saved.token() !== undefined;
|
|
860
1321
|
if (opts.json) {
|
|
@@ -874,8 +1335,8 @@ splatesTunnel.command("status").description("print the tunnel `splates tunnel se
|
|
|
874
1335
|
].join(`
|
|
875
1336
|
`));
|
|
876
1337
|
});
|
|
877
|
-
splatesTunnel.command("remove").use(
|
|
878
|
-
saved:
|
|
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),
|
|
879
1340
|
slots: slotTunnelsOf(ctx),
|
|
880
1341
|
creds: ctx.creds,
|
|
881
1342
|
task: await here(ctx),
|
|
@@ -885,65 +1346,111 @@ splatesTunnel.command("remove").use(repository6({ needs: "own" })).description(`
|
|
|
885
1346
|
})));
|
|
886
1347
|
|
|
887
1348
|
// worker.ts
|
|
1349
|
+
import { splatesHome as splatesHome3 } from "@stringsync/splates-server/models";
|
|
888
1350
|
import {
|
|
889
1351
|
addWorker,
|
|
890
1352
|
drainWorker,
|
|
1353
|
+
FileLogger,
|
|
891
1354
|
listWorkers,
|
|
892
1355
|
removeWorker,
|
|
893
1356
|
setWorkerSlots,
|
|
1357
|
+
WORKER_JOIN,
|
|
894
1358
|
wakeWorker,
|
|
895
1359
|
workerJoin,
|
|
896
1360
|
workerRecipe,
|
|
897
1361
|
workerRun
|
|
898
1362
|
} from "@stringsync/splates-server/workers";
|
|
899
|
-
import { cli as
|
|
900
|
-
import {
|
|
901
|
-
|
|
902
|
-
|
|
1363
|
+
import { cli as cli14 } from "webappwiz/cmd";
|
|
1364
|
+
import { CompositeLogger } from "webappwiz/log";
|
|
1365
|
+
import { z as z15 } from "zod";
|
|
1366
|
+
|
|
1367
|
+
// waiting.ts
|
|
1368
|
+
import { listQueue, moveQueued } from "@stringsync/splates-server/workers";
|
|
1369
|
+
import { cli as cli13 } from "webappwiz/cmd";
|
|
1370
|
+
import { z as z14 } from "zod";
|
|
1371
|
+
var DEV2 = {
|
|
903
1372
|
default: "",
|
|
904
|
-
description: "the
|
|
905
|
-
}
|
|
1373
|
+
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
|
|
1374
|
+
};
|
|
1375
|
+
var splatesWaiting = cli13("queue").description("the agents waiting for a slot on a worker of the running `splates server`: each one another agent started (with `splates add --agent`, or a lane's orchestrator handing out tasks), started or resumed while every worker it can run on is full, waits here, first come, first served, and starts once a slot frees, as when an agent left idle 30 minutes stops; none is ever failed for waiting. An agent you start waits only while no worker it can run on is up, as with this machine's own removed or none on the platform its project requires");
|
|
1376
|
+
splatesWaiting.command("list").description("list the agents waiting for a slot, first in line first: position, project, task, conversation, how long each has waited and why (no free slot, no worker up, or a platform no worker runs, as `needs macOS`)").option("dev", z14.string(), DEV2).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
|
|
1377
|
+
dev: opts.dev,
|
|
1378
|
+
json: opts.json,
|
|
1379
|
+
log: ctx.log,
|
|
1380
|
+
fs: ctx.fs,
|
|
1381
|
+
ps: ctx.ps,
|
|
1382
|
+
send: ctx.fetch
|
|
1383
|
+
}));
|
|
1384
|
+
splatesWaiting.command("move").description("move an agent waiting for a slot to another place in line, from 1 for the next to start, then list the queue").arg("which", z14.string(), {
|
|
1385
|
+
description: "the task it works, or its conversation's id"
|
|
1386
|
+
}).arg("position", z14.coerce.number().int(), {
|
|
1387
|
+
description: "where in line to put it, from 1"
|
|
1388
|
+
}).option("dev", z14.string(), DEV2).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
|
|
1389
|
+
which: opts.which,
|
|
1390
|
+
position: opts.position,
|
|
1391
|
+
dev: opts.dev,
|
|
1392
|
+
json: opts.json,
|
|
1393
|
+
log: ctx.log,
|
|
1394
|
+
fs: ctx.fs,
|
|
1395
|
+
ps: ctx.ps,
|
|
1396
|
+
send: ctx.fetch
|
|
1397
|
+
}));
|
|
1398
|
+
|
|
1399
|
+
// worker.ts
|
|
1400
|
+
var splatesWorker = cli14("worker").description("the machines that run tasks for `splates server`: each dials out to it and runs what it is given, agents, terminals and the git and process reads around them, on its own machine");
|
|
1401
|
+
splatesWorker.command("run").description("run a worker on this machine until stopped: it dials `splates server`, by default the one on this machine through a socket in ~/.splates only you may open, and dials again whenever the link drops, while what it runs carries on; it stops reading a process's output while the server is away too long, so nothing is lost. Once its agents have moved off it, a server on another build has it restart on that build, through `bunx @stringsync/splates@<build>`, as it does when it refuses this worker for another protocol; one that says no build has it exit `outdated`").option("server", z15.string(), {
|
|
1402
|
+
default: "",
|
|
1403
|
+
description: "the `splates server` to dial: its URL, whose /api/workers it opens, or unix:<path>; by default this machine's"
|
|
1404
|
+
}).option("coordinator", z15.string(), {
|
|
1405
|
+
default: "",
|
|
1406
|
+
description: "the old name for --server, still read for workers installed before it, and passed to a build older than it"
|
|
1407
|
+
}).option("slots", z15.coerce.number().int().min(0), {
|
|
906
1408
|
default: 0,
|
|
907
1409
|
description: "how many tasks this worker takes at once; by default one for each core and 4 GB of memory it has, whichever is fewer, as a VM such as WSL2 sees them"
|
|
908
|
-
}).option("id",
|
|
1410
|
+
}).option("id", z15.string(), {
|
|
909
1411
|
default: "",
|
|
910
|
-
description: "what the
|
|
911
|
-
}).option("join",
|
|
1412
|
+
description: "what the server knows this worker by, kept across restarts so it picks up where it was; by default the id it joined as, kept beside its key, else the machine's host name"
|
|
1413
|
+
}).option("join", z15.string(), {
|
|
912
1414
|
default: "",
|
|
913
|
-
description: "a join token from `splates worker add` on the
|
|
914
|
-
}).option("ephemeral",
|
|
1415
|
+
description: "a join token from `splates worker add` on the server, spent to join it before dialing; the key it answers is kept in this machine's system store, or read from SPLATES_WORKER_KEY on one without"
|
|
1416
|
+
}).option("ephemeral", z15.boolean(), {
|
|
915
1417
|
default: false,
|
|
916
1418
|
description: "keep the key --join answers in memory alone, for a machine destroyed rather than restarted, as one a worker provider makes"
|
|
917
|
-
}).option("key-file",
|
|
1419
|
+
}).option("key-file", z15.boolean(), {
|
|
918
1420
|
default: false,
|
|
919
1421
|
description: "keep the key --join answers in ~/.splates/worker.env, which only this user may read, rather than the system store, for a machine whose store is locked or missing at boot, as WSL's is with no one signed in; read it from there on each run after"
|
|
920
|
-
}).option("quiet",
|
|
1422
|
+
}).option("quiet", z15.boolean(), {
|
|
921
1423
|
default: false,
|
|
922
|
-
description: "say only what went wrong, not each time it joins or loses its
|
|
1424
|
+
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"
|
|
923
1425
|
}).action((opts, ctx) => workerRun({
|
|
924
|
-
coordinator: opts.coordinator,
|
|
1426
|
+
coordinator: opts.server || opts.coordinator,
|
|
925
1427
|
slots: opts.slots,
|
|
926
1428
|
id: opts.id,
|
|
927
|
-
join: opts.join,
|
|
1429
|
+
join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
|
|
928
1430
|
ephemeral: opts.ephemeral,
|
|
929
1431
|
keyFile: opts["key-file"],
|
|
930
|
-
log: opts.quiet ?
|
|
1432
|
+
log: opts.quiet ? quietLog(ctx) : ctx.log,
|
|
931
1433
|
ps: ctx.ps,
|
|
932
1434
|
fs: ctx.fs
|
|
933
1435
|
}));
|
|
934
|
-
splatesWorker.command("join").description("spend a join token from `splates worker add` and keep the key it answers, as `splates worker run --join` does, then exit, for an installer that leaves running the worker to a service").option("
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1436
|
+
splatesWorker.command("join").description("spend a join token from `splates worker add` and keep the key it answers, as `splates worker run --join` does, then exit, for an installer that leaves running the worker to a service").option("server", z15.string(), {
|
|
1437
|
+
default: "",
|
|
1438
|
+
description: "the `splates server` to join: its URL"
|
|
1439
|
+
}).option("coordinator", z15.string(), {
|
|
1440
|
+
default: "",
|
|
1441
|
+
description: "the old name for --server"
|
|
1442
|
+
}).option("join", z15.string(), {
|
|
1443
|
+
default: "",
|
|
1444
|
+
description: "the join token `splates worker add` printed there; by default read from SPLATES_WORKER_JOIN, which keeps it out of the process list other users of the machine can read"
|
|
1445
|
+
}).option("id", z15.string(), {
|
|
1446
|
+
default: "",
|
|
1447
|
+
description: "what the server knows this worker by, kept beside its key so a change of host name leaves it the same; by default the machine's host name"
|
|
1448
|
+
}).option("key-file", z15.boolean(), {
|
|
942
1449
|
default: false,
|
|
943
1450
|
description: "keep the key in ~/.splates/worker.env, which only this user may read, rather than the system store, for `splates worker run --key-file` to read"
|
|
944
1451
|
}).action((opts, ctx) => workerJoin({
|
|
945
|
-
coordinator: opts.coordinator,
|
|
946
|
-
join: opts.join,
|
|
1452
|
+
coordinator: opts.server || opts.coordinator,
|
|
1453
|
+
join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
|
|
947
1454
|
id: opts.id,
|
|
948
1455
|
keyFile: opts["key-file"],
|
|
949
1456
|
log: ctx.log,
|
|
@@ -951,13 +1458,14 @@ splatesWorker.command("join").description("spend a join token from `splates work
|
|
|
951
1458
|
fs: ctx.fs
|
|
952
1459
|
}));
|
|
953
1460
|
splatesWorker.command("recipe").description("print the shell script, run as root on a fresh Debian or Ubuntu machine, that installs what a Linux worker needs: bun, git, the shared browser and this build of splates, for the user `splates`. It is what a provider's snapshot is built from, and what the Windows installer runs in WSL").action((_opts, ctx) => workerRecipe({ log: ctx.log }));
|
|
954
|
-
splatesWorker.command("list").description("list the workers the running `splates server` has: kind (local over its socket, registered otherwise), platform, build, protocol, slots used and total, health with when it was last heard, tasks and $/hour, then any it refused for speaking another protocol").option("dev",
|
|
1461
|
+
splatesWorker.command("list").description("list the workers the running `splates server` has: kind (local over its socket, registered otherwise), platform, build, protocol, slots used and total, health with when it was last heard, tasks and $/hour, then any it refused for speaking another protocol").option("dev", z15.string(), {
|
|
955
1462
|
default: "",
|
|
956
1463
|
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
|
|
957
|
-
}).option("json",
|
|
1464
|
+
}).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
|
|
958
1465
|
dev: opts.dev,
|
|
959
1466
|
json: opts.json,
|
|
960
1467
|
log: ctx.log,
|
|
1468
|
+
fs: ctx.fs,
|
|
961
1469
|
ps: ctx.ps,
|
|
962
1470
|
send: ctx.fetch
|
|
963
1471
|
}));
|
|
@@ -965,71 +1473,84 @@ var dev = {
|
|
|
965
1473
|
default: "",
|
|
966
1474
|
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
|
|
967
1475
|
};
|
|
968
|
-
splatesWorker.command("add").description("add a worker to the running `splates server`: prints a join token, single use and good for 15 minutes, for `splates worker run --join <token>` on the machine to join, which gets a key of its own that `splates worker remove` revokes; with --local N, makes this machine's own worker run N agents at once and take new ones again, adding it back if it was removed; with --provider, has the `workerProvider` in splates.config.ts make one, booted from the snapshot splates builds for it, which joins on its own and whose machine `splates worker remove` destroys").option("local",
|
|
1476
|
+
splatesWorker.command("add").description("add a worker to the running `splates server`: prints a join token, single use and good for 15 minutes, for `splates worker run --join <token>` on the machine to join, which gets a key of its own that `splates worker remove` revokes; with --local N, makes this machine's own worker run N agents at once and take new ones again, adding it back if it was removed; with --provider, has the `workerProvider` in splates.config.ts make one, booted from the snapshot splates builds for it, which joins on its own and whose machine `splates worker remove` destroys").option("local", z15.coerce.number().int().min(0), {
|
|
969
1477
|
default: 0,
|
|
970
1478
|
description: "this machine's own worker, with this many slots"
|
|
971
|
-
}).option("provider",
|
|
1479
|
+
}).option("provider", z15.boolean(), {
|
|
972
1480
|
default: false,
|
|
973
1481
|
description: "have the server's worker provider make the worker, of its cheapest type unless --type says"
|
|
974
|
-
}).option("type",
|
|
1482
|
+
}).option("type", z15.string(), {
|
|
975
1483
|
default: "",
|
|
976
1484
|
description: "the worker provider's type to make, as it names it; implies --provider"
|
|
977
|
-
}).option("dev",
|
|
1485
|
+
}).option("dev", z15.string(), dev).action((opts, ctx) => addWorker({
|
|
978
1486
|
local: opts.local,
|
|
979
1487
|
provider: opts.provider,
|
|
980
1488
|
type: opts.type,
|
|
981
1489
|
dev: opts.dev,
|
|
982
1490
|
log: ctx.log,
|
|
1491
|
+
fs: ctx.fs,
|
|
983
1492
|
ps: ctx.ps,
|
|
984
1493
|
send: ctx.fetch
|
|
985
1494
|
}));
|
|
986
|
-
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",
|
|
1495
|
+
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(), {
|
|
987
1496
|
description: "the worker, as `splates worker list` names it"
|
|
988
|
-
}).option("undo",
|
|
1497
|
+
}).option("undo", z15.boolean(), {
|
|
989
1498
|
default: false,
|
|
990
1499
|
description: "take new agents again"
|
|
991
|
-
}).option("dev",
|
|
1500
|
+
}).option("dev", z15.string(), dev).action((opts, ctx) => drainWorker({
|
|
992
1501
|
id: opts.id,
|
|
993
1502
|
undo: opts.undo,
|
|
994
1503
|
dev: opts.dev,
|
|
995
1504
|
log: ctx.log,
|
|
1505
|
+
fs: ctx.fs,
|
|
996
1506
|
ps: ctx.ps,
|
|
997
1507
|
send: ctx.fetch
|
|
998
1508
|
}));
|
|
999
|
-
splatesWorker.command("wake").description("wake a worker asleep at home, a Windows PC that joined in WSL2: a worker awake on its network sends the Wake-on-LAN packet. Placement wakes one by itself when agents queue, before making a worker").arg("id",
|
|
1509
|
+
splatesWorker.command("wake").description("wake a worker asleep at home, a Windows PC that joined in WSL2: a worker awake on its network sends the Wake-on-LAN packet. Placement wakes one by itself when agents queue, before making a worker").arg("id", z15.string(), {
|
|
1000
1510
|
description: "the worker, as `splates worker list` names it"
|
|
1001
|
-
}).option("dev",
|
|
1511
|
+
}).option("dev", z15.string(), dev).action((opts, ctx) => wakeWorker({
|
|
1002
1512
|
id: opts.id,
|
|
1003
1513
|
dev: opts.dev,
|
|
1004
1514
|
log: ctx.log,
|
|
1515
|
+
fs: ctx.fs,
|
|
1005
1516
|
ps: ctx.ps,
|
|
1006
1517
|
send: ctx.fetch
|
|
1007
1518
|
}));
|
|
1008
|
-
splatesWorker.command("slots").description("set how many agents a worker runs at once, from now on, over what it started with").arg("id",
|
|
1519
|
+
splatesWorker.command("slots").description("set how many agents a worker runs at once, from now on, over what it started with").arg("id", z15.string(), {
|
|
1009
1520
|
description: "the worker, as `splates worker list` names it"
|
|
1010
|
-
}).arg("slots",
|
|
1521
|
+
}).arg("slots", z15.coerce.number().int().min(1), {
|
|
1011
1522
|
description: "how many at once"
|
|
1012
|
-
}).option("dev",
|
|
1523
|
+
}).option("dev", z15.string(), dev).action((opts, ctx) => setWorkerSlots({
|
|
1013
1524
|
id: opts.id,
|
|
1014
1525
|
slots: opts.slots,
|
|
1015
1526
|
dev: opts.dev,
|
|
1016
1527
|
log: ctx.log,
|
|
1528
|
+
fs: ctx.fs,
|
|
1017
1529
|
ps: ctx.ps,
|
|
1018
1530
|
send: ctx.fetch
|
|
1019
1531
|
}));
|
|
1020
|
-
splatesWorker.command("remove").description("remove a worker, revoking its key: one with agents on it drains and is removed once they end; --force ends them now, frees their slots, and places each again on another worker, to carry on its task from the checkpoint it last pushed. This machine's own worker, `local`, stops taking agents, its agents running to their end, and leaves this machine only coordinating until `splates worker add --local <slots>`; it takes no --force").arg("id",
|
|
1532
|
+
splatesWorker.command("remove").description("remove a worker, revoking its key: one with agents on it drains and is removed once they end; --force ends them now, frees their slots, and places each again on another worker, to carry on its task from the checkpoint it last pushed. This machine's own worker, `local`, stops taking agents, its agents running to their end, and leaves this machine only coordinating until `splates worker add --local <slots>`; it takes no --force").arg("id", z15.string(), {
|
|
1021
1533
|
description: "the worker, as `splates worker list` names it"
|
|
1022
|
-
}).option("force",
|
|
1534
|
+
}).option("force", z15.boolean(), {
|
|
1023
1535
|
default: false,
|
|
1024
1536
|
description: "end the agents on it now rather than wait for them"
|
|
1025
|
-
}).option("dev",
|
|
1537
|
+
}).option("dev", z15.string(), dev).action((opts, ctx) => removeWorker({
|
|
1026
1538
|
id: opts.id,
|
|
1027
1539
|
force: opts.force,
|
|
1028
1540
|
dev: opts.dev,
|
|
1029
1541
|
log: ctx.log,
|
|
1542
|
+
fs: ctx.fs,
|
|
1030
1543
|
ps: ctx.ps,
|
|
1031
1544
|
send: ctx.fetch
|
|
1032
1545
|
}));
|
|
1546
|
+
splatesWorker.mount("queue", splatesWaiting);
|
|
1547
|
+
function quietLog(ctx) {
|
|
1548
|
+
const file = new FileLogger(ctx.fs, `${splatesHome3(ctx.ps)}/worker.log`);
|
|
1549
|
+
return new CompositeLogger([
|
|
1550
|
+
{ info: () => {}, error: (...said) => ctx.log.error(...said) },
|
|
1551
|
+
file
|
|
1552
|
+
]);
|
|
1553
|
+
}
|
|
1033
1554
|
|
|
1034
1555
|
// splates.ts
|
|
1035
1556
|
function projectArgs(argv) {
|
|
@@ -1045,9 +1566,16 @@ function projectArgs(argv) {
|
|
|
1045
1566
|
}
|
|
1046
1567
|
return { argv };
|
|
1047
1568
|
}
|
|
1048
|
-
var splates =
|
|
1049
|
-
splates.command("
|
|
1050
|
-
splates.command("
|
|
1569
|
+
var splates = cli15("splates").use(exits());
|
|
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));
|
|
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) => {
|
|
1573
|
+
const format = await migrateRecords(ctx);
|
|
1574
|
+
if (ctx.ps.env(MIGRATING) === undefined) {
|
|
1575
|
+
ctx.log.info(`splates's records are in format ${format}`);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
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) => {
|
|
1051
1579
|
const code = await update({
|
|
1052
1580
|
root: ctx.git.root,
|
|
1053
1581
|
fs: ctx.fs,
|
|
@@ -1059,7 +1587,7 @@ splates.command("update").use(repository7({ needs: "own" })).description("bring
|
|
|
1059
1587
|
ctx.ps.exit(code);
|
|
1060
1588
|
}
|
|
1061
1589
|
});
|
|
1062
|
-
splates.command("heavy").description("run a heavy command (a full test run, an install) in one of this machine's heavy-job slots, waiting for one to come free first, so agents testing at once queue rather than run out of memory; `SPLATES_HEAVY_JOBS` sets how many run at once, 2 by default").rest("command",
|
|
1590
|
+
splates.command("heavy").description("run a heavy command (a full test run, an install) in one of this machine's heavy-job slots, waiting for one to come free first, so agents testing at once queue rather than run out of memory; `SPLATES_HEAVY_JOBS` sets how many run at once, 2 by default").rest("command", z16.string(), {
|
|
1063
1591
|
description: "the command and its arguments, after `--`"
|
|
1064
1592
|
}).action(async (opts, ctx) => {
|
|
1065
1593
|
const code = await runHeavy(ctx, opts.command);
|
|
@@ -1067,61 +1595,66 @@ splates.command("heavy").description("run a heavy command (a full test run, an i
|
|
|
1067
1595
|
ctx.ps.exit(code);
|
|
1068
1596
|
}
|
|
1069
1597
|
});
|
|
1070
|
-
splates.command("forget").use(
|
|
1071
|
-
splates.command("add").use(
|
|
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(), {
|
|
1600
|
+
description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
|
|
1601
|
+
}).option("base", z16.string(), {
|
|
1072
1602
|
default: "",
|
|
1073
1603
|
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"
|
|
1074
|
-
}).option("
|
|
1604
|
+
}).option("task", z16.string(), {
|
|
1075
1605
|
default: "",
|
|
1076
|
-
description: "
|
|
1077
|
-
}).option("goal",
|
|
1606
|
+
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"
|
|
1607
|
+
}).option("goal", z16.string(), {
|
|
1078
1608
|
default: "",
|
|
1079
|
-
description: "one or two lines on what done means, written under `## Goal` in the task's SPLATES.md; required unless --
|
|
1080
|
-
}).option("agent",
|
|
1609
|
+
description: "one or two lines on what done means, written under `## Goal` in the task's SPLATES.md; required unless --task seeds the Goal"
|
|
1610
|
+
}).option("agent", z16.string(), {
|
|
1081
1611
|
default: "",
|
|
1082
|
-
description: "
|
|
1083
|
-
}).option("model",
|
|
1612
|
+
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"
|
|
1613
|
+
}).option("model", z16.string(), {
|
|
1084
1614
|
default: "",
|
|
1085
1615
|
description: "with --agent: the model it works on, by its id or name on the roster (default: the agent's own)"
|
|
1086
|
-
}).option("effort",
|
|
1616
|
+
}).option("effort", z16.string(), {
|
|
1087
1617
|
default: "",
|
|
1088
1618
|
description: "with --agent: the thinking level it works at, one the agent offers (default: the agent's own)"
|
|
1089
|
-
}).option("prompt",
|
|
1619
|
+
}).option("prompt", z16.string(), {
|
|
1090
1620
|
default: "",
|
|
1091
|
-
description: "with --agent: what to tell the agent beyond claiming the task
|
|
1092
|
-
}).option("
|
|
1093
|
-
default:
|
|
1094
|
-
description: "with --agent:
|
|
1621
|
+
description: "with --agent: what to tell the agent beyond claiming the task"
|
|
1622
|
+
}).option("held", z16.string(), {
|
|
1623
|
+
default: "",
|
|
1624
|
+
description: "with --agent: the conversation `splates server`'s queue held for the queued task, which takes the task rather than a new one"
|
|
1095
1625
|
}).action(async (opts, ctx) => {
|
|
1096
|
-
|
|
1097
|
-
|
|
1626
|
+
await refuseGroomer4(ctx.service, "add");
|
|
1627
|
+
if (opts.agent === "" && (opts.model !== "" || opts.effort !== "" || opts.prompt !== "" || opts.held !== "")) {
|
|
1628
|
+
fail9("usage", "--model, --effort, --prompt and --held go with --agent", {
|
|
1629
|
+
task: opts.slug
|
|
1630
|
+
});
|
|
1098
1631
|
}
|
|
1099
|
-
if (opts.goal.trim() === "" && opts.
|
|
1100
|
-
|
|
1632
|
+
if (opts.goal.trim() === "" && opts.task === "") {
|
|
1633
|
+
fail9("usage", `say what done means: \`splates add ${opts.slug} --goal "<one or two lines>"\`, or start queued tasks with --task`, { task: opts.slug });
|
|
1101
1634
|
}
|
|
1102
1635
|
const dispatch = opts.agent === "" ? undefined : {
|
|
1103
1636
|
agent: opts.agent,
|
|
1104
1637
|
...opts.model === "" ? {} : { model: opts.model },
|
|
1105
1638
|
...await effortIn(ctx, opts.agent, opts.effort, {
|
|
1106
|
-
task: opts.
|
|
1639
|
+
task: opts.slug
|
|
1107
1640
|
}),
|
|
1108
|
-
...opts.prompt === "" ? {} : { prompt: opts.prompt }
|
|
1641
|
+
...opts.prompt === "" ? {} : { prompt: opts.prompt },
|
|
1642
|
+
...opts.held === "" ? {} : { held: opts.held }
|
|
1109
1643
|
};
|
|
1110
1644
|
const agents = dispatch === undefined ? [] : await seeded2(rosterOf2(ctx));
|
|
1111
|
-
const all = await ctx.
|
|
1112
|
-
const subjects =
|
|
1113
|
-
await ctx.journal.record("add", opts.
|
|
1645
|
+
const all = await ctx.tasks.all();
|
|
1646
|
+
const subjects = taskIds(opts.task).flatMap((id) => all.find((each) => each.id === id)?.subject ?? []);
|
|
1647
|
+
await ctx.journal.record("add", opts.slug, () => add(ctx, opts.slug, {
|
|
1114
1648
|
base: opts.base || undefined,
|
|
1115
|
-
|
|
1649
|
+
tasks: taskIds(opts.task),
|
|
1116
1650
|
goal: opts.goal.trim() || undefined,
|
|
1117
1651
|
dispatch,
|
|
1118
1652
|
agents,
|
|
1119
1653
|
away: ctx.elsewhere !== null,
|
|
1120
|
-
|
|
1121
|
-
onTree: dispatch === undefined ? () => follow(ctx, opts.task, { base: opts.base || undefined }) : undefined
|
|
1654
|
+
onTree: dispatch === undefined ? () => follow(ctx, opts.slug, { base: opts.base || undefined }) : undefined
|
|
1122
1655
|
}), {
|
|
1123
1656
|
base: opts.base || ctx.config.trunk,
|
|
1124
|
-
|
|
1657
|
+
tasks: taskIds(opts.task),
|
|
1125
1658
|
goal: opts.goal.trim() || null,
|
|
1126
1659
|
subjects,
|
|
1127
1660
|
agent: dispatch?.agent ?? null,
|
|
@@ -1131,85 +1664,56 @@ splates.command("add").use(repository7({ needs: "tasks" })).description("start a
|
|
|
1131
1664
|
await serveProject(ctx);
|
|
1132
1665
|
}
|
|
1133
1666
|
});
|
|
1134
|
-
splates.command("claim").use(
|
|
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) => {
|
|
1135
1668
|
await ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task), (claimed) => claimed);
|
|
1136
1669
|
await follow(ctx, opts.task);
|
|
1137
1670
|
});
|
|
1138
|
-
splates.command("merge").use(
|
|
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(), {
|
|
1672
|
+
default: "",
|
|
1673
|
+
description: "squash the task's commits into one with this message before landing, saying what the base gains in at least 5 characters"
|
|
1674
|
+
}).action(async (opts, ctx) => {
|
|
1675
|
+
await refuseGroomer4(ctx.service, "merge");
|
|
1139
1676
|
const task = await here(ctx);
|
|
1140
|
-
const landing = task === null ? null : await ctx.service.find(task);
|
|
1141
1677
|
const facts = {};
|
|
1142
|
-
await ctx.journal.record("merge", task, () => merge(ctx, ctx.ps.cwd(), facts), facts);
|
|
1678
|
+
await ctx.journal.record("merge", task, () => merge(ctx, ctx.ps.cwd(), facts, { message: opts.message }), facts);
|
|
1143
1679
|
if (task !== null) {
|
|
1144
|
-
await
|
|
1680
|
+
await addFactTasks(ctx, task).catch((error) => ctx.log.error(`could not queue tasks for what the merge found: ${error}`));
|
|
1145
1681
|
await follow(ctx, null, { from: task });
|
|
1146
1682
|
}
|
|
1147
|
-
if (landing !== null) {
|
|
1148
|
-
await reportToCaller(ctx, landing);
|
|
1149
|
-
}
|
|
1150
1683
|
});
|
|
1151
|
-
splates.command("remove").use(
|
|
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(), {
|
|
1152
1685
|
default: false,
|
|
1153
1686
|
description: "discard even when another agent holds the lease"
|
|
1154
|
-
}).option("reason",
|
|
1155
|
-
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)"
|
|
1687
|
+
}).option("reason", z16.optional(z16.enum(REMOVE_REASONS)), {
|
|
1688
|
+
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)"
|
|
1156
1689
|
}).action(async (opts, ctx) => {
|
|
1157
|
-
const removing = await ctx.service.find(opts.task);
|
|
1158
1690
|
await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force, reason: opts.reason }), ({ released }) => ({ why: opts.reason ?? null, released }));
|
|
1159
1691
|
await follow(ctx, null, { from: opts.task });
|
|
1160
|
-
await reportToCaller(ctx, removing);
|
|
1161
1692
|
});
|
|
1162
|
-
splates.command("list").use(
|
|
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(), {
|
|
1163
1694
|
default: false,
|
|
1164
1695
|
description: "add each task's Goal and changed files (committed or not), to check for overlap before starting"
|
|
1165
1696
|
}).action((opts, ctx) => list(ctx, { json: opts.json, files: opts.files }));
|
|
1166
|
-
splates.command("show").use(
|
|
1167
|
-
splates.command("wait").use(
|
|
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(), {
|
|
1168
1699
|
default: DEFAULT_TIMEOUT3.secs,
|
|
1169
1700
|
description: "how long to wait before giving up"
|
|
1170
1701
|
}).action((opts, ctx) => {
|
|
1171
1702
|
const timeout = Duration3.secs(opts["timeout-secs"]);
|
|
1172
1703
|
return ctx.journal.waiting("wait", opts.task, { until: untilAfter3(timeout) }, () => wait(ctx, opts.task, { timeout }));
|
|
1173
1704
|
});
|
|
1174
|
-
splates.command("log").use(
|
|
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(), {
|
|
1175
1706
|
default: DEFAULT_COUNT,
|
|
1176
1707
|
description: "how many entries to show"
|
|
1177
|
-
}).option("json",
|
|
1178
|
-
splates.
|
|
1179
|
-
|
|
1180
|
-
description: "port to listen on, or the next open one above it"
|
|
1181
|
-
}).option("allow-hosts", z13.string(), {
|
|
1182
|
-
default: "",
|
|
1183
|
-
description: "comma-separated host names the page may also be reached by, like a tunnel's; put Cloudflare Access in front of them with --access-team and --access-aud"
|
|
1184
|
-
}).option("access-team", z13.string(), {
|
|
1185
|
-
default: "",
|
|
1186
|
-
description: "the Cloudflare Access team in front of --allow-hosts, as in <team>.cloudflareaccess.com; with --access-aud, those hosts answer only to a verified Access token, and may then use conversations and terminals"
|
|
1187
|
-
}).option("access-aud", z13.string(), {
|
|
1188
|
-
default: "",
|
|
1189
|
-
description: "the AUD tag of that Access application"
|
|
1190
|
-
}).option("tunnel", z13.string(), {
|
|
1191
|
-
default: "",
|
|
1192
|
-
description: "the public host name of a tunnel to run, with cloudflared or the tunnel provider named by splates.config.ts in the repository it is run in, if any, which is allowed as a host too; refused without --access-team and --access-aud, so a tunnel never exposes the page unauthenticated. With none of the tunnel and Access flags, the tunnel `splates tunnel setup` saved runs, if any"
|
|
1193
|
-
}).option("tunnel-name", z13.string(), {
|
|
1194
|
-
default: "",
|
|
1195
|
-
description: "the named tunnel cloudflared runs for --tunnel, by default the one its own config names; not with a tunnel provider of splates.config.ts"
|
|
1196
|
-
}).option("ports-host", z13.string(), {
|
|
1197
|
-
default: "",
|
|
1198
|
-
description: "the wildcard base host to serve task ports under, saved for this machine so a bare `splates server` serves it too: each port a task's processes listen on opens at https://<task>-<port>.<base>, to whoever Cloudflare Access lets in, and is refused without Access; routing *.<base> here, its DNS and TLS, are yours. `none` forgets it, leaving each task port forwarded to a port on this machine's loopback alone"
|
|
1199
|
-
}).option("worker-slots", z13.coerce.number().int().min(1), {
|
|
1200
|
-
default: LOCAL_SLOTS,
|
|
1201
|
-
description: "how many agents that other agents started (with `splates add --agent`, or a lane's orchestrator handing out todos) this machine's worker runs at once; past them, each new or resumed one waits in the queue, first come, first served, which `splates queue` shows and reorders. An agent you start runs at once on any worker up, taking no slot. Read as the server starts its worker, which a restart from the page leaves running"
|
|
1202
|
-
}).option("slot", z13.boolean(), {
|
|
1203
|
-
default: false,
|
|
1204
|
-
description: "serve this checkout's splates at https://<word>.<zone>, a slot of its own on the zone `splates tunnel setup` uses, behind the same Access policy, so several run side by side and a phone opens each by name: a task's worktree keeps its word, as fox, until the task merges or is removed, which stops its server; run again, it stops the tree's last server first. Refused in a splates repository's main tree, which every agent shares. A slot keeps its own projects in ~/.splates/slots/<word>, apart from yours, starting with a throwaway project there; not with the tunnel and Access flags"
|
|
1205
|
-
}).action((opts, ctx) => serveDev(ctx, opts));
|
|
1206
|
-
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, which on a worker whose coordinator runs elsewhere only that worker's own ports have; give the user both").arg("task", z13.string(), {
|
|
1708
|
+
}).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
|
|
1709
|
+
splates.mount("server", splatesServer);
|
|
1710
|
+
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, which on a worker whose server runs elsewhere only that worker's own ports have; give the user both").arg("task", z16.string(), {
|
|
1207
1711
|
default: "",
|
|
1208
1712
|
description: "only this task's ports; every task's by default"
|
|
1209
|
-
}).option("dev",
|
|
1713
|
+
}).option("dev", z16.string(), {
|
|
1210
1714
|
default: "",
|
|
1211
1715
|
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 the coordinator this machine's worker relays to, else this machine's on its default port"
|
|
1212
|
-
}).option("json",
|
|
1716
|
+
}).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
|
|
1213
1717
|
task: opts.task,
|
|
1214
1718
|
dev: opts.dev,
|
|
1215
1719
|
json: opts.json,
|
|
@@ -1218,118 +1722,55 @@ splates.command("ports").description("list the ports tasks serve, found by the r
|
|
|
1218
1722
|
ps: ctx.ps,
|
|
1219
1723
|
send: ctx.fetch
|
|
1220
1724
|
}));
|
|
1221
|
-
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",
|
|
1725
|
+
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({
|
|
1222
1726
|
roster: rosterOf2(ctx),
|
|
1223
1727
|
offered: await offeredOf(ctx).all(),
|
|
1224
1728
|
log: ctx.log
|
|
1225
1729
|
}, { json: opts.json }));
|
|
1226
|
-
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",
|
|
1730
|
+
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(), {
|
|
1227
1731
|
default: "",
|
|
1228
1732
|
description: "a step's id; omit for every area's progress"
|
|
1229
|
-
}).option("json",
|
|
1733
|
+
}).option("json", z16.boolean(), { default: false, description: "emit JSON" }).option("skip", z16.boolean(), {
|
|
1230
1734
|
default: false,
|
|
1231
1735
|
description: "with a step: stop asking for it"
|
|
1232
|
-
}).option("unskip",
|
|
1736
|
+
}).option("unskip", z16.boolean(), {
|
|
1233
1737
|
default: false,
|
|
1234
1738
|
description: "with a step: ask for it again"
|
|
1235
|
-
}).
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
fail6("usage", "--skip or --unskip, not both", { step: opts.step });
|
|
1241
|
-
}
|
|
1242
|
-
await setup(ctx, opts);
|
|
1243
|
-
});
|
|
1244
|
-
splates.command("path").use(repository7({ 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", z13.string(), {
|
|
1739
|
+
}).option("undo", z16.boolean(), {
|
|
1740
|
+
default: false,
|
|
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"
|
|
1742
|
+
}).action((opts, ctx) => setup(ctx, opts));
|
|
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(), {
|
|
1245
1744
|
default: "",
|
|
1246
1745
|
description: "task name; omit for the main tree"
|
|
1247
1746
|
}).action((opts, ctx) => path(ctx, opts.task || undefined));
|
|
1248
|
-
splates.command("checkpoint").use(
|
|
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) => {
|
|
1249
1748
|
await checkpoint(ctx, ctx.ps.cwd());
|
|
1250
1749
|
});
|
|
1251
|
-
splates.command("
|
|
1252
|
-
description: "why this needs a human; with --review, what to look at before approving"
|
|
1253
|
-
}).option("task", z13.string(), {
|
|
1254
|
-
default: "",
|
|
1255
|
-
description: "task name, when run outside its worktree"
|
|
1256
|
-
}).option("review", z13.boolean(), {
|
|
1257
|
-
default: false,
|
|
1258
|
-
description: "ask a person to approve merging, the reason saying what to look at, approved in chat; refused until the task is ready to merge, with every question under ## Blocked and step under ## Next checked"
|
|
1259
|
-
}).action(async (opts, ctx) => {
|
|
1260
|
-
const task = opts.task || await here(ctx);
|
|
1261
|
-
await ctx.journal.record("escalate", task, () => escalate(ctx, opts.reason, ctx.ps.cwd(), {
|
|
1262
|
-
task: opts.task || undefined,
|
|
1263
|
-
review: opts.review
|
|
1264
|
-
}), (facts) => facts);
|
|
1265
|
-
if (task !== null) {
|
|
1266
|
-
await reportToCaller(ctx, await ctx.service.find(task));
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
splates.command("tell").use(repository7({ needs: "tasks" })).description("say something in the conversation of the agent a task was handed to with `splates add --agent` or `splates handoff --relay`, as the user would: how an orchestrator wakes an agent that sits idle, stops it for a change of plans, or passes on what its lane taught it, and how a relaying agent passes on the user's answers; only the conversation that handed the task out may").arg("task", z13.string(), { description: "task name" }).arg("text", z13.string(), { description: "what to say, as markdown" }).option("stop", z13.boolean(), {
|
|
1270
|
-
default: false,
|
|
1271
|
-
description: "stop the turn the agent is running first, as the user's Stop does, then say text: for a change of plans"
|
|
1272
|
-
}).action((opts, ctx) => ctx.journal.record("tell", opts.task, () => tell(ctx, opts.task, opts.text, { stop: opts.stop }), (to) => ({ to })));
|
|
1273
|
-
splates.command("handoff").use(repository7({ needs: "own" })).description("hand the task you are at work on to another agent on the roster (`splates agents`), which `splates server` starts in its worktree, told `[SPLATES HANDOFF <task>]` to claim it and carry on from its SPLATES.md; it answers to the user as you did, or with --relay to you, and this conversation leaves the task, though a lane's orchestrator that started you goes on hearing of it; refused to an agent that answers to another conversation, and for a task another conversation is at work on").arg("task", z13.string(), { description: "task name" }).option("agent", z13.string(), {
|
|
1274
|
-
description: "the agent on the roster to hand it to"
|
|
1275
|
-
}).option("model", z13.string(), {
|
|
1276
|
-
default: "",
|
|
1277
|
-
description: "the model it works on, by its id or name on the roster (default: the agent's own)"
|
|
1278
|
-
}).option("effort", z13.string(), {
|
|
1279
|
-
default: "",
|
|
1280
|
-
description: "the thinking level it works at, one the agent offers (default: the agent's own)"
|
|
1281
|
-
}).option("prompt", z13.string(), {
|
|
1282
|
-
default: "",
|
|
1283
|
-
description: "what to tell the agent beyond taking the task over, such as why it is handed on"
|
|
1284
|
-
}).option("relay", z13.boolean(), {
|
|
1285
|
-
default: false,
|
|
1286
|
-
description: "for work you judged another model fits better: the agent answers to you rather than the user, reporting back with `[SPLATES REPORT <task>]` and told what the user says with `splates tell`; needs `splates server`"
|
|
1287
|
-
}).action(async (opts, ctx) => {
|
|
1288
|
-
const agents = await seeded2(rosterOf2(ctx));
|
|
1289
|
-
const effort = await effortIn(ctx, opts.agent, opts.effort, {
|
|
1290
|
-
task: opts.task
|
|
1291
|
-
});
|
|
1292
|
-
await ctx.journal.record("handoff", opts.task, () => handoff(ctx, opts.task, {
|
|
1293
|
-
agent: opts.agent,
|
|
1294
|
-
...opts.model === "" ? {} : { model: opts.model },
|
|
1295
|
-
...effort,
|
|
1296
|
-
...opts.prompt === "" ? {} : { prompt: opts.prompt }
|
|
1297
|
-
}, agents, { relay: opts.relay }), (picked) => ({
|
|
1298
|
-
agent: picked.agent,
|
|
1299
|
-
model: picked.model ?? null,
|
|
1300
|
-
relay: opts.relay
|
|
1301
|
-
}));
|
|
1302
|
-
await follow(ctx, null, { from: opts.task });
|
|
1303
|
-
});
|
|
1304
|
-
splates.command("attach").use(repository7({ 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, and 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", z13.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(), {
|
|
1305
1751
|
default: "",
|
|
1306
1752
|
description: "files to store, comma separated"
|
|
1307
|
-
}).option("task",
|
|
1753
|
+
}).option("task", z16.string(), {
|
|
1308
1754
|
default: "",
|
|
1309
1755
|
description: "task name, when run outside its worktree"
|
|
1310
|
-
}).option("remove",
|
|
1756
|
+
}).option("remove", z16.string(), {
|
|
1311
1757
|
default: "",
|
|
1312
1758
|
description: "files it holds to drop first, comma separated, by path or stored name"
|
|
1313
|
-
}).option("conversation",
|
|
1759
|
+
}).option("conversation", z16.string(), {
|
|
1314
1760
|
default: "",
|
|
1315
1761
|
description: "conversation to keep the files with, when $SPLATES_CONVERSATION is not set"
|
|
1316
|
-
}).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx,
|
|
1762
|
+
}).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList6(opts.files), ctx.ps.cwd(), {
|
|
1317
1763
|
task: opts.task || undefined,
|
|
1318
|
-
remove:
|
|
1764
|
+
remove: commaList6(opts.remove),
|
|
1319
1765
|
conversation: opts.conversation || undefined
|
|
1320
|
-
}), (stored) => ({ stored, removed:
|
|
1321
|
-
splates.command("retry").use(
|
|
1322
|
-
splates.mount("artifact", splatesArtifact).mount("
|
|
1323
|
-
|
|
1324
|
-
// main.ts
|
|
1325
|
-
async function main() {
|
|
1326
|
-
if (basename2(process.argv[1] ?? "") === LEGACY) {
|
|
1327
|
-
legacyWarnings.once(LEGACY, "splates");
|
|
1328
|
-
}
|
|
1329
|
-
const ps = systemPs();
|
|
1330
|
-
const { project, argv } = projectArgs(ps.args);
|
|
1331
|
-
await splates.run({ fs: new NodeFs, ps, assets, project }, argv);
|
|
1332
|
-
}
|
|
1766
|
+
}), (stored) => ({ stored, removed: commaList6(opts.remove) })));
|
|
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));
|
|
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);
|
|
1333
1769
|
|
|
1334
1770
|
// index.ts
|
|
1335
|
-
|
|
1771
|
+
if (basename2(process.argv[1] ?? "") === LEGACY) {
|
|
1772
|
+
legacyWarnings.once(LEGACY, "splates");
|
|
1773
|
+
}
|
|
1774
|
+
var ps = systemPs();
|
|
1775
|
+
var { project, argv } = projectArgs(ps.args);
|
|
1776
|
+
await splates.run({ fs: new NodeFs, ps, assets, project }, argv);
|