@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,113 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun --no-env-file
|
|
2
|
+
/**
|
|
3
|
+
* timeout — GNU coreutils `timeout` for a machine without it (macOS without coreutils). The SessionStart hook puts this
|
|
4
|
+
* directory at the end of the agent's Bash PATH (bin/elves.ts, `agentShellScript`), so a real `timeout` wins;
|
|
5
|
+
* tests/capabilities/agent-shell.
|
|
6
|
+
*
|
|
7
|
+
* timeout [-s SIGNAL] [-k DURATION] [--preserve-status] [--foreground] [-v] DURATION COMMAND [ARG]...
|
|
8
|
+
*
|
|
9
|
+
* DURATION — a number with an optional suffix s, m, h, d; 0 — no limit. On expiry the command's process group gets
|
|
10
|
+
* SIGNAL (TERM by default): a grandchild that outlived the command would hold the call's output until its own end;
|
|
11
|
+
* with -k — KILL after DURATION more. Exit code: 124 — timed out (137 if KILL ended the command, as GNU), the
|
|
12
|
+
* command's own with --preserve-status; 125 — timeout itself failed; 126 — the command can't run; 127 — not found;
|
|
13
|
+
* otherwise the command's (128 + N for signal N). INT, TERM, HUP and QUIT that timeout gets go to the command.
|
|
14
|
+
* The `--no-env-file` flag in the shebang: Bun would load the project's env files and pass them to the command.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { constants } from "node:os";
|
|
18
|
+
|
|
19
|
+
const USAGE = "Usage: timeout [-s SIGNAL] [-k DURATION] [--preserve-status] [--foreground] [-v] DURATION COMMAND [ARG]...";
|
|
20
|
+
const SIGNALS: Record<string, number> = constants.signals;
|
|
21
|
+
/** setTimeout fires at once past 2^31 - 1 ms (24.8 days): a longer limit waits that long — no agent call lasts it. */
|
|
22
|
+
const MAX_DELAY = 2 ** 31 - 1;
|
|
23
|
+
const UNITS: Record<string, number> = { "": 1, s: 1, m: 60, h: 3600, d: 86400 };
|
|
24
|
+
|
|
25
|
+
function fail(message: string): never {
|
|
26
|
+
process.stderr.write(`timeout: ${message}\n`);
|
|
27
|
+
process.exit(125);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** DURATION in milliseconds. */
|
|
31
|
+
function duration(text: string) {
|
|
32
|
+
const m = /^(\d+(?:\.\d*)?|\.\d+)([smhd]?)$/.exec(text);
|
|
33
|
+
if (!m) fail(`invalid time interval '${text}'\n${USAGE}`);
|
|
34
|
+
return Math.min(Number(m[1]) * (UNITS[m[2] ?? ""] ?? 1) * 1000, MAX_DELAY);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A signal by name (KILL, SIGKILL, kill) or number. */
|
|
38
|
+
function signal(text: string): NodeJS.Signals {
|
|
39
|
+
const name = /^\d+$/.test(text) ? Object.keys(SIGNALS).find((k) => SIGNALS[k] === Number(text)) : `SIG${text.toUpperCase().replace(/^SIG/, "")}`;
|
|
40
|
+
if (!name || !Object.hasOwn(SIGNALS, name)) fail(`${text}: invalid signal`);
|
|
41
|
+
return name as NodeJS.Signals;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
|
|
46
|
+
/** The value of an option before DURATION: `-s KILL`, `-sKILL`, `--signal KILL`, `--signal=KILL`; not it — undefined. */
|
|
47
|
+
function value(short: string, long: string) {
|
|
48
|
+
const a = args[0] ?? "";
|
|
49
|
+
const inline = a.startsWith(`${long}=`) ? a.slice(long.length + 1) : a.startsWith(short) && a.length > short.length ? a.slice(short.length) : undefined;
|
|
50
|
+
if (inline !== undefined) return args.shift(), inline;
|
|
51
|
+
if (a !== short && a !== long) return undefined;
|
|
52
|
+
args.shift();
|
|
53
|
+
return args.shift() ?? fail(`option requires an argument -- '${short.slice(1)}'\n${USAGE}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let sig: NodeJS.Signals = "SIGTERM";
|
|
57
|
+
let killAfter = 0;
|
|
58
|
+
let preserve = false;
|
|
59
|
+
let foreground = false;
|
|
60
|
+
let verbose = false;
|
|
61
|
+
// options go before DURATION, as with GNU: what follows belongs to the command
|
|
62
|
+
for (let a = args[0]; a?.startsWith("-") && a !== "-"; a = args[0]) {
|
|
63
|
+
let v;
|
|
64
|
+
if (a === "--") {
|
|
65
|
+
args.shift();
|
|
66
|
+
break;
|
|
67
|
+
} else if ((v = value("-s", "--signal")) !== undefined) sig = signal(v);
|
|
68
|
+
else if ((v = value("-k", "--kill-after")) !== undefined) killAfter = duration(v);
|
|
69
|
+
else if (a === "--preserve-status") (preserve = true), args.shift();
|
|
70
|
+
else if (a === "--foreground") (foreground = true), args.shift();
|
|
71
|
+
else if (a === "-v" || a === "--verbose") (verbose = true), args.shift();
|
|
72
|
+
else if (a === "--help") process.stdout.write(USAGE + "\n"), process.exit(0);
|
|
73
|
+
else fail(`unrecognized option '${a}'\n${USAGE}`);
|
|
74
|
+
}
|
|
75
|
+
const [limitText, cmd, ...rest] = args;
|
|
76
|
+
if (limitText === undefined || cmd === undefined) fail(`missing operand\n${USAGE}`);
|
|
77
|
+
const limit = duration(limitText);
|
|
78
|
+
|
|
79
|
+
// its own process group (a session) — the signal reaches the command's children too; --foreground — the command only
|
|
80
|
+
const child = spawn(cmd, rest, { stdio: "inherit", detached: !foreground });
|
|
81
|
+
let timedOut = false;
|
|
82
|
+
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
83
|
+
|
|
84
|
+
function send(s: NodeJS.Signals) {
|
|
85
|
+
if (verbose && timedOut) process.stderr.write(`timeout: sending signal ${s.slice(3)} to command '${cmd}'\n`);
|
|
86
|
+
try {
|
|
87
|
+
if (foreground || child.pid === undefined) child.kill(s);
|
|
88
|
+
else process.kill(-child.pid, s);
|
|
89
|
+
} catch {
|
|
90
|
+
// the group is already gone
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (limit > 0) {
|
|
95
|
+
timers.push(
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
timedOut = true;
|
|
98
|
+
send(sig);
|
|
99
|
+
if (killAfter > 0) timers.push(setTimeout(() => send("SIGKILL"), killAfter));
|
|
100
|
+
}, limit),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"] as const) process.on(s, () => send(s));
|
|
104
|
+
|
|
105
|
+
child.on("error", (e: NodeJS.ErrnoException) => {
|
|
106
|
+
process.stderr.write(`timeout: failed to run command '${cmd}': ${e.code === "ENOENT" ? "No such file or directory" : e.message}\n`);
|
|
107
|
+
process.exit(e.code === "ENOENT" ? 127 : 126);
|
|
108
|
+
});
|
|
109
|
+
child.on("exit", (code, signalName) => {
|
|
110
|
+
for (const t of timers) clearTimeout(t);
|
|
111
|
+
const status = signalName ? 128 + (SIGNALS[signalName] ?? 0) : (code ?? 0);
|
|
112
|
+
process.exit(timedOut && !preserve && signalName !== "SIGKILL" ? 124 : status);
|
|
113
|
+
});
|
package/claude/CLAUDE.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Claude Code
|
|
2
|
+
|
|
3
|
+
Shared rules — `AGENTS.md`; here — only what Claude Code has. `install -g`
|
|
4
|
+
puts both into `~/.claude/rules/` (`elves.md`, `elves-claude.md`): loaded in
|
|
5
|
+
every project, whatever its `CLAUDE.md`.
|
|
6
|
+
|
|
7
|
+
- Session transcripts (`~/.claude/projects/<path-with-dashes>*/`) are the
|
|
8
|
+
source of actuals for the `est` skill; `~/.claude/settings.json` must have
|
|
9
|
+
`"cleanupPeriodDays": 365`, otherwise history is cleaned before it's needed.
|
|
10
|
+
- The app's worktree names the branch `claude/<slug>-<hash>` — it is renamed
|
|
11
|
+
when the task is taken: `github task status <N> "In progress" --branch
|
|
12
|
+
<type>/<N>-<slug>` (a bare `git branch -m` fails on `.git/config` locked by
|
|
13
|
+
a parallel worktree and leaves the upstream behind). The session panel
|
|
14
|
+
(branch, diff to `main`, "Create PR") reads git, not GitHub: after a merge
|
|
15
|
+
it is accurate once the checkout is on `origin/main` (`github task close`
|
|
16
|
+
moves it).
|
|
17
|
+
- Auto-fix (CI monitor) — off: its conflict event wants the base merged in,
|
|
18
|
+
breaking the rebase merge. After `gh pr create` — `set_monitor` with
|
|
19
|
+
`auto_fix: false` and the PR's url; a conflict — rebase onto a fresh
|
|
20
|
+
`origin/main`; an executor's PR — its executor's (`docs/pr-checks.md`,
|
|
21
|
+
"Auto-fix"). Checks and the deploy — `ci-wait` as a background command,
|
|
22
|
+
its completion wakes the session; no `Monitor`, watcher loop or
|
|
23
|
+
`ScheduleWakeup` on top: an armed wakeup keeps `archive_session` refused
|
|
24
|
+
even after `stop`.
|
|
25
|
+
- Merge in auto mode — `gh pr merge <N> --rebase` as a separate call, without
|
|
26
|
+
`cd`, `|`, `;`: otherwise the `Bash(gh pr merge *)` rule (set by the owner)
|
|
27
|
+
won't match and the classifier rejects the merge.
|
|
28
|
+
- `install -g` puts a `SessionStart` hook into `~/.claude/settings.json`
|
|
29
|
+
(`startup|resume|clear`): `check --hook` — the machine (canon, skills, hook)
|
|
30
|
+
and the project's flow with its mode and language (`AGENTS.md`, "First step
|
|
31
|
+
of a session") into the start of the context: a thin project's lag —
|
|
32
|
+
`update` in the session's task, a copy — the canon twice and its move task,
|
|
33
|
+
no flow — install it; a
|
|
34
|
+
session with a pinned task (a resume after the app restarted) gets the wait
|
|
35
|
+
to restart — `github task resume` with the session id from the hook's input.
|
|
36
|
+
The hook always exits 0 (else Claude Code drops its output), timeout 60 s.
|
|
37
|
+
No hook output in context — run `check` by hand. The hook also writes the
|
|
38
|
+
agent's Bash environment to `$CLAUDE_ENV_FILE`: zsh passes a glob without
|
|
39
|
+
matches and `====` as is, `timeout` works without coreutils; the
|
|
40
|
+
«agent Bash: not set up» line — it didn't, follow the line.
|
|
41
|
+
- `set_session_title` renames the session; in auto mode `archive_session`
|
|
42
|
+
with `self` archives it, ending the conversation: the last action, after
|
|
43
|
+
the "can be closed" line. Before it — `TaskStop` for each own
|
|
44
|
+
background task still running: a live one gives "still has live work".
|
|
45
|
+
Refused after that — don't retry: the owner archives from the sidebar.
|
|
46
|
+
- Skills — the machine's `~/.claude/skills/<name>` (symlinks to
|
|
47
|
+
`~/.agents/skills/<name>`), a thin project has no others; the skill
|
|
48
|
+
directory is `${CLAUDE_SKILL_DIR}`; a same-named machine skill wins over a
|
|
49
|
+
project's. `SKILL.md` frontmatter also reads `when_to_use`,
|
|
50
|
+
`argument-hint`, `allowed-tools`.
|
|
51
|
+
- The Bash sandbox: `gh` in it fails on TLS (`x509: OSStatus -26276`). A
|
|
52
|
+
skill script that calls `gh` (`github`, `est`, `dashboard`, `spec-run`) —
|
|
53
|
+
at once a separate call with `dangerouslyDisableSandbox`: no
|
|
54
|
+
`excludedCommands` entry takes `bun <file>` out. Entries `gh *` and `bash
|
|
55
|
+
<path>/wait-ci.sh *` (`ci-wait`) do — the owner's step; without them — the
|
|
56
|
+
same separate call. The `task-env` skill, section "Docker and the sandbox".
|
|
57
|
+
- A subagent that writes gets `isolation: worktree`: an isolated session
|
|
58
|
+
doesn't write into others' worktrees, an agent without it inherits the
|
|
59
|
+
parent's isolation. An agent cut off (a limit, a dropped session) leaves
|
|
60
|
+
uncommitted work in its worktree — `git status` there before resuming;
|
|
61
|
+
after a base change — dependencies from the lockfile, else typecheck runs
|
|
62
|
+
on old packages.
|
|
63
|
+
- In a cloud session (claude.ai/code) the cloud assigns the branch — rename
|
|
64
|
+
it or push to another one only with the user's permission
|
|
65
|
+
(`docs/cloud-sessions.md`).
|
|
66
|
+
- Another task isn't a session card (`spawn_task`, `start_session`): the
|
|
67
|
+
question «queue #M?», on "yes" its executor — `Agent` with `isolation:
|
|
68
|
+
"worktree"`, `run_in_background: true` and the prompt `github` printed.
|
|
69
|
+
The executor isn't `TaskStop`ped: the last line — after its answer.
|
|
70
|
+
- An Elves clone: `install -g --link` links `~/.claude/rules/elves*.md` to
|
|
71
|
+
it, its `.claude/CLAUDE.md` keeps the root `AGENTS.md` from loading twice.
|