@miroshnik/elves 0.0.0 → 2026.1018.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/AGENTS.md +310 -0
  2. package/README.md +344 -0
  3. package/bin/ai-dev.ts +7 -0
  4. package/bin/elves.ts +2847 -0
  5. package/bin/shims/timeout +113 -0
  6. package/claude/CLAUDE.md +71 -0
  7. package/dist/github/scripts/github.js +3428 -0
  8. package/dist/spec/scripts/architecture.js +628 -0
  9. package/dist/spec/scripts/eslint-config.js +54 -0
  10. package/dist/spec/scripts/harness-qzb932c8.js +959 -0
  11. package/dist/spec/scripts/harness-v9s1v9w3.js +16045 -0
  12. package/dist/spec/scripts/harness.js +53 -0
  13. package/dist/spec/scripts/knip-hints.cjs +34 -0
  14. package/docs/ci-concurrency.md +82 -0
  15. package/docs/cloud-sessions.md +203 -0
  16. package/docs/judgment.md +81 -0
  17. package/docs/parallel-checkouts.md +220 -0
  18. package/docs/pr-checks.md +117 -0
  19. package/docs/testing.md +304 -0
  20. package/package.json +70 -1
  21. package/skills/ci-wait/SKILL.md +180 -0
  22. package/skills/ci-wait/scripts/wait-ci.sh +377 -0
  23. package/skills/dashboard/SKILL.md +212 -0
  24. package/skills/dashboard/scripts/dashboard.ts +912 -0
  25. package/skills/dashboard/scripts/sessions.ts +772 -0
  26. package/skills/est/SKILL.md +221 -0
  27. package/skills/est/reference.md +381 -0
  28. package/skills/est/scripts/est.ts +4854 -0
  29. package/skills/github/SKILL.md +206 -0
  30. package/skills/github/locales/en.json +449 -0
  31. package/skills/github/locales/ru.json +449 -0
  32. package/skills/github/reference.md +1073 -0
  33. package/skills/github/scripts/github.ts +4390 -0
  34. package/skills/github/scripts/locale.ts +495 -0
  35. package/skills/github/scripts/package.json +3 -0
  36. package/skills/slot/SKILL.md +145 -0
  37. package/skills/slot/scripts/package.json +3 -0
  38. package/skills/slot/scripts/slot.ts +279 -0
  39. package/skills/spec/SKILL.md +247 -0
  40. package/skills/spec/canon.md +219 -0
  41. package/skills/spec/checks.md +709 -0
  42. package/skills/spec/ci.md +378 -0
  43. package/skills/spec/migration.md +125 -0
  44. package/skills/spec/reference.md +889 -0
  45. package/skills/spec/scripts/architecture.ts +839 -0
  46. package/skills/spec/scripts/c4.ts +184 -0
  47. package/skills/spec/scripts/eslint-config.ts +82 -0
  48. package/skills/spec/scripts/harness.ts +1483 -0
  49. package/skills/spec/scripts/knip-hints.cjs +34 -0
  50. package/skills/spec/scripts/knip-hints.d.cts +17 -0
  51. package/skills/spec/scripts/package-run.ts +22 -0
  52. package/skills/spec/scripts/package.json +3 -0
  53. package/skills/spec/scripts/spec-break.ts +232 -0
  54. package/skills/spec/scripts/spec-claims.ts +198 -0
  55. package/skills/spec/scripts/spec-diff.ts +1111 -0
  56. package/skills/spec/scripts/spec-doc.ts +744 -0
  57. package/skills/spec/scripts/spec-exceptions.ts +233 -0
  58. package/skills/spec/scripts/spec-publish.ts +214 -0
  59. package/skills/spec/scripts/spec-run.ts +235 -0
  60. package/skills/spec/scripts/spec-shard.ts +267 -0
  61. package/skills/spec/scripts/speclib.ts +826 -0
  62. package/skills/spec/scripts/vendor/babel-parser.LICENSE +19 -0
  63. package/skills/spec/scripts/vendor/babel-parser.cjs +14615 -0
  64. package/skills/spec/scripts/vendor/babel-parser.d.cts +3 -0
  65. package/skills/task-env/SKILL.md +231 -0
  66. package/skills/task-env/proxy/compose.yaml +51 -0
  67. package/skills/task-env/reference.md +254 -0
  68. package/skills/task-env/scripts/task-env.ts +1017 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * locale — the project language and the names in it, one module for the scripts of all skills and the installer: `est`,
