@miroshnik/elves 0.0.0 → 2026.1018.1
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/AGENTS.md +310 -0
- package/README.md +344 -0
- package/bin/ai-dev.ts +7 -0
- package/bin/elves.ts +2847 -0
- package/bin/shims/timeout +113 -0
- package/claude/CLAUDE.md +71 -0
- package/dist/github/scripts/github.js +3428 -0
- package/dist/spec/scripts/architecture.js +628 -0
- package/dist/spec/scripts/eslint-config.js +54 -0
- package/dist/spec/scripts/harness-qzb932c8.js +959 -0
- package/dist/spec/scripts/harness-v9s1v9w3.js +16045 -0
- package/dist/spec/scripts/harness.js +53 -0
- package/dist/spec/scripts/knip-hints.cjs +34 -0
- package/docs/ci-concurrency.md +82 -0
- package/docs/cloud-sessions.md +204 -0
- package/docs/judgment.md +81 -0
- package/docs/parallel-checkouts.md +220 -0
- package/docs/pr-checks.md +117 -0
- package/docs/testing.md +304 -0
- package/package.json +70 -1
- package/skills/ci-wait/SKILL.md +180 -0
- package/skills/ci-wait/scripts/wait-ci.sh +377 -0
- package/skills/dashboard/SKILL.md +212 -0
- package/skills/dashboard/scripts/dashboard.ts +912 -0
- package/skills/dashboard/scripts/sessions.ts +772 -0
- package/skills/est/SKILL.md +222 -0
- package/skills/est/reference.md +381 -0
- package/skills/est/scripts/est.ts +4867 -0
- package/skills/github/SKILL.md +206 -0
- package/skills/github/locales/en.json +449 -0
- package/skills/github/locales/ru.json +449 -0
- package/skills/github/reference.md +1073 -0
- package/skills/github/scripts/github.ts +4390 -0
- package/skills/github/scripts/locale.ts +495 -0
- package/skills/github/scripts/package.json +3 -0
- package/skills/slot/SKILL.md +145 -0
- package/skills/slot/scripts/package.json +3 -0
- package/skills/slot/scripts/slot.ts +279 -0
- package/skills/spec/SKILL.md +247 -0
- package/skills/spec/canon.md +219 -0
- package/skills/spec/checks.md +709 -0
- package/skills/spec/ci.md +378 -0
- package/skills/spec/migration.md +125 -0
- package/skills/spec/reference.md +889 -0
- package/skills/spec/scripts/architecture.ts +839 -0
- package/skills/spec/scripts/c4.ts +184 -0
- package/skills/spec/scripts/eslint-config.ts +82 -0
- package/skills/spec/scripts/harness.ts +1483 -0
- package/skills/spec/scripts/knip-hints.cjs +34 -0
- package/skills/spec/scripts/knip-hints.d.cts +17 -0
- package/skills/spec/scripts/package-run.ts +22 -0
- package/skills/spec/scripts/package.json +3 -0
- package/skills/spec/scripts/spec-break.ts +232 -0
- package/skills/spec/scripts/spec-claims.ts +198 -0
- package/skills/spec/scripts/spec-diff.ts +1111 -0
- package/skills/spec/scripts/spec-doc.ts +744 -0
- package/skills/spec/scripts/spec-exceptions.ts +233 -0
- package/skills/spec/scripts/spec-publish.ts +214 -0
- package/skills/spec/scripts/spec-run.ts +235 -0
- package/skills/spec/scripts/spec-shard.ts +267 -0
- package/skills/spec/scripts/speclib.ts +826 -0
- package/skills/spec/scripts/vendor/babel-parser.LICENSE +19 -0
- package/skills/spec/scripts/vendor/babel-parser.cjs +14615 -0
- package/skills/spec/scripts/vendor/babel-parser.d.cts +3 -0
- package/skills/task-env/SKILL.md +229 -0
- package/skills/task-env/proxy/compose.yaml +48 -0
- package/skills/task-env/reference.md +246 -0
- package/skills/task-env/scripts/task-env.ts +981 -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,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.
|