@miroshnik/elves 2026.1019.2 → 2026.1022.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.
- package/AGENTS.md +19 -17
- package/README.md +4 -4
- package/bin/ai-dev.ts +5 -2
- package/bin/elves.ts +162 -46
- package/dist/github/scripts/github.js +1033 -272
- package/dist/spec/scripts/architecture.js +4 -4
- package/dist/spec/scripts/{harness-bpyj4s91.js → harness-3yk31f52.js} +6 -6
- package/dist/spec/scripts/{harness-tdg4dnq3.js → harness-xxye8g22.js} +62 -18
- package/dist/spec/scripts/harness.js +1 -2
- package/docs/ci-concurrency.md +6 -1
- package/docs/cloud-sessions.md +3 -1
- package/docs/pr-checks.md +16 -0
- package/docs/testing.md +5 -1
- package/package.json +2 -2
- package/skills/ci-wait/SKILL.md +6 -5
- package/skills/ci-wait/scripts/wait-ci.sh +33 -21
- package/skills/github/SKILL.md +13 -11
- package/skills/github/locales/en.json +21 -4
- package/skills/github/locales/ru.json +21 -4
- package/skills/github/reference.md +214 -10
- package/skills/github/scripts/github.ts +1439 -288
- package/skills/github/scripts/locale.ts +88 -22
- package/skills/spec/SKILL.md +3 -1
- package/skills/spec/ci.md +57 -0
- package/skills/spec/reference.md +5 -1
- package/skills/spec/scripts/harness.ts +13 -11
- package/skills/spec/scripts/speclib.ts +52 -34
- package/skills/task-env/SKILL.md +2 -2
- package/skills/task-env/scripts/task-env.ts +67 -20
package/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ repositories, no colleagues or clients — in files, git history or issues;
|
|
|
15
15
|
examples are made up. **A skill is `SKILL.md` + `scripts/`** in TypeScript
|
|
16
16
|
under Bun (Agent Skills; README, "Repository layout"); repository specifics
|
|
17
17
|
— local configuration. **Budgets** (standard `canon-size`): this file ≤
|
|
18
|
-
3,000 words,
|
|
18
|
+
3,000 words, `SKILL.md` ≤ 2,500, a description ≤ 1,024 characters.
|
|
19
19
|
Here — what always applies and where to go; reference — a skill's
|
|
20
20
|
`reference.md` or `--help`, `docs/`.
|
|
21
21
|
|
|
@@ -31,7 +31,7 @@ a release is the owner's step (`elves release`), I cut one only when asked.
|
|
|
31
31
|
briefly what changed (it lists the skipped releases) and re-read what was
|
|
32
32
|
updated: it applies, not what loaded at the start.
|
|
33
33
|
- **The project is behind** (or git ignores flow files) — `update` in the
|
|
34
|
-
task's branch: the package version
|
|
34
|
+
task's branch: the package version.
|
|
35
35
|
- **The project keeps a copy of the flow** — the move is a task of its own,
|
|
36
36
|
one per project (`check` prints its `github task new`): I recommend it and
|
|
37
37
|
ask whether to queue it.
|
|
@@ -118,14 +118,15 @@ done. The session can be closed.` · `All done, but there are questions: …` ·
|
|
|
118
118
|
regression and a fix for a green PR are the same task. A new epic — the
|
|
119
119
|
question «run the dispatcher on epic #M?», without In progress.
|
|
120
120
|
- **A dispatcher** — "do all tasks of epic #M", "fix all High bugs", a "yes"
|
|
121
|
-
— any session, its task stays; one started
|
|
122
|
-
not a worktree: executors start from `main`.
|
|
123
|
-
executors' prompts; an executor — a background
|
|
124
|
-
a cloud session: at most 3 subagents and 5 cloud
|
|
125
|
-
owner's number replaces either; failed — a task
|
|
126
|
-
executor session whose task closed — the dispatcher
|
|
127
|
-
project, not one still working in the background or
|
|
128
|
-
answer; outside auto the owner closes it.
|
|
121
|
+
(scope, not merges outside auto) — any session, its task stays; one started
|
|
122
|
+
for it — in the project folder, not a worktree: executors start from `main`.
|
|
123
|
+
`github task queue` prints executors' prompts; an executor — a background
|
|
124
|
+
subagent (own worktree) or a cloud session: at most 3 subagents and 5 cloud
|
|
125
|
+
sessions at once, the owner's number replaces either; failed — a task
|
|
126
|
+
comment, no restart; an executor session whose task closed — the dispatcher
|
|
127
|
+
archives it in an auto project, not one still working in the background or
|
|
128
|
+
awaiting the owner's answer; outside auto the owner closes it. A batch
|
|
129
|
+
project — `github batch release`.
|
|
129
130
|
- **The session is named `#<number> <task title>`** (without a task — by
|
|
130
131
|
meaning, no number) as soon as the task is clear (no tool — ask the user):
|
|
131
132
|
`est` links the session to the task by it.
|
|
@@ -167,8 +168,8 @@ and worktrees") — otherwise there is no actual.
|
|
|
167
168
|
### Epic refresh on task close
|
|
168
169
|
|
|
169
170
|
Once a task is merged, **a subagent with a fresh context** brings the open
|
|
170
|
-
tasks of its epic in line with what shipped
|
|
171
|
-
|
|
171
|
+
tasks of its epic in line with what shipped: `github task actualize <N>`
|
|
172
|
+
does the mechanics and prints its prompt,
|
|
172
173
|
in the same turn as `ci-wait merged <PR>` in the background; after both —
|
|
173
174
|
`github task close`. The last line of the answer ("Language"): merged,
|
|
174
175
|
deployed, branch removed, epic refreshed, no questions — "all done, can be
|
|
@@ -197,11 +198,12 @@ it.
|
|
|
197
198
|
rebase and `--force-with-lease` — only on a textual conflict or my own
|
|
198
199
|
changes. **Before merging — `github pr premerge`:** `main` moved after the
|
|
199
200
|
PR's CI — fast checks on the merge with it; red — I fix the root cause;
|
|
200
|
-
`main` red by itself (
|
|
201
|
+
`main` red by itself (code 3) — no merge and no fix in the PR: one bug
|
|
201
202
|
for it, I wait for it to close.
|
|
202
203
|
- A merge that deploys — I wait for the deploy on the merge SHA (`ci-wait`)
|
|
203
|
-
**before `github task close
|
|
204
|
-
|
|
204
|
+
**before `github task close`**; a batch's — its release's CI, deploy,
|
|
205
|
+
refresh.
|
|
206
|
+
Others' branches, stacks and unmerged work I don't touch.
|
|
205
207
|
- Mechanics — the `github` skill, "Git, PRs and merging — mechanics";
|
|
206
208
|
waiting for checks and deploys — `docs/pr-checks.md`.
|
|
207
209
|
|
|
@@ -283,8 +285,8 @@ skill (`canon.md`, `checks.md`).
|
|
|
283
285
|
skill) at `<service>.<N>.<project>.localhost`, not on a host port; others'
|
|
284
286
|
stacks and busy ports I don't touch — `docs/parallel-checkouts.md`.
|
|
285
287
|
- **A subagent gets a unit of ~50 turns** (one decision's folder, one file,
|
|
286
|
-
one spec), not a 200-turn zone:
|
|
287
|
-
|
|
288
|
+
one spec), not a 200-turn zone: its price is quadratic in turns. The
|
|
289
|
+
assignment has the task
|
|
288
290
|
number after the word for "task" or at the start, the epic after the word
|
|
289
291
|
for "epic" ("Language") — so `est` links its work. **Fewer turns:** a file
|
|
290
292
|
in one full read, independent tool calls in one turn.
|
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ An agent's sandbox (Claude Code's Bash) doesn't let bunx write its cache `~/.bun
|
|
|
178
178
|
|
|
179
179
|
`install` in a project asks in the terminal whether the agent should do tasks entirely by itself — answer the task's questions by recommendation, commit, push, fix CI and merge its own PR on green checks (default — no); without a terminal — `install --auto` or `--no-auto`, without the flag the previous answer stays. `check` names the mode. With auto, `install` prints a hint for Claude Code: the app rejects a merge without human review in this mode too, and the session merges its own PR by itself only with the `Bash(gh pr merge *)` rule in `permissions.allow` of `~/.claude/settings.json` — the owner adds it, `install` doesn't change the machine's permission settings.
|
|
180
180
|
|
|
181
|
-
`install` also asks for the **project language** the agent works in: dialog, tasks, prompts, documents; the flow itself is written in English. It is stored in `.agents/elves.json` as `language` — a BCP 47 code (`ru`, `en`, `pt-BR`), `en` by default; an install of a previous version without the field counts as `ru`. Without a terminal — `install --lang <code>`, without the flag the previous language stays; `update` keeps it, `check` and the hook name it. The flow isn't installed into an Elves clone — `install` writes only these two settings
|
|
181
|
+
`install` also asks for the **project language** the agent works in: dialog, tasks, prompts, documents; the flow itself is written in English. It is stored in `.agents/elves.json` as `language` — a BCP 47 code (`ru`, `en`, `pt-BR`), `en` by default; an install of a previous version without the field counts as `ru`. Without a terminal — `install --lang <code>`, without the flag the previous language stays; `update` keeps it, `check` and the hook name it. The flow isn't installed into an Elves clone — `install` writes only its manifest there: these two settings, `env` and `batch` carried over as in a project.
|
|
182
182
|
|
|
183
183
|
<details>
|
|
184
184
|
<summary><b>What goes where</b></summary>
|
|
@@ -186,7 +186,7 @@ An agent's sandbox (Claude Code's Bash) doesn't let bunx write its cache `~/.bun
|
|
|
186
186
|
**Into a project** — thin: no copy of the flow, its agents get the canon and the skills from the machine. Committed with the project:
|
|
187
187
|
|
|
188
188
|
- `@miroshnik/elves` in `devDependencies` of `package.json` — the exact version of the release, the project's package manager updates its lockfile: the harness and the skills' scripts at the project's version for its tests and CI, colleagues and the cloud session ("The package in a project" below); a project without `package.json` has nothing to pin.
|
|
189
|
-
- `.agents/elves.json` — `auto` — whether the agent runs a task entirely by itself, without a "yes" for commit, push and merge, `language` — the project language,
|
|
189
|
+
- `.agents/elves.json` — `auto` — whether the agent runs a task entirely by itself, without a "yes" for commit, push and merge, `language` — the project language, `env` — the task stack settings of `task-env`, and `batch` — whether a dispatcher merges its tasks into a batch branch instead of `main` (`github task queue`).
|
|
190
190
|
- `.agents/slot.sh` — the machine queue's shim the `package.json` checks source (`slot` below).
|
|
191
191
|
- A block at the top of the project's `AGENTS.md` linking to the canon on the machine, `~/.agents/elves/AGENTS.md` — for the agents that read the project's file.
|
|
192
192
|
- The cloud session's `SessionStart` hook in `.claude/settings.json` (your own settings and hooks stay): in a Claude Code cloud session (`CLAUDE_CODE_REMOTE=true`) it installs a fresh Bun and the flow onto the session's machine and runs `check --hook`, outside the cloud it exits at once — no setup script in the environment is needed (`docs/cloud-sessions.md`).
|
|
@@ -258,9 +258,9 @@ Skills are installed onto the machine with the flow (`install -g`); the agent pi
|
|
|
258
258
|
|
|
259
259
|
### [`github`](skills/github/SKILL.md) — project and tasks by the canon
|
|
260
260
|
|
|
261
|
-
Checks and fixes the GitHub project and the default-branch rule, creates a task with all its fields in one command, shows a task with its epic, blockers, PR checks and open questions in one output and puts an answer at a question, before merging checks the PR's merge with a fresh `main` if it moved after CI, after merging does the epic refresh's mechanics and prints the subagent's prompt, and closes the task in one call — `est` actual, Done, epic and milestone, the task's Compose stacks and its merged branches removed — sets decision labels from the PR diff, names the wait a resumed session restarts for its task, and gives a dispatcher its queue — the ready tasks of an epic or a filter in order, each with the prompt of its executor, a background subagent.
|
|
261
|
+
Checks and fixes the GitHub project and the default-branch rule, creates a task with all its fields in one command, shows a task with its epic, blockers, PR checks and open questions in one output and puts an answer at a question, before merging checks the PR's merge with a fresh `main` if it moved after CI, after merging does the epic refresh's mechanics and prints the subagent's prompt, and closes the task in one call — `est` actual, Done, epic and milestone, the task's Compose stacks and its merged branches removed — sets decision labels from the PR diff, names the wait a resumed session restarts for its task, and gives a dispatcher its queue — the ready tasks of an epic or a filter in order, each with the prompt of its executor, a background subagent; in a batch project the executors merge into the dispatcher's batch branch, and it reaches `main` by one release PR — one full CI run and one deploy.
|
|
262
262
|
|
|
263
|
-
`github project check` · `github project fix` · `github task new | show | answer | status | drop | actualize | close | resume | queue` · `github pr labels | premerge`
|
|
263
|
+
`github project check` · `github project fix` · `github task new | show | answer | status | drop | actualize | close | resume | queue` · `github batch release` · `github pr labels | premerge`
|
|
264
264
|
|
|
265
265
|
### [`est`](skills/est/SKILL.md) — estimates from actuals
|
|
266
266
|
|
package/bin/ai-dev.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* The installer under its name before the rename (#544) — `bin/elves.ts` itself: `update -g` of a release before the
|
|
3
3
|
* rename pulls a `--link` clone and runs `<clone>/bin/ai-dev.ts install -g --link` with the clone's code, and this file
|
|
4
4
|
* keeps that run working — the links, the SessionStart hook and the clone's origin come from the updated clone. Not a
|
|
5
|
-
* command of the package (`bin` in package.json has only `elves`).
|
|
5
|
+
* command of the package (`bin` in package.json has only `elves`). `bin/elves.ts` runs its command line only as the
|
|
6
|
+
* entry point — here it is called.
|
|
6
7
|
*/
|
|
7
|
-
import "./elves.ts";
|
|
8
|
+
import { main } from "./elves.ts";
|
|
9
|
+
|
|
10
|
+
process.exitCode = await main(process.argv.slice(2));
|
package/bin/elves.ts
CHANGED
|
@@ -42,23 +42,25 @@
|
|
|
42
42
|
* in a project — only `auto`: whether the agent runs a task entirely by itself — answers to questions by
|
|
43
43
|
* recommendation, commit, push, fixing CI and merging its own PR on green checks (AGENTS.md, "What I do without asking
|
|
44
44
|
* and what needs a yes"), `language` — the language the agent works in in the project: dialog, tasks, prompts,
|
|
45
|
-
* documents (AGENTS.md, "Language"; the flow is written in English),
|
|
46
|
-
* task-env skill. `install` in a project sets auto and language — by a question in the terminal or by
|
|
45
|
+
* documents (AGENTS.md, "Language"; the flow is written in English), `env` — the task stack settings of the
|
|
46
|
+
* task-env skill, and `batch` — the dispatcher's batches (epic #645). `install` in a project sets auto and language — by a question in the terminal or by
|
|
47
47
|
* the flags `--auto` / `--no-auto` and `--lang <code>`; by default auto is "no", the language is `en`, without a
|
|
48
48
|
* terminal and a flag — the previous value, `update` keeps it. An install of a previous version has no `language`
|
|
49
49
|
* field, and the flow before it worked only in Russian: its language is `ru` (`LEGACY_LANGUAGE` of the github skill's
|
|
50
50
|
* language module), `update` writes it. With auto `install` suggests the Claude Code permission rule for the merge
|
|
51
51
|
* (MERGE_HINT) — it doesn't change the machine's settings. The flow isn't installed into an Elves clone (the canon is
|
|
52
|
-
* at its root): `install` writes a single manifest there, with `auto` and `language
|
|
52
|
+
* at its root): `install` writes a single manifest there, with `auto` and `language`, `env` and `batch` carried over as
|
|
53
|
+
* in a project; `check` there compares nothing but a batch clone's CI (`batchCi`).
|
|
53
54
|
* The project's flow files are committed — the ones the project's git ignores (`.agents/`) the install un-ignores with
|
|
54
55
|
* exceptions in an Elves block at the end of `.gitignore`, `check` reports them as a mismatch (`gitignoreFlow`). A
|
|
55
56
|
* project's compose file `check` holds to the task isolation rules
|
|
56
57
|
* with the task-env skill's command (`composeCheck`): a violation is a mismatch too, `update` doesn't fix it; so is a
|
|
57
58
|
* project with standards or architecture without a `test:spec` script — `check` names the lines (`specScript`); a
|
|
58
|
-
* `test` that runs them a second time next to `test:spec` — an `○` line with `test` without them
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* variables to tasks, it warns
|
|
59
|
+
* `test` that runs them a second time next to `test:spec` — an `○` line with `test` without them; a project with
|
|
60
|
+
* batches whose CI runs on a PR into a batch branch — a mismatch, `❌` lines (`batchCi`). The project's checks — the
|
|
61
|
+
* `package.json` scripts `lint`, `typecheck`, `test`, `test:*` — the install wraps in the machine queue, the slot skill
|
|
62
|
+
* (`packageScripts`); if such a script calls turbo and turbo.json doesn't pass the queue variables to tasks, it warns
|
|
63
|
+
* with a ready-made line (`turboEnv`).
|
|
62
64
|
*
|
|
63
65
|
* A copy on the machine without `--link` and a project's package version follow Elves releases — tags vYYYY.MM.DD — not
|
|
64
66
|
* main: the bunx package from main is only a launcher — it finds the latest release and restarts from it by SHA
|
|
@@ -220,8 +222,8 @@ fixing CI, merge on green checks (auto in .agents/elves.json, default — no); w
|
|
|
220
222
|
merge — Bash(gh pr merge *) in ~/.claude/settings.json: the owner adds it, install doesn't change the machine's
|
|
221
223
|
settings. --lang <code> — the project language the agent works in: dialog, tasks, prompts, documents (language in
|
|
222
224
|
.agents/elves.json, a BCP 47 code: ru, en, pt-BR); in a terminal — a question, en by default, without the flag and a
|
|
223
|
-
terminal — the previous one. In an Elves clone writes only auto and language, and
|
|
224
|
-
from the clone itself.
|
|
225
|
+
terminal — the previous one. In an Elves clone writes only the manifest — auto and language, env and batch carried
|
|
226
|
+
over as in a project — and its cloud hook — with zsh, the flow from the clone itself.
|
|
225
227
|
-g — onto the machine: the canon in ~/.agents/elves with Claude Code's rules ~/.claude/rules/elves*.md and the global
|
|
226
228
|
rule files of Codex, Gemini CLI, Copilot, OpenCode and Amp linking to it (someone else's file stays), the skills
|
|
227
229
|
(~/.agents/skills, ~/.claude/skills), the SessionStart hook and the elves command in the terminal — in ~/.local/bin or
|
|
@@ -239,8 +241,9 @@ Examples:
|
|
|
239
241
|
elves check --hook
|
|
240
242
|
|
|
241
243
|
Whether the install is behind, changes nothing: 0 — up to date, 1 — behind, the project's git ignores flow files, its
|
|
242
|
-
compose file breaks task isolation
|
|
243
|
-
|
|
244
|
+
compose file breaks task isolation, standards and architecture have no test:spec script or the CI of a project (an
|
|
245
|
+
Elves clone too) with "batch": true runs a workflow on a PR into a batch branch — on.pull_request lets a batch branch in,
|
|
246
|
+
its gate is the checks before a commit and pr premerge (ci.md of the spec skill, «Batches»), 2 — the check is unavailable; the machine's copy and a project's package version are compared with the latest release (from the npm
|
|
244
247
|
package — by restarting from the latest release's package), a --link clone — with origin/main. For a project names the
|
|
245
248
|
mode (auto) and the language, holds the compose file to the task isolation rules (❌ per violation, ○ — not checked: no
|
|
246
249
|
docker compose; the task-env skill), names a test that runs standards and architecture again next to test:spec (○, the
|
|
@@ -738,19 +741,16 @@ interface Manifest {
|
|
|
738
741
|
auto?: boolean;
|
|
739
742
|
language?: string;
|
|
740
743
|
env?: unknown;
|
|
744
|
+
batch?: unknown;
|
|
741
745
|
}
|
|
742
746
|
|
|
743
747
|
/**
|
|
744
748
|
* The install's manifest: `.agents/elves.json`, until `update` moves it — the manifest of an earlier release under the
|
|
745
|
-
* flow's former name (FORMER.manifest); missing or broken — empty.
|
|
749
|
+
* flow's former name (FORMER.manifest); missing or broken — empty. Read by `manifestOf` of the github skill's locale
|
|
750
|
+
* module — the one reader of the manifest (tests/standards/project-manifest).
|
|
746
751
|
*/
|
|
747
752
|
function readManifest(root: string): Manifest {
|
|
748
|
-
|
|
749
|
-
try {
|
|
750
|
-
return file ? JSON.parse(readFileSync(file, "utf8")) : {};
|
|
751
|
-
} catch {
|
|
752
|
-
return {};
|
|
753
|
-
}
|
|
753
|
+
return locale.manifestOf(root).fields as Manifest;
|
|
754
754
|
}
|
|
755
755
|
|
|
756
756
|
/**
|
|
@@ -761,9 +761,9 @@ function readManifest(root: string): Manifest {
|
|
|
761
761
|
* doesn't compare the SHA and the tag: the install is up to date if what it installs matches (a release that changes
|
|
762
762
|
* only the installer doesn't touch projects). auto and language — the project settings "a task entirely without
|
|
763
763
|
* asking" and the language the agent works in, always written; the machine doesn't have them (undefined). `env` —
|
|
764
|
-
* the project's task stack settings (the task-env skill: HTTP services and their ports)
|
|
765
|
-
* install carries
|
|
766
|
-
* "behind".
|
|
764
|
+
* the project's task stack settings (the task-env skill: HTTP services and their ports), `batch` — the dispatcher's
|
|
765
|
+
* batches (the github skill): the project writes them, the install carries them over. A dry run doesn't compare them
|
|
766
|
+
* either: editing a field by hand doesn't make the install "behind".
|
|
767
767
|
* The order survives a run cut off midway (an agent's sandbox refuses writes to `.claude/skills`): skills Elves no
|
|
768
768
|
* longer has go while the old manifest still lists them, and the new manifest is written before the copies — a repeat
|
|
769
769
|
* run takes them for the flow's and finishes the install instead of leaving them as someone else's.
|
|
@@ -1374,15 +1374,15 @@ const modeLines = (root: string) => [` tasks: ${autoOf(root) ? "entirely by its
|
|
|
1374
1374
|
const CLONE_NOTE = "an Elves clone: the flow isn't installed into it — the canon is at its root";
|
|
1375
1375
|
|
|
1376
1376
|
/**
|
|
1377
|
-
* An Elves clone: of the install it gets only the
|
|
1378
|
-
* (`
|
|
1379
|
-
* the clone
|
|
1377
|
+
* An Elves clone: of the install it gets only the manifest — the mode, the language and the fields the project writes
|
|
1378
|
+
* (`env`, `batch`), carried over by the same rule as in a project (`manifestText`) — and its cloud hook (`cloudHook`) —
|
|
1379
|
+
* the clone carries it in its commit, the install writes the same; the manifest under the former name goes.
|
|
1380
1380
|
*/
|
|
1381
1381
|
function installClone(root: string, auto: boolean, language: string) {
|
|
1382
1382
|
const file = path.join(root, MANIFEST);
|
|
1383
|
-
const
|
|
1383
|
+
const text = manifestText(root, auto, language);
|
|
1384
1384
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
1385
|
-
writeFileSync(file,
|
|
1385
|
+
writeFileSync(file, text);
|
|
1386
1386
|
removeFormer(root, FORMER.manifest);
|
|
1387
1387
|
note(`${MANIFEST} — auto: ${auto}, language: ${language}; ${CLONE_NOTE}`);
|
|
1388
1388
|
projectHook(root, true);
|
|
@@ -1413,7 +1413,8 @@ const shownPackage = (spec: string | undefined) => (spec === undefined ? `withou
|
|
|
1413
1413
|
|
|
1414
1414
|
/**
|
|
1415
1415
|
* A project with a copy of the flow of an earlier release: the canon `.agents/elves/` (under the former name too) or a
|
|
1416
|
-
* manifest of a copy — the installed skills, the SHA; a thin project's manifest has only the mode, the language, env
|
|
1416
|
+
* manifest of a copy — the installed skills, the SHA; a thin project's manifest has only the mode, the language, env
|
|
1417
|
+
* and batch.
|
|
1417
1418
|
*/
|
|
1418
1419
|
function isCopy(root: string) {
|
|
1419
1420
|
const m = readManifest(root);
|
|
@@ -1457,21 +1458,35 @@ function removeCopy(root: string) {
|
|
|
1457
1458
|
if (gone.length) note(`${gone.join(", ")} — removed: ${COPY_NOTE}`);
|
|
1458
1459
|
}
|
|
1459
1460
|
|
|
1460
|
-
/**
|
|
1461
|
-
|
|
1461
|
+
/**
|
|
1462
|
+
* The fields of a project's manifest the project writes itself — the task stack settings (`env`, the task-env skill)
|
|
1463
|
+
* and the dispatcher's batches (`batch`, the github skill's `task queue`, epic #645): the install carries them over.
|
|
1464
|
+
*/
|
|
1465
|
+
const CARRIED_FIELDS = ["env", "batch"] as const;
|
|
1466
|
+
|
|
1467
|
+
/** The fields of a project's manifest: the install's auto and language and the project's own; the rest are a copy's (`isCopy`). */
|
|
1468
|
+
const PROJECT_FIELDS: string[] = ["auto", "language", ...CARRIED_FIELDS];
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* The manifest the install writes — of a thin project and an Elves clone alike (#672): auto and language, the
|
|
1472
|
+
* CARRIED_FIELDS of the current manifest as they are; the fields of a copy go.
|
|
1473
|
+
*/
|
|
1474
|
+
function manifestText(root: string, auto: boolean, language: string) {
|
|
1475
|
+
const m = readManifest(root);
|
|
1476
|
+
const carried = CARRIED_FIELDS.filter((k) => m[k] !== undefined).map((k) => [k, m[k]]);
|
|
1477
|
+
return JSON.stringify({ auto, language, ...Object.fromEntries(carried) }, null, 2) + "\n";
|
|
1478
|
+
}
|
|
1462
1479
|
|
|
1463
1480
|
/**
|
|
1464
|
-
* The project's manifest
|
|
1465
|
-
*
|
|
1466
|
-
* A dry run doesn't compare the values: auto and language edited by hand don't make the install behind.
|
|
1481
|
+
* The project's manifest (`manifestText`); the manifest under the former name moves into it. A dry run doesn't compare
|
|
1482
|
+
* the values: auto and language edited by hand don't make the install behind.
|
|
1467
1483
|
*/
|
|
1468
1484
|
function projectManifest(root: string, auto: boolean, language: string) {
|
|
1469
1485
|
const file = path.join(root, MANIFEST);
|
|
1470
|
-
const m = readManifest(root);
|
|
1471
1486
|
if (dry) {
|
|
1472
|
-
if (!existsSync(file) || Object.keys(
|
|
1487
|
+
if (!existsSync(file) || Object.keys(readManifest(root)).some((k) => !PROJECT_FIELDS.includes(k))) change(existsSync(file) ? "~" : "+", file);
|
|
1473
1488
|
} else {
|
|
1474
|
-
const text =
|
|
1489
|
+
const text = manifestText(root, auto, language);
|
|
1475
1490
|
if (!existsSync(file) || readFileSync(file, "utf8") !== text) {
|
|
1476
1491
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
1477
1492
|
writeFileSync(file, text);
|
|
@@ -1683,10 +1698,10 @@ function rewriteCopyRefs(root: string) {
|
|
|
1683
1698
|
}
|
|
1684
1699
|
|
|
1685
1700
|
/**
|
|
1686
|
-
* A mention of `.agents
|
|
1687
|
-
* the machine aren't one.
|
|
1701
|
+
* A mention of a path inside `.agents/` (a glob too) that isn't the thin project's own: the directory itself, the
|
|
1702
|
+
* manifest, the shim, `~/.agents` and `$HOME/.agents` of the machine aren't one (#642).
|
|
1688
1703
|
*/
|
|
1689
|
-
const COPY_MENTION = /(?<![\w~}-]|~\/|HOME\/|HOME}\/)\.agents(?!
|
|
1704
|
+
const COPY_MENTION = /(?<![\w~}-]|~\/|HOME\/|HOME}\/)\.agents\\?\/(?!(?:elves\\?\.json|slot\\?\.sh)\b)[\w*.{]/;
|
|
1690
1705
|
|
|
1691
1706
|
/**
|
|
1692
1707
|
* What still names `.agents` once the copy is gone: exceptions `.agents/**` in linters, formatters and configs aren't
|
|
@@ -1865,7 +1880,7 @@ function inside(dir: string, p: string) {
|
|
|
1865
1880
|
function ownCommand(file: string) {
|
|
1866
1881
|
const st = lstat(file);
|
|
1867
1882
|
if (st?.isSymbolicLink()) {
|
|
1868
|
-
const to = readlinkSync(file);
|
|
1883
|
+
const to = path.resolve(path.dirname(file), readlinkSync(file));
|
|
1869
1884
|
return /(?:^|\/)bin\/elves\.ts$/.test(to) && !to.split("/").includes("node_modules");
|
|
1870
1885
|
}
|
|
1871
1886
|
if (!st?.isFile() || st.size > 4096) return false;
|
|
@@ -1940,7 +1955,8 @@ function installCommand(src: string, home: string, linkMode: boolean) {
|
|
|
1940
1955
|
note(`${shown(dst)} — ${COMMAND_NOTE}: ${linkMode ? `→ ${target}` : (/^exec (.*) "\$@"$/m.exec(text)?.[1] ?? "")}`);
|
|
1941
1956
|
}
|
|
1942
1957
|
const dirs = pathDirs();
|
|
1943
|
-
|
|
1958
|
+
// `node_modules/.bin` in the process's PATH is the package manager's (bunx, npx, a script), not the terminal's (#642)
|
|
1959
|
+
const first = dirs.slice(0, dirs.indexOf(dir)).filter((d) => !/(?:^|\/)node_modules\/\.bin$/.test(d)).map((d) => path.join(d, COMMAND)).find(runnable);
|
|
1944
1960
|
if (first) say(first, `comes first on PATH: elves runs it, not ${shown(dst)} — remove it or put ${shown(dir)} before ${shown(path.dirname(first))} in PATH`);
|
|
1945
1961
|
}
|
|
1946
1962
|
|
|
@@ -2191,6 +2207,95 @@ function specScript(root: string): { lines: string[]; missing: boolean } {
|
|
|
2191
2207
|
return { lines: [`${lead}; put in "scripts" — test without them, they don't run twice: ${spec}, ${filtered}`], missing: true };
|
|
2192
2208
|
}
|
|
2193
2209
|
|
|
2210
|
+
/** Where a batch project's CI is described: the spec skill on the machine (`install -g`). */
|
|
2211
|
+
const BATCHES_DOC = "~/.agents/skills/spec/ci.md, «Batches»";
|
|
2212
|
+
/** A batch branch for a workflow's `on.pull_request` filter — the name `task queue` gives (the github skill). */
|
|
2213
|
+
const BATCH_SAMPLE = "batch/2026-01-01-1";
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* Whether a workflow gets a run on a PR into `base`, by its `on:` (tests/standards/pr-workflow-run): `yes`, `no` or
|
|
2217
|
+
* `unknown` — not decidable without the PR's diff and event. `pull_request` as a string, in an array, a key with
|
|
2218
|
+
* filters or null; `pull_request_target` runs in the base, not on the PR's head — `no`. The branch filter goes first: a
|
|
2219
|
+
* base it filters out gets no run whatever the diff. Its patterns — GitHub's filter pattern cheat sheet: in order, `!`
|
|
2220
|
+
* at the start drops what the earlier ones took, a later pattern takes it back; `*` — anything but `/`, `**` — anything,
|
|
2221
|
+
* `?`, `+` and `[…]` — as in a regex; a pattern that isn't one takes nothing. Then a paths filter and `types` without
|
|
2222
|
+
* `synchronize` — `unknown`. What follows from the answer is the reader's: `batchCi` — a `❌` line on `yes` and on
|
|
2223
|
+
* `unknown` into a batch branch (a PR into it starts no workflow). A copy — PR_RUN in skills/ci-wait/scripts/wait-ci.sh
|
|
2224
|
+
* (`ci-wait pr` waits on `yes`): ci-wait and the installer don't import each other (tests/standards/skill-imports),
|
|
2225
|
+
* both answer by one table.
|
|
2226
|
+
*/
|
|
2227
|
+
export function prRun(on: unknown, base: string): "yes" | "no" | "unknown" {
|
|
2228
|
+
const list = (v: unknown) => [v].flat();
|
|
2229
|
+
const takes = (patterns: unknown[]) => {
|
|
2230
|
+
let taken = false;
|
|
2231
|
+
for (const p of patterns) {
|
|
2232
|
+
if (typeof p !== "string") continue;
|
|
2233
|
+
const negated = p.startsWith("!");
|
|
2234
|
+
let re: RegExp;
|
|
2235
|
+
try {
|
|
2236
|
+
re = new RegExp(`^${(negated ? p.slice(1) : p).replace(/[.^$(){}|\\]/g, "\\$&").replace(/\*\*?/g, (m) => (m === "**" ? ".*" : "[^/]*"))}$`);
|
|
2237
|
+
} catch {
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (re.test(base)) taken = !negated;
|
|
2241
|
+
}
|
|
2242
|
+
return taken;
|
|
2243
|
+
};
|
|
2244
|
+
let filter: unknown;
|
|
2245
|
+
if (typeof on === "string" || Array.isArray(on)) {
|
|
2246
|
+
if (!list(on).includes("pull_request")) return "no";
|
|
2247
|
+
filter = {};
|
|
2248
|
+
} else if (on && typeof on === "object" && Object.hasOwn(on, "pull_request")) {
|
|
2249
|
+
filter = (on as Record<string, unknown>).pull_request ?? {};
|
|
2250
|
+
} else return "no";
|
|
2251
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter)) return "unknown";
|
|
2252
|
+
const f = filter as Record<string, unknown>;
|
|
2253
|
+
if (Object.hasOwn(f, "branches") ? !takes(list(f.branches)) : Object.hasOwn(f, "branches-ignore") && takes(list(f["branches-ignore"]))) return "no";
|
|
2254
|
+
if (Object.hasOwn(f, "paths") || Object.hasOwn(f, "paths-ignore")) return "unknown";
|
|
2255
|
+
if (Object.hasOwn(f, "types") && !list(f.types).includes("synchronize")) return "unknown";
|
|
2256
|
+
return "yes";
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/** A workflow's triggers (`Bun.YAML`); not YAML — null: GitHub doesn't run it. */
|
|
2260
|
+
function workflowOf(text: string): { on?: unknown } | null {
|
|
2261
|
+
try {
|
|
2262
|
+
const wf: unknown = Bun.YAML.parse(text);
|
|
2263
|
+
return wf && typeof wf === "object" ? wf : null;
|
|
2264
|
+
} catch {
|
|
2265
|
+
return null;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
/**
|
|
2270
|
+
* A batch project's CI (`"batch": true` in the manifest, epic #645; `batchProject` of the locale module — the same
|
|
2271
|
+
* answer as `task queue`'s), a thin project's and an Elves clone's alike (#672): a PR into the batch branch starts no
|
|
2272
|
+
* workflow (#676) — its gate is the checks the executor
|
|
2273
|
+
* runs before a commit and `pr premerge`, the full CI is the release PR's and `main`'s (`BATCHES_DOC`); a run on every
|
|
2274
|
+
* task PR repeats what the executor ran and holds its slot. A mismatch update doesn't fix, as the compose file and
|
|
2275
|
+
* `test:spec`: a `❌` line per workflow of `.github/workflows/*.y(a)ml` that by `prRun` runs on a PR into a batch
|
|
2276
|
+
* branch — `yes`, or `unknown` (a paths filter, `types` without `synchronize`): it may run, the line says so. `no` —
|
|
2277
|
+
* no line: a branch filter that leaves the batch branch out, push only, `pull_request_target`. A workflow that doesn't
|
|
2278
|
+
* parse runs nothing on GitHub either. Not a batch project — CI isn't read.
|
|
2279
|
+
*/
|
|
2280
|
+
function batchCi(root: string): { lines: string[]; missing: boolean } {
|
|
2281
|
+
if (!locale.batchProject(root)) return { lines: [], missing: false };
|
|
2282
|
+
const dir = ".github/workflows";
|
|
2283
|
+
let files: string[] = [];
|
|
2284
|
+
try {
|
|
2285
|
+
files = readdirSync(path.join(root, dir)).filter((f) => /\.ya?ml$/.test(f)).sort();
|
|
2286
|
+
} catch {
|
|
2287
|
+
// no workflows — nothing runs on a PR into a batch branch
|
|
2288
|
+
}
|
|
2289
|
+
const lines: string[] = [];
|
|
2290
|
+
for (const f of files) {
|
|
2291
|
+
const run = prRun(workflowOf(readFileSync(path.join(root, dir, f), "utf8"))?.on, BATCH_SAMPLE);
|
|
2292
|
+
if (run === "no") continue;
|
|
2293
|
+
const how = run === "yes" ? "runs" : "may run (by the PR's diff or event)";
|
|
2294
|
+
lines.push(` ❌ ${dir}/${f}: on.pull_request ${how} on a PR into a batch branch — its gate is the checks before a commit and pr premerge, the full CI is the release's; add branches-ignore: ['batch/**'] (${BATCHES_DOC})`);
|
|
2295
|
+
}
|
|
2296
|
+
return { lines, missing: lines.length > 0 };
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2194
2299
|
/** The check failed: what and why. */
|
|
2195
2300
|
const unavailable = (global: boolean, e: unknown): Result => ({ code: 2, text: `Elves ${global ? "on the machine" : "in the project"}: check unavailable — ${reason(e)}` });
|
|
2196
2301
|
|
|
@@ -2290,8 +2395,12 @@ function cloudLines(root: string): string[] {
|
|
|
2290
2395
|
function check(global: boolean): Result {
|
|
2291
2396
|
const { home, root, installed } = target(global);
|
|
2292
2397
|
const where = global ? "on the machine" : "in the project";
|
|
2293
|
-
// an Elves clone has nothing to compare against, but the session mode is needed in it — as in a project
|
|
2294
|
-
|
|
2398
|
+
// an Elves clone has nothing to compare against, but the session mode is needed in it — as in a project, and so is
|
|
2399
|
+
// the CI of its batches (#672)
|
|
2400
|
+
if (!installed && !global && isElvesClone(root)) {
|
|
2401
|
+
const batch = batchCi(root);
|
|
2402
|
+
return { code: batch.missing ? 1 : 0, text: [`Elves ${where}: not installed — an Elves clone, the canon is at its root`, ...batch.lines, ...cloudLines(root), ...modeLines(root)].join("\n") };
|
|
2403
|
+
}
|
|
2295
2404
|
if (!installed) return { code: 0, text: `Elves ${where}: not installed`, absent: true };
|
|
2296
2405
|
const clone = global ? linkedClone(home) : null;
|
|
2297
2406
|
if (clone) return checkLink(home, clone);
|
|
@@ -2306,10 +2415,12 @@ function check(global: boolean): Result {
|
|
|
2306
2415
|
const compose = global ? { lines: [], violations: false } : composeCheck(root);
|
|
2307
2416
|
// standards without test:spec — a mismatch update doesn't add either
|
|
2308
2417
|
const spec = global ? { lines: [], missing: false } : specScript(root);
|
|
2309
|
-
|
|
2418
|
+
// a batch project's CI that runs on a PR into a batch branch — a mismatch update doesn't fix either
|
|
2419
|
+
const batch = global ? { lines: [], missing: false } : batchCi(root);
|
|
2420
|
+
const project = [...warnings, ...compose.lines, ...spec.lines, ...batch.lines, ...(global ? [] : cloudLines(root)), ...mode];
|
|
2310
2421
|
const stale = changes.filter((c) => c.op !== "❌" && c.op !== "!");
|
|
2311
2422
|
const ignored = changes.filter((c) => c.op === "❌");
|
|
2312
|
-
if (!stale.length && !ignored.length) return { code: compose.violations || spec.missing ? 1 : 0, compose: compose.violations, spec: spec.missing, text: [`Elves ${where}: up to date (${fresh})`, ...project].join("\n") };
|
|
2423
|
+
if (!stale.length && !ignored.length) return { code: compose.violations || spec.missing || batch.missing ? 1 : 0, compose: compose.violations, spec: spec.missing, text: [`Elves ${where}: up to date (${fresh})`, ...project].join("\n") };
|
|
2313
2424
|
const m = readManifest(root);
|
|
2314
2425
|
const fixable = ignored.some((c) => !c.manual);
|
|
2315
2426
|
const lost = "ignores flow files — they are missing from the commit, other checkouts and the cloud session:";
|
|
@@ -3034,7 +3145,12 @@ function takeTag(args: string[]): { tag: string | undefined; rest: string[] } {
|
|
|
3034
3145
|
return i < 0 ? { tag: undefined, rest: args } : { tag: args[i], rest: args.filter((_, j) => j !== i) };
|
|
3035
3146
|
}
|
|
3036
3147
|
|
|
3037
|
-
|
|
3148
|
+
/**
|
|
3149
|
+
* The command line. It runs only as the entry point (`import.meta.main`, as the skills' scripts): a test imports the
|
|
3150
|
+
* installer for `prRun` (tests/standards/pr-workflow-run); the installer under its path before the rename calls it
|
|
3151
|
+
* itself.
|
|
3152
|
+
*/
|
|
3153
|
+
export async function main(argv: string[]): Promise<number> {
|
|
3038
3154
|
const [cmd = "", ...args] = argv;
|
|
3039
3155
|
// a skill's script — at this package's version, the project's: no launcher, the help is the script's
|
|
3040
3156
|
if (Object.hasOwn(SCRIPTS, cmd)) return runScript(cmd, args);
|
|
@@ -3118,4 +3234,4 @@ async function main(argv: string[]): Promise<number> {
|
|
|
3118
3234
|
}
|
|
3119
3235
|
}
|
|
3120
3236
|
|
|
3121
|
-
process.exitCode = await main(process.argv.slice(2));
|
|
3237
|
+
if (import.meta.main) process.exitCode = await main(process.argv.slice(2));
|