3
+ * `spec` and `dashboard` import it from `github` — there's one dictionary, in this skill's `locales/`, and `github`
4
+ * doesn't depend on them; the installer in `bin/` imports it from its own package.
5
+ *
6
+ * The language is `language` in `.agents/elves.json` of the project root (the nearest directory up with `.git`,
7
+ * otherwise the directory itself), until `update` moves it — in the former manifest (`FORMER_MANIFEST`): a manifest
8
+ * without the field (an install of a previous version) — `ru`, no manifest — `en`. Names are `locales/<language>.json`,
9
+ * the same keys for each language; no dictionary for the language — `en`. Scripts write in the project language and parse the names of any dictionary language (`everyNames`):
10
+ * a project that changed its language and tasks of other repositories are read without knowing their language.
11
+ *
12
+ * The dictionaries are static JSON imports, not a read of the `locales/` directory: the module is imported by the `spec`
13
+ * harness, which a project's test loads under the project's runner — Playwright and Jest load it as CommonJS, where
14
+ * `import.meta` and top-level `await` are syntax errors, and ESM under Node has no `__dirname`
15
+ * (tests/standards/harness-load); a static import is resolved from the module itself by any loader. For the same
16
+ * runners — only erasable TypeScript and `node:` APIs, and `scripts/package.json` with `"type": "module"`: under Node a
17
+ * project with `"type": "commonjs"` would otherwise load the copy as CommonJS.
18
+ *
19
+ * The `LOCALES` map is typed by `Names`: tsc checks every dictionary against it; a new `locales/<language>.json` is a
20
+ * language only once it's in the map (the flow-english standard holds that every file is).
21
+ */
22
+
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import path from "node:path";
25
+
26
+ import en from "../locales/en.json" with { type: "json" };
27
+ import ru from "../locales/ru.json" with { type: "json" };
28
+
29
+ /** Word forms for a number by CLDR; a language without a form has the same as other. */
30
+ export interface Plural {
31
+ one: string;
32
+ few: string;
33
+ many: string;
34
+ other: string;
35
+ }
36
+
37
+ /**
38
+ * Names the scripts write to GitHub, to the project's documents and tests, and parse from there: the shape of
39
+ * `locales/en.json`, each language has the same keys (the flow-english standard).
40
+ */
41
+ export interface Names {
42
+ issueTypes: { task: string; bug: string; epic: string };
43
+ sections: { questions: string; scenarios: string };
44
+ /**
45
+ * `questions` — a task with open questions in its description (`questionsDescription` — the label's description when
46
+ * `task new` creates it); `externalBlocker` — a task waiting for something outside the repository that isn't an issue:
47
+ * `task queue` skips it.
48
+ */
49
+ labels: { questions: string; questionsDescription: string; epicDescription: string; externalBlocker: string };
50
+ status: { backlog: string; inProgress: string; done: string };
51
+ fields: { estimate: string; actual: string; tokens: string; cost: string };
52
+ views: { board: string; table: string; roadmap: string };
53
+ labelMarkers: { decision: string; slice: string; architecturePath: string };
54
+ /** `externalBlocker` — the start of the comment with the reason of the label of the same name. */
55
+ comments: { state: string; actual: string; closedByPr: string; externalBlocker: string };
56
+ subagentPrompt: { task: string; epic: string };
57
+ /** A bug about a red default branch: the title "<base> is red: <checks>". */
58
+ mainRed: { title: string; check: string; checks: string };
59
+ /**
60
+ * The epic refresh (`github task actualize`): the subagent's prompt (`lead`, `fetch`), its assignment (`--assignment`)
61
+ * and the line of the script's own fixes in a comment on the epic (`mechanics` of `unblocked`, `unlabeled`,
62
+ * `milestone`). `linkWords` — phrases before `#N` that copy a dependency into text, separated by `|`; the phrases of
63
+ * all languages are parsed. `report` — the line the subagent's comment on the epic starts with, its result: `task
64
+ * close` refuses until it finds it, in any language (#571).
65
+ */
66
+ actualize: {
67
+ lead: string;
68
+ fetch: string;
69
+ shipped: string;
70
+ prs: string;
71
+ noPr: string;
72
+ files: string;
73
+ epic: string;
74
+ openTasks: string;
75
+ openTask: string;
76
+ noBody: string;
77
+ copies: string;
78
+ copy: string;
79
+ answers: string;
80
+ commentAnswer: string;
81
+ checks: string;
82
+ scope: string;
83
+ comment: string;
84
+ answer: string;
85
+ linkWords: string;
86
+ report: string;
87
+ mechanics: string;
88
+ unblocked: string;
89
+ unlabeled: string;
90
+ milestone: string;
91
+ };
92
+ /** The dispatcher's executor (`task queue`): its prompt and its whole assignment (`--assignment`), lines in order. */
93
+ queue: {
94
+ prompt: string;
95
+ epic: string;
96
+ lead: string;
97
+ resume: string;
98
+ subagent: string;
99
+ session: string;
100
+ wait: string;
101
+ git: string;
102
+ subagents: string;
103
+ /** The executor's answer waits for its own subagents (#565). */
104
+ live: string;
105
+ found: string;
106
+ question: string;
107
+ answer: string;
108
+ /** The assignment refused: the task's previous executor or its subagent is alive (#565). */
109
+ alive: string;
110
+ /** A cloud executor's brief (`--assignment N --cloud`, #609): whole, `#` only at its task — the epic without it. */
111
+ cloudEpic: string;
112
+ cloud: string;
113
+ cloudDoc: string;
114
+ cloudClose: string;
115
+ cloudDispatcher: string;
116
+ cloudQuestion: string;
117
+ };
118
+ /** The est skill's "Estimate" and "Actual" comments and the lines a human appends to them. */
119
+ est: {
120
+ answer: string;
121
+ manual: string;
122
+ reason: string;
123
+ hours: string;
124
+ /**
125
+ * Words with which a PR or commit text closes a task (`Closes #N`), space-separated; the words of all languages
126
+ * are parsed.
127
+ */
128
+ closingWords: string;
129
+ /** Service words of a title for picking analogs, space-separated; the words of all languages are dropped. */
130
+ stopWords: string;
131
+ fact: {
132
+ unavailable: string;
133
+ body: string;
134
+ estimate: string;
135
+ noEstimate: string;
136
+ sessions: Plural;
137
+ prompts: Plural;
138
+ subagents: Plural;
139
+ commits: Plural;
140
+ prs: string;
141
+ noPr: string;
142
+ diff: string;
143
+ diffLines: string;
144
+ diffNa: string;
145
+ shared: string;
146
+ commitUnit: string;
147
+ taken: string;
148
+ overlap: string;
149
+ cloudMissing: string;
150
+ tokens: string;
151
+ tokensSplit: string;
152
+ cost: string;
153
+ unpriced: string;
154
+ steps: string;
155
+ /** The actual's split by waits, minutes (est, #518): {ci}, {merged}, {checks}, {slot} — `waitsSlot` or empty, {rest}. */
156
+ waits: string;
157
+ waitsSlot: string;
158
+ and: string;
159
+ cloudAgent: string;
160
+ };
161
+ cloud: { actual: string; body: string; tokens: string; cost: string; fields: string; unavailable: string; session: string };
162
+ estimate: {
163
+ headline: string;
164
+ tokens: string;
165
+ partial: string;
166
+ body: string;
167
+ auto: string;
168
+ forecast: string;
169
+ expertOver: string;
170
+ expert: string;
171
+ spread: string;
172
+ correction: string;
173
+ tokCorrection: string;
174
+ k: string;
175
+ noK: string;
176
+ levelRepo: string;
177
+ levelAll: string;
178
+ none: string;
179
+ otherProject: string;
180
+ analog: string;
181
+ unknown: string;
182
+ partialCoverage: string;
183
+ notInProject: string;
184
+ labels: Plural;
185
+ type: string;
186
+ words: Plural;
187
+ size: string;
188
+ };
189
+ };
190
+ /** The description of a flow copy commit that `update` suggests: "Elves flow <tag>"; nothing parses it back. */
191
+ updateCommit: string;
192
+ /**
193
+ * The title of the project's flow update task that `check` suggests: "Update the Elves flow" — constant, so
194
+ * `task new` refuses a second open one.
195
+ */
196
+ updateTask: string;
197
+ /**
198
+ * Names before the flow's rename to Elves (#544) that scripts still recognise: `updateTask` — `task new` of the update
199
+ * task refuses while an open one has the former title, so a project doesn't get a second one.
200
+ */
201
+ formerly: { updateTask: string };
202
+ standardIssue: string;
203
+ autoAnswer: string;
204
+ lastLine: { done: string; questions: string; remaining: string };
205
+ prSpecSection: string;
206
+ spec: SpecNames;
207
+ }
208
+
209
+ /** Names of the spec skill: the `spec-doc` documentation, the `spec-diff` PR section, `c4` diagrams, harness tests. */
210
+ interface SpecNames {
211
+ /** "12 tests" — the spec-doc and spec-diff counters. */
212
+ tests: Plural;
213
+ /** Disabling the linter: without rules — all of them, without a description — no reason. */
214
+ lint: { allRules: string; noReason: string };
215
+ /** Names of the tests the harness registers (`harness.ts`): these are the project's `it`. */
216
+ harness: {
217
+ registryNotEmpty: string;
218
+ registryFinds: string;
219
+ violator: string;
220
+ oneRecord: string;
221
+ markOneElement: string;
222
+ elementFiles: string;
223
+ outside: string;
224
+ exception: string;
225
+ exceptionFiles: string;
226
+ exceptionJson: string;
227
+ exceptionRules: string;
228
+ markRules: string;
229
+ badExample: string;
230
+ bad: string;
231
+ good: string;
232
+ outsideExample: string;
233
+ codeFiles: string;
234
+ lintExceptions: string;
235
+ knip: {
236
+ files: string;
237
+ exports: string;
238
+ types: string;
239
+ dependencies: string;
240
+ unlisted: string;
241
+ unresolved: string;
242
+ enumMembers: string;
243
+ nsMembers: string;
244
+ duplicates: string;
245
+ binaries: string;
246
+ catalog: string;
247
+ catalogReferences: string;
248
+ known: string;
249
+ hints: string;
250
+ };
251
+ ignoreNoReason: string;
252
+ env: { inCode: string; declaredName: string; undeclared: string; declared: string; readName: string; unread: string };
253
+ };
254
+ /** Registries, test names and offenders of the architecture model check (`architecture.ts`). */
255
+ architecture: {
256
+ dirsRegistry: string;
257
+ dirName: string;
258
+ dirViolator: string;
259
+ workspaceRegistry: string;
260
+ workspaceName: string;
261
+ workspaceViolator: string;
262
+ importsRegistry: string;
263
+ importsName: string;
264
+ importsViolator: string;
265
+ allowedRegistry: string;
266
+ allowedName: string;
267
+ allowedViolator: string;
268
+ containersRegistry: string;
269
+ libraryName: string;
270
+ moduleName: string;
271
+ containersViolator: string;
272
+ edgesRegistry: string;
273
+ edgeName: string;
274
+ edgeViolator: string;
275
+ unitsRegistry: string;
276
+ unitName: string;
277
+ unitViolator: string;
278
+ clientsRegistry: string;
279
+ clientName: string;
280
+ clientViolator: string;
281
+ hostsRegistry: string;
282
+ hostName: string;
283
+ hostPerimeter: string;
284
+ hostViolator: string;
285
+ inAdapter: string;
286
+ inAdapters: string;
287
+ packagesRegistry: string;
288
+ packageName: string;
289
+ packagePerimeter: string;
290
+ packageViolator: string;
291
+ withAdapter: string;
292
+ withAdapters: string;
293
+ envRegistry: string;
294
+ envNoun: string;
295
+ headersRegistry: string;
296
+ headerNoun: string;
297
+ readName: string;
298
+ readNameMany: string;
299
+ readPerimeter: string;
300
+ readViolator: string;
301
+ };
302
+ /** The `docs/spec` documentation: the header mark, headings, sections of the three roots, test counters and marks. */
303
+ doc: {
304
+ mark: string;
305
+ title: string;
306
+ intro: string;
307
+ capabilities: string;
308
+ architecture: string;
309
+ standards: string;
310
+ skippedCount: Plural;
311
+ failingCount: Plural;
312
+ variants: Plural;
313
+ skipped: string;
314
+ failing: string;
315
+ todo: string;
316
+ outside: string;
317
+ outsideIntro: string;
318
+ none: string;
319
+ rules: string;
320
+ };
321
+ /** The architecture page and C4 diagrams from the model. */
322
+ c4: {
323
+ title: string;
324
+ intro: string;
325
+ system: string;
326
+ context: string;
327
+ containers: string;
328
+ components: string;
329
+ perimeter: string;
330
+ viaModule: string;
331
+ viaModules: string;
332
+ uses: string;
333
+ dependsOn: string;
334
+ calls: string;
335
+ library: string;
336
+ table: string;
337
+ summary: string;
338
+ moduleCount: Plural;
339
+ containerCount: Plural;
340
+ externalCount: Plural;
341
+ };
342
+ /**
343
+ * The `spec-diff` PR section: test lists, moving into the tree, the summary, decisions outside names, the scenarios
344
+ * check.
345
+ */
346
+ diff: {
347
+ base: string;
348
+ unchanged: string;
349
+ removed: string;
350
+ changed: string;
351
+ added: string;
352
+ empty: string;
353
+ outsideMark: string;
354
+ moved: string;
355
+ movedFolder: string;
356
+ files: Plural;
357
+ summary: string;
358
+ folder: string;
359
+ harnessColumn: string;
360
+ total: string;
361
+ outsideFiles: string;
362
+ notStatements: string;
363
+ scenarios: string;
364
+ noScenarios: string;
365
+ becameTests: string;
366
+ noTest: string;
367
+ extraMark: string;
368
+ extraTitle: string;
369
+ extraListed: string;
370
+ extraFull: string;
371
+ harnessCheck: string;
372
+ fromReport: string;
373
+ harnessNote: string;
374
+ harnessGap: string;
375
+ noSpecBase: string;
376
+ partialReport: string;
377
+ modelModule: string;
378
+ modelDependency: string;
379
+ modelPackage: string;
380
+ registry: string;
381
+ registryNamed: string;
382
+ rule: string;
383
+ ruleNamed: string;
384
+ deadCode: string;
385
+ deadCodeNamed: string;
386
+ architecture: string;
387
+ modelTitle: string;
388
+ exceptionsTitle: string;
389
+ checksTitle: string;
390
+ removedKind: string;
391
+ addedKind: string;
392
+ summarized: string;
393
+ };
394
+ /**
395
+ * Tests of the `spec-claims` entry points check: describe, registry and names; "not empty" and exception are
396
+ * harness names.
397
+ */
398
+ claims: {
399
+ describe: string;
400
+ registry: string;
401
+ unique: string;
402
+ called: string;
403
+ known: string;
404
+ };
405
+ }
406
+
407
+ /** A BCP 47 language code: the language and optional subtags — `ru`, `en`, `pt-BR`, `zh-Hant`. */
408
+ const LANGUAGE_CODE = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
409
+ const DEFAULT_LANGUAGE = "en";
410
+ /**
411
+ * The language of an install of a previous version — a manifest without the `language` field: before the field the flow
412
+ * worked only in Russian.
413
+ */
414
+ const LEGACY_LANGUAGE = "ru";
415
+ /** The install manifest of the project root. */
416
+ const MANIFEST = path.join(".agents", "elves.json");
417
+ /**
418
+ * The manifest of an install made before the flow was renamed to Elves: `update` moves it to `MANIFEST`, until then the
419
+ * project language is read from it.
420
+ */
421
+ const FORMER_MANIFEST = path.join(".agents", "ai-dev.json");
422
+ /** The dictionary: language → names, one entry per `locales/<language>.json`. */
423
+ const LOCALES: Record<string, Names> = { en, ru };
424
+
425
+ /**
426
+ * The project root: the nearest directory up with `.git` (a directory or a worktree file), otherwise the directory
427
+ * itself.
428
+ */
429
+ function projectRoot(dir: string): string {
430
+ const start = path.resolve(dir);
431
+ for (let d = start; ; d = path.dirname(d)) {
432
+ if (existsSync(path.join(d, ".git"))) return d;
433
+ if (path.dirname(d) === d) return start;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * The project language of the `dir` directory (by default — the current one): the manifest's `language` (`MANIFEST`,
439
+ * otherwise `FORMER_MANIFEST`), no field — ru, no manifest — en.
440
+ */
441
+ function projectLanguage(dir: string = process.cwd()): string {
442
+ const root = projectRoot(dir);
443
+ const file = [MANIFEST, FORMER_MANIFEST].map((m) => path.join(root, m)).find((f) => existsSync(f));
444
+ if (!file) return DEFAULT_LANGUAGE;
445
+ let manifest: { language?: unknown } = {};
446
+ try {
447
+ manifest = JSON.parse(readFileSync(file, "utf8")) ?? {};
448
+ } catch {
449
+ // a broken manifest — like a manifest without the field
450
+ }
451
+ return typeof manifest.language === "string" && LANGUAGE_CODE.test(manifest.language) ? manifest.language : LEGACY_LANGUAGE;
452
+ }
453
+
454
+ /** The names of a language: `locales/<language>.json`, no dictionary for the language — `en`. */
455
+ function names(language: string): Names {
456
+ return Object.hasOwn(LOCALES, language) ? LOCALES[language]! : en;
457
+ }
458
+
459
+ /** The dictionary's languages, `en` first. */
460
+ function languages(): string[] {
461
+ return [DEFAULT_LANGUAGE, ...Object.keys(LOCALES).filter((l) => l !== DEFAULT_LANGUAGE).sort()];
462
+ }
463
+
464
+ /** The names of all dictionary languages — for parsing. */
465
+ function everyNames(): Names[] {
466
+ return languages().map(names);
467
+ }
468
+
469
+ /** One name in all dictionary languages without repeats: `every((n) => n.sections.questions)`. */
470
+ function every(pick: (n: Names) => string): string[] {
471
+ return [...new Set(everyNames().map(pick))];
472
+ }
473
+
474
+ /** Substitutes `{var}` in a name template; an unknown variable stays as is. */
475
+ function fill(template: string, vars: Record<string, string | number>): string {
476
+ return template.replace(/\{(\w+)\}/g, (m, k: string) => (Object.hasOwn(vars, k) ? String(vars[k]) : m));
477
+ }
478
+
479
+ /** The word form for a number by the language's rules (CLDR: one, few, many, other); no such form — other. */
480
+ function plural(language: string, forms: Plural, n: number): string {
481
+ const form = new Intl.PluralRules(language).select(n);
482
+ return (forms as Partial<Record<Intl.LDMLPluralRule, string>>)[form] ?? forms.other;
483
+ }
484
+
485
+ /** "12 tests": the number and the word form. */
486
+ function count(language: string, forms: Plural, n: number): string {
487
+ return `${n} ${plural(language, forms, n)}`;
488
+ }
489
+
490
+ /** A regex from a name: special characters escaped. */
491
+ function escapeRe(text: string): string {
492
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
493
+ }
494
+
495
+ export default { LANGUAGE_CODE, DEFAULT_LANGUAGE, LEGACY_LANGUAGE, projectRoot, projectLanguage, names, languages, everyNames, every, fill, plural, count, escapeRe };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: slot
3
+ description: "A queue for heavy runs on the machine — project checks (`lint`, `typecheck`, `test`, `test:*`) go through `slot`, and installing the flow wraps the `package.json` scripts in it; two runs at a time, together half of the cores, the rest wait in arrival order and print whom they wait for; the command gets `ELVES_SLOT_CPUS` (a quarter of the cores) for the runner's workers. When — you run a full project check, tests, typecheck, lint, e2e; a run is silent with the line 'slot: waiting for the machine queue'; parallel sessions choke the machine and runs hit timeouts; you set up runner workers (Vitest, Playwright, Jest) locally; a heavy command bypasses the project's scripts — even if the word 'queue' was never said."
4
+ allowed-tools: Bash(bun *skills/slot/scripts/slot.ts *)
5
+ ---
6
+
7
+ # slot — the machine's heavy runs in a queue
8
+
9
+ Parallel sessions run the full check all at once, and each runner is sized for
10
+ the whole machine: a run that takes seconds alone drags on for minutes next to
11
+ a dozen neighbors and gets cut off by the tool timeout, and a retry adds load.
12
+ The `scripts/slot.ts` script next to this file lets two heavy runs go at a
13
+ time, the rest wait in arrival order; together the slots take half of the
14
+ cores, the other half is for the human, the app and the agents themselves. Why
15
+ it is so and what was rejected — `tests/capabilities/slot/slot.md` in Elves.
16
+
17
+ ```bash
18
+ bun --no-env-file <skill dir>/scripts/slot.ts '<command>' [arguments…]
19
+ ```
20
+
21
+ The command runs through `sh`, the arguments are appended to its end, as with
22
+ `npm run`; the exit code is the command's. No queue in CI (`CI`) and inside a
23
+ slot (`ELVES_SLOT`): a script that calls another queued script would
24
+ otherwise wait for itself. A `package.json` script doesn't pass its command to
25
+ `slot.ts` — it sources the project's shim `.agents/slot.sh` (below).
26
+
27
+ ## How it is enabled
28
+
29
+ Installing the flow (`install`, `update`) puts the shim `.agents/slot.sh` into
30
+ the project and wraps its `package.json` scripts `lint`, `typecheck`, `test`
31
+ and `test:*` in it: `"test": ". .agents/slot.sh && vitest run"`. The script's
32
+ shell sources the shim: `elves slot --for $$` — the package
33
+ `@miroshnik/elves` of the project's `devDependencies`, from `node_modules/.bin`
34
+ that `npm run` and `bun run` put on the script's `PATH` — takes a slot for that
35
+ shell, held until it exits, and the shell exports `ELVES_SLOT` and
36
+ `ELVES_SLOT_CPUS` to the command. Without `elves` on `PATH` (CI before the
37
+ dependencies are installed, a checkout without the package) the command runs
38
+ as is; `elves` failed — the script fails with its code. The command stays a
39
+ plain command of the script: knip sees its binary as a used dependency, and
40
+ `bun test` gives knip the test files and the `preload` of `bunfig.toml` as
41
+ entries; passed to `slot` as a string, it was hidden, and the project's dead
42
+ code went red from the flow install. Yarn 2+ runs scripts in its own shell,
43
+ without `.`: there the command stays a string of the package's `slot`, hidden
44
+ from knip, and the package goes through bunx at the project's version —
45
+ Yarn 2+ runs a `.ts` bin by Node with any `nodeLinker`, and PnP has no
46
+ `node_modules`:
47
+ `"test": "bunx @miroshnik/elves@\"$(jq -r '.devDependencies[\"@miroshnik/elves\"]' package.json)\" slot 'vitest run'"`
48
+ (`jq` on the machine). Everyone calls the
49
+ scripts — the agent, a human in a terminal, `github pr premerge` — and
50
+ everyone joins one queue. `build` isn't wrapped — the host and Docker run it,
51
+ where the shim or the package may be missing; watch isn't either: it's long and
52
+ would hold a slot for a long time. Removed the wrapper by hand, or it's a
53
+ previous install's (a copy's `.agents/skills/slot/scripts/slot.sh`, the command
54
+ a string of `slot.ts`) — `check` names the script, `update` puts the wrapper
55
+ back.
56
+
57
+ ## The project's env files are off
58
+
59
+ Bun loads the env files of the working directory — `.env`,
60
+ `.env.<NODE_ENV>`, `.env.local` — into its process by itself, and `slot.ts`
61
+ passes its environment to the command. `.env.local` often holds production
62
+ keys (`vercel env pull`): without `--no-env-file` they reach a runner that
63
+ never saw them before the wrapper — tests of "the key isn't set" go red
64
+ locally and green in CI, a test calling an external service goes there with a
65
+ production key. With the flag the command gets exactly the environment it
66
+ would get without the wrapper: `npm run` and `bun run` don't load env files
67
+ for a `package.json` script, and a runner that loads them itself (`bun test`
68
+ — `.env.test`) still does. A script's command doesn't go through `slot.ts`
69
+ at all — it gets the script's environment and the two queue variables:
70
+ `elves slot --for` only prints them. The package's `elves slot '<command>'`
71
+ — a direct call from `node_modules/.bin`, the Yarn 2+ wrapper through bunx —
72
+ goes through the bin's shebang: `sh` starts Bun with `--no-env-file`. Not `env = false` in the
73
+ project's `bunfig.toml`:
74
+ it would turn env files off for every `bun` of a Bun project, its own
75
+ `bun test` and `bun run dev` included. The same flag is on every call of a
76
+ skill script (`tests/standards/no-env-file` in Elves): a child `bun` inherits
77
+ what its parent loaded.
78
+
79
+ ## What the agent does
80
+
81
+ | Moment | Action |
82
+ |---|---|
83
+ | Check before a commit, a full run, e2e | via the project's scripts (`npm run test -- --changed=origin/main`, `bun run typecheck`): they are already queued; which tests before a commit — `docs/testing.md`. As a background command with a generous timeout (Claude Code — `run_in_background`, up to 2 h): waiting in the queue doesn't eat the tool timeout, completion wakes the session |
84
+ | `slot: waiting for the machine queue — running …` | not a hang: who holds the slots (directory, command) and how long they have been running. Don't kill someone else's run or delete its ticket |
85
+ | `slot: waited …` | the time in the queue; compare run speed without it — by CI or by `user` from `time` |
86
+ | A targeted run (`vitest run <file>`) | bypasses the queue — it's cheap |
87
+ | A heavy command bypassing the project's scripts | `bun --no-env-file <skill dir>/scripts/slot.ts '<command>'` |
88
+ | Runner workers locally | from `ELVES_SLOT_CPUS` — below |
89
+
90
+ ## Runner workers
91
+
92
+ In a slot the command gets `ELVES_SLOT_CPUS` — a quarter of the cores. A
93
+ runner that counts workers from all cores takes the whole machine even in a
94
+ slot; its config reads the variable, and in CI and outside the queue it keeps
95
+ its own default:
96
+
97
+ ```ts
98
+ // vitest.config.ts
99
+ maxWorkers: Number(process.env.ELVES_SLOT_CPUS) || undefined,
100
+ // playwright.config.ts
101
+ workers: Number(process.env.ELVES_SLOT_CPUS) || (process.env.CI ? 2 : undefined),
102
+ // jest — via a script flag: jest --maxWorkers=${ELVES_SLOT_CPUS:-50%}
103
+ ```
104
+
105
+ **A monorepo on turbo.** turbo 2 in strict env mode (the default) passes to
106
+ tasks only the variables named in `turbo.json`: without pass-through the
107
+ runner config doesn't see `ELVES_SLOT_CPUS` and takes the whole machine, and
108
+ a nested script in `slot` without `ELVES_SLOT` waits for its own slot. In the
109
+ root of `turbo.json`:
110
+
111
+ ```json
112
+ "globalPassThroughEnv": ["ELVES_SLOT", "ELVES_SLOT_CPUS"]
113
+ ```
114
+
115
+ Not `env` and not `globalEnv`: they go into the cache hash, while
116
+ `ELVES_SLOT` is different for every run and the share of cores depends on the
117
+ machine. A script in `slot` calls turbo without pass-through — `install` and
118
+ `check` warn with a ready-made line.
119
+
120
+ **The flow's former name.** Before the rename the variables were
121
+ `AI_DEV_SLOT` and `AI_DEV_SLOT_CPUS`: `slot` exports them too, with the same
122
+ values, and either name marks a nested call — a project's runner configs and
123
+ `turbo.json` keep working until they are renamed, and so does the slot of a
124
+ project copy `update` hasn't brought yet. `check` names `turbo.json` that
125
+ passes only the former names, with the line to put.
126
+
127
+ ## Queue
128
+
129
+ A run's ticket is a file `<time>-<pid>` in `slots/` of the personal
130
+ configuration directory (`$ELVES_CONFIG_DIR`, otherwise `~/.config/elves`);
131
+ it holds the directory, the command and the start time. Who is in the queue —
132
+ `ls` of that directory. A process died, even one killed without cleanup — its
133
+ ticket doesn't count and is removed by the first one to see it: there is
134
+ nothing to clean up by hand.
135
+
136
+ An agent's sandbox doesn't let the configuration be written: there the ticket
137
+ goes to `elves/slots` of the temp directory (`$TMPDIR`; Claude Code's sandbox
138
+ gives all sessions of the user one), and the queue is read from all places at
139
+ once, Claude Code's temp directory (`$CLAUDE_TMPDIR`) included — runs inside
140
+ and outside the sandbox wait for each other; a human's terminal doesn't see
141
+ the sandbox's runs; the tickets of an earlier release's slot in
142
+ `ai-dev/slots` of the temp directory count too. The checks run in the sandbox,
143
+ without disabling it. No
144
+ place takes the ticket — one line `slot: running without the queue — …` and
145
+ the command runs anyway.
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }