@miroshnik/elves 2026.1018.2 → 2026.1019.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/README.md +2 -2
- package/bin/elves.ts +159 -7
- package/dist/github/scripts/github.js +82 -25
- package/dist/spec/scripts/architecture.js +2 -2
- package/dist/spec/scripts/{harness-v9s1v9w3.js → harness-ay62ws50.js} +1 -1
- package/dist/spec/scripts/{harness-qzb932c8.js → harness-wqrt5gpa.js} +18 -2
- package/dist/spec/scripts/harness.js +2 -2
- package/package.json +3 -2
- package/skills/ci-wait/SKILL.md +3 -1
- package/skills/ci-wait/scripts/wait-ci.sh +8 -3
- package/skills/github/SKILL.md +7 -7
- package/skills/github/locales/en.json +9 -1
- package/skills/github/locales/ru.json +9 -1
- package/skills/github/reference.md +82 -29
- package/skills/github/scripts/github.ts +131 -29
- package/skills/github/scripts/locale.ts +13 -0
package/README.md
CHANGED
|
@@ -155,7 +155,7 @@ No exaggeration: the spec format isn't the main token cost; what costs more is h
|
|
|
155
155
|
You need `git` and [Bun](https://bun.sh) ≥ 1.2 — the installer (`bunx`) and the skill scripts run under it; a project's CI on Node adds the `oven-sh/setup-bun` step; the `github`, `est` and `dashboard` skills need [`gh`](https://cli.github.com) with the `project` scope (`gh auth refresh -s project`); `task-env` needs Docker with Compose (Docker Desktop on macOS).
|
|
156
156
|
|
|
157
157
|
```bash
|
|
158
|
-
# onto the machine — the canon, skills and the hook for every agent it has; or a global package:
|
|
158
|
+
# onto the machine — the canon, skills and the hook for every agent it has, and the elves command on PATH; or a global package:
|
|
159
159
|
# bun add -g @miroshnik/elves && elves install -g
|
|
160
160
|
bunx @miroshnik/elves install -g
|
|
161
161
|
|
|
@@ -193,7 +193,7 @@ An agent's sandbox (Claude Code's Bash) doesn't let bunx write its cache `~/.bun
|
|
|
193
193
|
- Updating — `update` bumps the package version: `package.json` and the lockfile, a few lines in the branch of the current task, committed with it. A project with a copy of the flow of an earlier release moves to a thin one in a task of its own, one per project — `check` prints its `task new`, the hook names the canon loaded twice until then: `update` removes the copy, its symlinks and `.gitignore` exceptions, leads imports and CI paths into the copy to the package and lists what else names `.agents`.
|
|
194
194
|
- The manifest, the shim, `AGENTS.md` and `.claude/settings.json`, if the project's git ignores them (`.agents/` or `.claude/` in `.gitignore`), are un-ignored by `install` and `update` with exceptions — an Elves block at the end of `.gitignore`, committed with them; what an exception can't un-ignore (a directory excluded by a personal rule), and anything ignored until `update` runs — `❌` in the output of the install and of `check`.
|
|
195
195
|
|
|
196
|
-
**Onto the machine** (`-g`) — the canon, the skills and the hook: a thin project keeps no copy of the canon, its agents get it from the machine. `~/.agents/elves/` — `AGENTS.md`, `claude/CLAUDE.md`, `docs/*.md`; Claude Code — symlinks `~/.claude/rules/elves.md` and `elves-claude.md` (loaded in every project without the external-imports dialog), `~/.claude/skills` and the `SessionStart` hook in `~/.claude/settings.json`; the other agents — their global rule file linking to the canon (`~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.copilot/copilot-instructions.md`, `~/.config/opencode/AGENTS.md`, `~/.config/amp/AGENTS.md`; a file of your own stays) and `~/.agents/skills`. A project that still has a copy loads the canon twice until it moves — the hook names it. A Claude Code cloud session gets the same from the project's `SessionStart` hook — `npm i -g bun@latest @miroshnik/elves && elves install -g` (the image's Bun lags behind), then `check --hook`; the machine's hook isn't there (the cloud doesn't read `~/.claude/settings.json`), an environment's setup script with the same command is an optional faster start. In an Elves clone with `--link` the machine's canon is the clone; its `.claude/CLAUDE.md` keeps the root `AGENTS.md` from loading a second time.
|
|
196
|
+
**Onto the machine** (`-g`) — the canon, the skills and the hook: a thin project keeps no copy of the canon, its agents get it from the machine. `~/.agents/elves/` — `AGENTS.md`, `claude/CLAUDE.md`, `docs/*.md`; Claude Code — symlinks `~/.claude/rules/elves.md` and `elves-claude.md` (loaded in every project without the external-imports dialog), `~/.claude/skills` and the `SessionStart` hook in `~/.claude/settings.json`; the other agents — their global rule file linking to the canon (`~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.copilot/copilot-instructions.md`, `~/.config/opencode/AGENTS.md`, `~/.config/amp/AGENTS.md`; a file of your own stays) and `~/.agents/skills`. The `elves` command in the terminal — in `~/.local/bin`, or the first directory of `PATH` inside the home directory you can write to: with `--link` a symlink to the clone's `bin/elves.ts`, otherwise a shim to the machine's release (`bunx @miroshnik/elves@<version>`) that `update -g` rewrites; an `elves` of your own (a script, a global package) stays, and `check -g` names it, a command missing or of another release, an `elves` earlier on `PATH`, and — when `PATH` has no such directory — the command that adds `~/.local/bin`. A project that still has a copy loads the canon twice until it moves — the hook names it. A Claude Code cloud session gets the same from the project's `SessionStart` hook — `npm i -g bun@latest @miroshnik/elves && elves install -g` (the image's Bun lags behind), then `check --hook`; the machine's hook isn't there (the cloud doesn't read `~/.claude/settings.json`), an environment's setup script with the same command is an optional faster start. In an Elves clone with `--link` the machine's canon is the clone; its `.claude/CLAUDE.md` keeps the root `AGENTS.md` from loading a second time.
|
|
197
197
|
|
|
198
198
|
**Personal configuration** — everything that depends on specific repositories (the registry for `est`, your own model prices, notes) — lives outside the flow, in `~/.config/elves`; another directory — the `ELVES_CONFIG_DIR` variable, read by all skill scripts. `ELVES_PRIVATE=<path>` with `install -g` makes `~/.config/elves` a symlink to a private checkout; with `ELVES_CONFIG_DIR` the symlink isn't needed — the variable points at the checkout itself.
|
|
199
199
|
|
package/bin/elves.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* bunx @miroshnik/elves install — into the project: the git root (or the current directory)
|
|
8
8
|
* bunx @miroshnik/elves install -g — machine-wide: the canon, skills and hook
|
|
9
|
-
* elves install -g — the same from the npm package (npm i -g @miroshnik/elves)
|
|
9
|
+
* elves install -g — the same from the npm package (npm i -g @miroshnik/elves);
|
|
10
|
+
* install -g puts the elves command on PATH (`installCommand`)
|
|
10
11
|
* bun <Elves clone>/bin/elves.ts install -g --link — from a clone: symlinks to the clone, edits show at once
|
|
11
12
|
* bunx @miroshnik/elves check [-g] — is the install behind: 0 — no, 1 — yes, 2 — can't tell
|
|
12
13
|
* bunx @miroshnik/elves update [-g] — bring the install up to date
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
* proxy name) instead of holding copies. No dependencies, only node: APIs.
|
|
77
78
|
*/
|
|
78
79
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
79
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, readSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
80
|
+
import { accessSync, constants as fsConstants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, readSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
80
81
|
import os from "node:os";
|
|
81
82
|
import path from "node:path";
|
|
82
83
|
import { isatty } from "node:tty";
|
|
@@ -223,7 +224,10 @@ terminal — the previous one. In an Elves clone writes only auto and language,
|
|
|
223
224
|
from the clone itself.
|
|
224
225
|
-g — onto the machine: the canon in ~/.agents/elves with Claude Code's rules ~/.claude/rules/elves*.md and the global
|
|
225
226
|
rule files of Codex, Gemini CLI, Copilot, OpenCode and Amp linking to it (someone else's file stays), the skills
|
|
226
|
-
(~/.agents/skills, ~/.claude/skills)
|
|
227
|
+
(~/.agents/skills, ~/.claude/skills), the SessionStart hook and the elves command in the terminal — in ~/.local/bin or
|
|
228
|
+
the first directory of PATH inside the home directory that can be written: with --link a symlink to the clone's
|
|
229
|
+
bin/elves.ts, otherwise a shim to the machine's release (bunx ${PACKAGE}@<version>); someone else's elves there stays
|
|
230
|
+
(!), no such directory — no command, check -g names how to add one; without a terminal too — a cloud session's project hook
|
|
227
231
|
or setup script (CLAUDE_CODE_REMOTE=true) gets the canon and skills without the hook: the cloud doesn't read
|
|
228
232
|
~/.claude/settings.json. From the npm package (npm i -g / bun add -g ${PACKAGE}) — the release of its version, without
|
|
229
233
|
the network. --link — from an Elves clone: symlinks to the clone instead of copies, edits show up immediately.
|
|
@@ -242,7 +246,9 @@ mode (auto) and the language, holds the compose file to the task isolation rules
|
|
|
242
246
|
docker compose; the task-env skill), names a test that runs standards and architecture again next to test:spec (○, the
|
|
243
247
|
ready test without them) and warns (!) if a script in slot calls turbo and turbo.json doesn't pass the queue variables
|
|
244
248
|
to tasks or a line leads into a file of the flow copy that update doesn't rewrite — the file, the line number and its
|
|
245
|
-
text (the exit code stays the same);
|
|
249
|
+
text (the exit code stays the same); for the machine the elves command missing or of another release is falling
|
|
250
|
+
behind, someone else's elves in its place or one earlier on PATH — !, no directory for it — ! with the command that
|
|
251
|
+
adds ~/.local/bin to PATH; a thin project behind — update in this session's task (the package version in
|
|
246
252
|
package.json and the lockfile), a project with a copy of the flow — the move to a thin project in a task of its own: the
|
|
247
253
|
task new command of the github skill, one task per project. For the machine names the task-env proxy of *.localhost
|
|
248
254
|
addresses — running, not running or running
|
|
@@ -260,7 +266,7 @@ Examples:
|
|
|
260
266
|
update: `elves update [-g]
|
|
261
267
|
|
|
262
268
|
Brings it up to date: a --link clone — git pull --ff-only, the machine's copy — install of the latest release: the
|
|
263
|
-
canon, the skills and the
|
|
269
|
+
canon, the skills, the hook and the elves shim of that release; a project — install of the latest release: ${PACKAGE} in package.json and the
|
|
264
270
|
lockfile, a few lines in the branch of the current task; a project with a copy of the flow moves to a thin one (install
|
|
265
271
|
--help), by hand after it — a check of the project that knows the queue wrapper by the copy's path (. .agents/slot.sh
|
|
266
272
|
now), AI_DEV_SLOT* in the project's code → ELVES_SLOT*, formatting the shortened imports, git add -A before the first
|
|
@@ -1794,6 +1800,150 @@ function agentRules(home: string, has: (dir: string, cmd?: string) => boolean) {
|
|
|
1794
1800
|
}
|
|
1795
1801
|
}
|
|
1796
1802
|
|
|
1803
|
+
/**
|
|
1804
|
+
* The `elves` command in the terminal (#632): `bunx @miroshnik/elves …` and `bun <clone>/bin/elves.ts …` aren't a
|
|
1805
|
+
* command, and the global package's bin directory (`~/.bun/bin`) may not be on PATH at all — Bun from Homebrew.
|
|
1806
|
+
* Where — `commandDir`; what — with `--link` a symlink to the clone's `bin/elves.ts` (its `sh` shebang works through a
|
|
1807
|
+
* symlink), otherwise a shim to the machine's release through bunx (`commandShim`): the bunx directory is temporary,
|
|
1808
|
+
* and the command matches the canon on the machine — `update -g` rewrites it with the release. Ours (`ownCommand`) is
|
|
1809
|
+
* replaced; someone else's `elves` in that directory — a script of the user's, a global package — stays with a `!`
|
|
1810
|
+
* line, and so does an `elves` earlier on PATH that the shell runs instead. No directory for it — no command: the `!`
|
|
1811
|
+
* line of `check -g` has the command that adds `~/.local/bin` to PATH. Not in a cloud session: there the flow comes
|
|
1812
|
+
* by `npm i -g` (`CLOUD_FLOW`), and its `elves` is the command.
|
|
1813
|
+
*/
|
|
1814
|
+
const COMMAND = "elves";
|
|
1815
|
+
/** The shim's line by which the install knows its own file. */
|
|
1816
|
+
const COMMAND_MARK = "# Elves: the machine's elves command";
|
|
1817
|
+
/** What a dry run names next to the command's path. */
|
|
1818
|
+
const COMMAND_NOTE = "the elves command in the terminal";
|
|
1819
|
+
|
|
1820
|
+
/** PATH directories: absolute, in order, without repeats. */
|
|
1821
|
+
const pathDirs = () => [...new Set((process.env.PATH ?? "").split(path.delimiter).filter((d) => path.isAbsolute(d)).map((d) => path.resolve(d)))];
|
|
1822
|
+
|
|
1823
|
+
/** A directory the install can write into. */
|
|
1824
|
+
function writableDir(dir: string) {
|
|
1825
|
+
try {
|
|
1826
|
+
accessSync(dir, fsConstants.W_OK);
|
|
1827
|
+
return statSync(dir).isDirectory();
|
|
1828
|
+
} catch {
|
|
1829
|
+
return false;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/** A file the shell runs: a regular file (through symlinks) with the execute bit. */
|
|
1834
|
+
function runnable(file: string) {
|
|
1835
|
+
try {
|
|
1836
|
+
accessSync(file, fsConstants.X_OK);
|
|
1837
|
+
return statSync(file).isFile();
|
|
1838
|
+
} catch {
|
|
1839
|
+
return false;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/** Under dir, dir itself aside — by the path as given or by the real one (macOS /var → /private/var). */
|
|
1844
|
+
function inside(dir: string, p: string) {
|
|
1845
|
+
const real = (x: string) => {
|
|
1846
|
+
try {
|
|
1847
|
+
return realpathSync(x);
|
|
1848
|
+
} catch {
|
|
1849
|
+
return x;
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
return [
|
|
1853
|
+
[dir, p],
|
|
1854
|
+
[real(dir), real(p)],
|
|
1855
|
+
].some(([d, x]) => {
|
|
1856
|
+
const rel = path.relative(d!, x!);
|
|
1857
|
+
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* The install's own command: a symlink to a clone's `bin/elves.ts` — not into node_modules, that is a global package;
|
|
1863
|
+
* a clone gone or moved too — or a file with COMMAND_MARK.
|
|
1864
|
+
*/
|
|
1865
|
+
function ownCommand(file: string) {
|
|
1866
|
+
const st = lstat(file);
|
|
1867
|
+
if (st?.isSymbolicLink()) {
|
|
1868
|
+
const to = readlinkSync(file);
|
|
1869
|
+
return /(?:^|\/)bin\/elves\.ts$/.test(to) && !to.split("/").includes("node_modules");
|
|
1870
|
+
}
|
|
1871
|
+
if (!st?.isFile() || st.size > 4096) return false;
|
|
1872
|
+
try {
|
|
1873
|
+
return readFileSync(file, "utf8").includes(COMMAND_MARK);
|
|
1874
|
+
} catch {
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/**
|
|
1880
|
+
* The command's directory: where PATH already has ours; otherwise `~/.local/bin` — if PATH has it (missing — the
|
|
1881
|
+
* install creates it); otherwise the first directory of PATH inside the home directory that can be written — not
|
|
1882
|
+
* outside it: system directories need root, and the install doesn't ask for it. null — no such directory on PATH.
|
|
1883
|
+
*/
|
|
1884
|
+
function commandDir(home: string): string | null {
|
|
1885
|
+
const dirs = pathDirs();
|
|
1886
|
+
const own = dirs.find((d) => ownCommand(path.join(d, COMMAND)));
|
|
1887
|
+
if (own) return own;
|
|
1888
|
+
const mine = dirs.filter((d) => inside(home, d));
|
|
1889
|
+
const local = path.join(home, ".local/bin");
|
|
1890
|
+
if (mine.includes(local) && (!lstat(local) || writableDir(local))) return local;
|
|
1891
|
+
return mine.find(writableDir) ?? null;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/**
|
|
1895
|
+
* The shim of a machine with a copy: the release it was installed from by its npm package — the bunx package of the
|
|
1896
|
+
* same version as the canon; a release unknown — by the SHA (a clone without `--link`, a package pinned by SHA),
|
|
1897
|
+
* without it — the latest release.
|
|
1898
|
+
*/
|
|
1899
|
+
function commandShim(src: string) {
|
|
1900
|
+
const version = release ? npmVersion(release) : null;
|
|
1901
|
+
const sha = version ? null : sourceSha(src);
|
|
1902
|
+
const spec = version ? `${PACKAGE}@${version}` : sha ? `github:${SOURCE}#${sha}` : PACKAGE;
|
|
1903
|
+
const what = version ? `release ${release}` : sha ? `Elves ${short(sha)}` : "the latest release";
|
|
1904
|
+
return ["#!/bin/sh", `${COMMAND_MARK} — ${what}; set by \`${RUN} install -g\`, update -g rewrites it`, `exec bunx --silent ${spec} "$@"`, ""].join("\n");
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* No directory for the command: what the owner runs — `~/.local/bin` into PATH in the shell's profile and the same
|
|
1909
|
+
* install with it on PATH.
|
|
1910
|
+
*/
|
|
1911
|
+
function pathCommand(src: string, linkMode: boolean) {
|
|
1912
|
+
const shell = path.basename(process.env.SHELL ?? "");
|
|
1913
|
+
const profile = shell === "zsh" ? "~/.zshrc" : shell === "bash" ? "~/.bashrc" : "~/.profile";
|
|
1914
|
+
const again = linkMode ? `bun --no-env-file ${shWord(path.join(src, "bin/elves.ts"))} install -g --link` : `${RUN} update -g`;
|
|
1915
|
+
return `mkdir -p ~/.local/bin && echo 'export PATH="$HOME/.local/bin:$PATH"' >> ${profile} && PATH="$HOME/.local/bin:$PATH" ${again}`;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/** The `elves` command (see COMMAND): a dry run names what it would change and the `!` lines. */
|
|
1919
|
+
function installCommand(src: string, home: string, linkMode: boolean) {
|
|
1920
|
+
if (remote()) return;
|
|
1921
|
+
const shown = (p: string) => (inside(home, p) ? `~/${path.relative(home, p)}` : p);
|
|
1922
|
+
const say = (file: string, text: string) => (dry ? void change("!", file, { note: text }) : warn(`${shown(file)} — ${text}`));
|
|
1923
|
+
const dir = commandDir(home);
|
|
1924
|
+
if (!dir) {
|
|
1925
|
+
const text = `${COMMAND_NOTE} isn't installed: PATH has no directory inside the home directory that can be written — add ~/.local/bin: ${pathCommand(src, linkMode)}`;
|
|
1926
|
+
return dry ? void change("!", path.join(home, ".local/bin", COMMAND), { note: text }) : void note(text);
|
|
1927
|
+
}
|
|
1928
|
+
const dst = path.join(dir, COMMAND);
|
|
1929
|
+
const st = lstat(dst);
|
|
1930
|
+
if (st && !ownCommand(dst)) return say(dst, `someone else's elves (a script of the user's, a global package) — left as is; the machine's command goes here once it's removed: ${RUN} update -g`);
|
|
1931
|
+
const target = path.join(src, "bin/elves.ts");
|
|
1932
|
+
const text = linkMode ? linkText(target, dst, home) : commandShim(src);
|
|
1933
|
+
const same = linkMode ? Boolean(st?.isSymbolicLink()) && readlinkSync(dst) === text : Boolean(st?.isFile()) && runnable(dst) && readFileSync(dst, "utf8") === text;
|
|
1934
|
+
if (!same && dry) change(st ? "~" : "+", dst, { note: COMMAND_NOTE });
|
|
1935
|
+
else if (!same) {
|
|
1936
|
+
if (st) rmSync(dst);
|
|
1937
|
+
mkdirSync(dir, { recursive: true });
|
|
1938
|
+
if (linkMode) symlinkSync(text, dst);
|
|
1939
|
+
else writeFileSync(dst, text, { mode: 0o755 });
|
|
1940
|
+
note(`${shown(dst)} — ${COMMAND_NOTE}: ${linkMode ? `→ ${target}` : (/^exec (.*) "\$@"$/m.exec(text)?.[1] ?? "")}`);
|
|
1941
|
+
}
|
|
1942
|
+
const dirs = pathDirs();
|
|
1943
|
+
const first = dirs.slice(0, dirs.indexOf(dir)).map((d) => path.join(d, COMMAND)).find(runnable);
|
|
1944
|
+
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
|
+
}
|
|
1946
|
+
|
|
1797
1947
|
/**
|
|
1798
1948
|
* The URL of a GitHub origin on the repository's name before the rename (LEGACY_SOURCE, the name is case-insensitive) —
|
|
1799
1949
|
* the same URL on SOURCE: the form, the credentials, the port and `.git` stay; another repository or host — null.
|
|
@@ -1835,8 +1985,8 @@ const remote = () => process.env.CLAUDE_CODE_REMOTE === "true";
|
|
|
1835
1985
|
/**
|
|
1836
1986
|
* The machine (#557): the canon `~/.agents/elves/` — a copy or, with `--link`, a symlink to the clone — with Claude
|
|
1837
1987
|
* Code's rules `~/.claude/rules/elves*.md` (the same `installRules` and `claudeRules` as in a project: what a previous
|
|
1838
|
-
* install left under the flow's former name moves), the other agents' rule files (`agentRules`), skills
|
|
1839
|
-
* Claude Code is on the machine — `~/.claude`, `claude` on PATH or a cloud session: its project hook (`cloudHook`) or
|
|
1988
|
+
* install left under the flow's former name moves), the other agents' rule files (`agentRules`), skills, the `elves`
|
|
1989
|
+
* command in the terminal (`installCommand`) and the hook. Claude Code is on the machine — `~/.claude`, `claude` on PATH or a cloud session: its project hook (`cloudHook`) or
|
|
1840
1990
|
* its environment's setup script runs in a fresh home. The cloud doesn't read `~/.claude/settings.json` — no hook there.
|
|
1841
1991
|
*/
|
|
1842
1992
|
function installGlobal(src: string, linkMode: boolean) {
|
|
@@ -1879,6 +2029,8 @@ function installGlobal(src: string, linkMode: boolean) {
|
|
|
1879
2029
|
note(`${CONFIG} → ${priv}`);
|
|
1880
2030
|
}
|
|
1881
2031
|
}
|
|
2032
|
+
// the last step: an agent's sandbox may refuse a directory of PATH — the canon, the skills and the hook are in place
|
|
2033
|
+
installCommand(src, home, linkMode);
|
|
1882
2034
|
}
|
|
1883
2035
|
|
|
1884
2036
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
3
|
locale_default
|
|
4
|
-
} from "../../spec/scripts/harness-
|
|
4
|
+
} from "../../spec/scripts/harness-wqrt5gpa.js";
|
|
5
5
|
|
|
6
6
|
// skills/github/scripts/github.ts
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
@@ -11,12 +11,14 @@ import path from "node:path";
|
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
12
|
var N = locale_default.names(locale_default.projectLanguage());
|
|
13
13
|
var canonViews = () => [
|
|
14
|
-
{ name: N.views.board, layout: "BOARD_LAYOUT" },
|
|
15
|
-
{ name: N.views.table, layout: "TABLE_LAYOUT" },
|
|
14
|
+
{ name: N.views.board, layout: "BOARD_LAYOUT", columns: ["TITLE", STATUS, "LABELS", "SUB_ISSUES_PROGRESS"] },
|
|
15
|
+
{ name: N.views.table, layout: "TABLE_LAYOUT", columns: ["TITLE", STATUS, "LABELS", "ASSIGNEES", "SUB_ISSUES_PROGRESS", N.fields.estimate], groupBy: STATUS },
|
|
16
16
|
{ name: N.views.roadmap, layout: "ROADMAP_LAYOUT" }
|
|
17
17
|
];
|
|
18
|
+
var BUILTIN_COLUMNS = { TITLE: "Title", ASSIGNEES: "Assignees", LABELS: "Labels", SUB_ISSUES_PROGRESS: "Sub-issues progress" };
|
|
18
19
|
var STATUS = "Status";
|
|
19
20
|
var statusOptions = () => [N.status.backlog, N.status.inProgress, N.status.done];
|
|
21
|
+
var STATUS_COLORS = ["GREEN", "YELLOW", "PURPLE"];
|
|
20
22
|
var numberFields = () => [N.fields.estimate, N.fields.actual, N.fields.tokens, N.fields.cost];
|
|
21
23
|
var issueTypes = () => [N.issueTypes.task, N.issueTypes.bug, N.issueTypes.epic];
|
|
22
24
|
var PRIORITY = "Priority";
|
|
@@ -36,6 +38,12 @@ var ISSUE_LABEL_LIMIT = 100;
|
|
|
36
38
|
var WORKFLOW_ADDED = "Item added to project";
|
|
37
39
|
var WORKFLOW_CLOSED = "Item closed";
|
|
38
40
|
var WORKFLOW_AUTO_ADD = "Auto-add to project";
|
|
41
|
+
var WORKFLOW_MERGED = "Pull request merged";
|
|
42
|
+
var WORKFLOW_AUTO_CLOSE = "Auto-close issue";
|
|
43
|
+
var WORKFLOW_SUB_ISSUES = "Auto-add sub-issues to project";
|
|
44
|
+
var WORKFLOW_ARCHIVE = "Auto-archive items";
|
|
45
|
+
var WORKFLOWS = [WORKFLOW_ADDED, WORKFLOW_CLOSED, WORKFLOW_MERGED, WORKFLOW_AUTO_CLOSE, WORKFLOW_AUTO_ADD, WORKFLOW_SUB_ISSUES, WORKFLOW_ARCHIVE];
|
|
46
|
+
var ARCHIVE_FILTER = "is:issue is:closed updated:<@today-2w";
|
|
39
47
|
var DEFAULT_TEMPLATE = "miroshnik/6";
|
|
40
48
|
var RULESET = "elves";
|
|
41
49
|
var FORMER_RULESET = "ai-dev";
|
|
@@ -122,6 +130,8 @@ var Q = {
|
|
|
122
130
|
id number name layout filter
|
|
123
131
|
sortByFields(first: 5) { nodes { direction field { ... on ProjectV2FieldCommon { name } } } }
|
|
124
132
|
verticalGroupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } }
|
|
133
|
+
groupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } }
|
|
134
|
+
fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name dataType } } }
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
fields(first: 100) {
|
|
@@ -650,7 +660,9 @@ function loadProject(io, id, withItems = true) {
|
|
|
650
660
|
layout: v.layout,
|
|
651
661
|
filter: v.filter ?? null,
|
|
652
662
|
sortBy: (v.sortByFields?.nodes ?? []).map((s) => s.field?.name).filter(Boolean),
|
|
653
|
-
columnsBy: (v.verticalGroupByFields?.nodes ?? []).map((f) => f?.name).filter(Boolean)
|
|
663
|
+
columnsBy: (v.verticalGroupByFields?.nodes ?? []).map((f) => f?.name).filter(Boolean),
|
|
664
|
+
groupBy: (v.groupByFields?.nodes ?? []).map((f) => f?.name).filter(Boolean),
|
|
665
|
+
columns: (v.fields?.nodes ?? []).filter((f) => f?.id).map((f) => ({ id: f.id, name: f.name, dataType: f.dataType }))
|
|
654
666
|
})),
|
|
655
667
|
fields: p.fields.nodes.filter((f) => f && f.id).map((f) => ({ id: f.id, name: f.name, dataType: f.dataType, isIssueField: !!f.isIssueField, options: f.options ?? null })),
|
|
656
668
|
workflows: p.workflows.nodes.filter(Boolean),
|
|
@@ -692,7 +704,12 @@ function analyze(s) {
|
|
|
692
704
|
for (const other of s.repo.linked.filter((x) => x.id !== p.id)) {
|
|
693
705
|
link(`another project is linked: ${q(other.title)} (${other.url})`, { kind: "confirm", text: `unlink project ${q(other.title)} from the repository`, mutations: [{ op: "UnlinkProject", input: { projectId: other.id, repositoryId: s.repo.id } }] });
|
|
694
706
|
}
|
|
695
|
-
const
|
|
707
|
+
const fieldAlias = fieldAliases();
|
|
708
|
+
const columnKey = (f) => BUILTIN_FIELDS.has(f.dataType) ? f.dataType : fieldAlias[f.name] ?? f.name;
|
|
709
|
+
const columnName = (key) => p.fields.find((f) => columnKey(f) === key)?.name ?? BUILTIN_COLUMNS[key] ?? key;
|
|
710
|
+
const columnFields = (keys) => keys.map((k) => p.fields.find((f) => columnKey(f) === k)).filter((f) => !!f);
|
|
711
|
+
const configuration = (keys) => ({ visibleFieldIds: columnFields(keys).map((f) => f.id) });
|
|
712
|
+
const views = add("views", `Views ${canonViews().map((v) => q(v.name)).join(", ")} — the right layout, columns and grouping, no filter, no others`);
|
|
696
713
|
const canonNames = new Set(canonViews().map((v) => v.name));
|
|
697
714
|
const spare = p.views.filter((v) => !canonNames.has(v.name));
|
|
698
715
|
for (const c of canonViews()) {
|
|
@@ -703,7 +720,8 @@ function analyze(s) {
|
|
|
703
720
|
spare.splice(spare.indexOf(cand), 1);
|
|
704
721
|
views(`missing ${q(c.name)}`, guarded(`rename view ${q(cand.name)} → ${q(c.name)}`, { op: "UpdateView", input: { viewId: cand.id, name: c.name } }));
|
|
705
722
|
} else {
|
|
706
|
-
|
|
723
|
+
const input = { projectId: p.id, name: c.name, layout: c.layout, ...c.columns ? { configuration: configuration(c.columns) } : {} };
|
|
724
|
+
views(`missing ${q(c.name)}`, api(`create view ${q(c.name)} (${c.layout})`, { op: "CreateView", input }));
|
|
707
725
|
}
|
|
708
726
|
continue;
|
|
709
727
|
}
|
|
@@ -714,13 +732,26 @@ function analyze(s) {
|
|
|
714
732
|
if (c.layout === "BOARD_LAYOUT" && v.layout === c.layout && v.columnsBy[0] !== STATUS) {
|
|
715
733
|
views(`${q(c.name)} columns not by Status`, ui(`${q(c.name)}: View options → Column by → Status → Save`, `${p.url}/views/${v.number}`));
|
|
716
734
|
}
|
|
735
|
+
if (c.columns) {
|
|
736
|
+
const have = v.columns.map(columnKey);
|
|
737
|
+
const missing = c.columns.filter((k) => !have.includes(k)).map((k) => q(columnName(k)));
|
|
738
|
+
const extra = v.columns.filter((f) => !c.columns.includes(columnKey(f))).map((f) => q(f.name));
|
|
739
|
+
if (missing.length || extra.length) {
|
|
740
|
+
const why = [missing.length ? `missing ${missing.join(", ")}` : "", extra.length ? `extra ${extra.join(", ")}` : ""].filter(Boolean).join("; ");
|
|
741
|
+
const set = columnFields(c.columns).map((f) => f.name).join(", ");
|
|
742
|
+
views(`${q(c.name)} columns: ${why}`, api(`${q(c.name)}: columns ${set}`, { op: "UpdateView", input: { viewId: v.id, configuration: configuration(c.columns) } }));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (c.groupBy && v.layout === c.layout && v.groupBy[0] !== c.groupBy) {
|
|
746
|
+
views(`${q(c.name)} not grouped by ${c.groupBy}`, ui(`${q(c.name)}: View options → Group by → ${c.groupBy} → Save`, `${p.url}/views/${v.number}`));
|
|
747
|
+
}
|
|
717
748
|
}
|
|
718
749
|
for (const v of spare)
|
|
719
750
|
views(`extra ${q(v.name)}`, guarded(`delete view ${q(v.name)}`, { op: "DeleteView", input: { viewId: v.id } }));
|
|
720
|
-
const status = add("status", `${STATUS}: ${statusOptions().join(" → ")}`);
|
|
751
|
+
const status = add("status", `${STATUS}: ${statusOptions().join(" → ")} (${STATUS_COLORS.join(", ")})`);
|
|
721
752
|
const sf = p.fields.find((f) => f.name === STATUS && f.options);
|
|
722
753
|
if (!sf) {
|
|
723
|
-
status(`no ${STATUS} field`, api(`create field ${STATUS}`, { op: "CreateField", input: { projectId: p.id, dataType: "SINGLE_SELECT", name: STATUS, singleSelectOptions: statusOptions().map((name) => ({ name, color:
|
|
754
|
+
status(`no ${STATUS} field`, api(`create field ${STATUS}`, { op: "CreateField", input: { projectId: p.id, dataType: "SINGLE_SELECT", name: STATUS, singleSelectOptions: statusOptions().map((name, i) => ({ name, color: STATUS_COLORS[i], description: "" })) } }));
|
|
724
755
|
} else if (sf.options.map((o) => o.name).join(`
|
|
725
756
|
`) !== statusOptions().join(`
|
|
726
757
|
`)) {
|
|
@@ -730,15 +761,17 @@ function analyze(s) {
|
|
|
730
761
|
return i < 0 ? null : left.splice(i, 1)[0];
|
|
731
762
|
};
|
|
732
763
|
const aliases = statusAliases();
|
|
733
|
-
const next = statusOptions().map((name) => {
|
|
764
|
+
const next = statusOptions().map((name, i) => {
|
|
734
765
|
const o = take((x) => x.name === name) ?? take((x) => aliases[x.name.toLowerCase()] === name);
|
|
735
|
-
return o ? { id: o.id, name, color:
|
|
766
|
+
return o ? { id: o.id, name, color: STATUS_COLORS[i], description: o.description ?? "" } : { name, color: STATUS_COLORS[i], description: "" };
|
|
736
767
|
});
|
|
737
768
|
const drop = left.length ? `; delete ${left.map((o) => q(o.name)).join(", ")}` : "";
|
|
738
769
|
status(`options: ${sf.options.map((o) => o.name).join(", ") || "none"}`, guarded(`${STATUS}: ${statusOptions().join(", ")} (rename the existing ones keeping the id${drop})`, { op: "UpdateField", input: { fieldId: sf.id, singleSelectOptions: next } }));
|
|
770
|
+
} else if (sf.options.some((o, i) => o.color !== STATUS_COLORS[i])) {
|
|
771
|
+
const next = sf.options.map((o, i) => ({ id: o.id, name: o.name, color: STATUS_COLORS[i], description: o.description ?? "" }));
|
|
772
|
+
status(`colors: ${sf.options.map((o) => `${o.name} ${o.color}`).join(", ")}`, api(`${STATUS}: colors ${STATUS_COLORS.join(", ")} (keeping the id)`, { op: "UpdateField", input: { fieldId: sf.id, singleSelectOptions: next } }));
|
|
739
773
|
}
|
|
740
774
|
const fields = add("fields", `Fields ${numberFields().map(q).join(", ")} (Number), no other custom ones`);
|
|
741
|
-
const fieldAlias = fieldAliases();
|
|
742
775
|
const renamed = new Set;
|
|
743
776
|
for (const name of numberFields()) {
|
|
744
777
|
const f = p.fields.find((x) => x.name === name);
|
|
@@ -804,13 +837,17 @@ function analyze(s) {
|
|
|
804
837
|
types(`extra ${q(x.name)}`, { kind: "confirm", text: `disable type ${q(x.name)}`, url, mutations: [{ op: "UpdateIssueType", input: { issueTypeId: x.id, isEnabled: false } }] });
|
|
805
838
|
}
|
|
806
839
|
}
|
|
807
|
-
const wf = add("workflows", `Workflows ${
|
|
840
|
+
const wf = add("workflows", `Workflows ${WORKFLOWS.map(q).join(", ")} are on`);
|
|
808
841
|
const how = {
|
|
809
842
|
[WORKFLOW_ADDED]: `Edit → Set value → ${STATUS}: ${N.status.backlog}`,
|
|
810
843
|
[WORKFLOW_CLOSED]: `Edit → Set value → ${STATUS}: ${N.status.done}`,
|
|
811
|
-
[
|
|
844
|
+
[WORKFLOW_MERGED]: `Edit → Set value → ${STATUS}: ${N.status.done}`,
|
|
845
|
+
[WORKFLOW_AUTO_CLOSE]: `Edit → ${STATUS}: ${N.status.done}`,
|
|
846
|
+
[WORKFLOW_AUTO_ADD]: `Edit → repository ${s.repo.name}, filter is:issue is:open (Enter)`,
|
|
847
|
+
[WORKFLOW_SUB_ISSUES]: "Edit",
|
|
848
|
+
[WORKFLOW_ARCHIVE]: `Edit → filter ${ARCHIVE_FILTER} (Enter)`
|
|
812
849
|
};
|
|
813
|
-
for (const name of
|
|
850
|
+
for (const name of WORKFLOWS) {
|
|
814
851
|
const w = p.workflows.find((x) => x.name === name);
|
|
815
852
|
if (w?.enabled)
|
|
816
853
|
continue;
|
|
@@ -1280,6 +1317,7 @@ var handedOutFile = (io) => {
|
|
|
1280
1317
|
return file && `${file}.tasks`;
|
|
1281
1318
|
};
|
|
1282
1319
|
var CLOUD_HANDOUT = "@cloud";
|
|
1320
|
+
var SESSION_HANDOUT = "@session";
|
|
1283
1321
|
function handedOut(io, slug) {
|
|
1284
1322
|
const file = handedOutFile(io);
|
|
1285
1323
|
return file ? handOutsIn(file, slug) : [];
|
|
@@ -1292,7 +1330,8 @@ function handOutsIn(file, slug) {
|
|
|
1292
1330
|
return lines.filter((l) => l.startsWith(`${slug}#`)).map((l) => {
|
|
1293
1331
|
const [number, ...rest] = l.slice(slug.length + 1).split(" ");
|
|
1294
1332
|
const worktree = rest.join(" ").trim();
|
|
1295
|
-
|
|
1333
|
+
const executor = worktree === CLOUD_HANDOUT ? "cloud" : worktree === SESSION_HANDOUT ? "session" : "subagent";
|
|
1334
|
+
return { number: Number(number), worktree: executor === "subagent" ? worktree || null : null, executor };
|
|
1296
1335
|
}).filter((h) => Number.isInteger(h.number) && h.number > 0);
|
|
1297
1336
|
}
|
|
1298
1337
|
function realPath(p) {
|
|
@@ -1809,8 +1848,10 @@ function cmdTaskQueue(io, slug, o) {
|
|
|
1809
1848
|
const pr = openPr(i);
|
|
1810
1849
|
const work = [status === N.status.inProgress ? q(N.status.inProgress) : "", pr === undefined ? "" : `open PR #${pr} https://github.com/${slug}/pull/${pr}`].filter(Boolean);
|
|
1811
1850
|
const hand = handed.find((h) => h.number === i.number);
|
|
1812
|
-
if (!work.length && hand?.cloud)
|
|
1851
|
+
if (!work.length && hand?.executor === "cloud")
|
|
1813
1852
|
work.push(`handed out to a cloud executor of this session, its wait — a background command: bash ${waitCi()} issue ${i.number}`);
|
|
1853
|
+
else if (!work.length && hand?.executor === "session")
|
|
1854
|
+
work.push("handed out to an app executor session of this session (start_session) — the end of each of its turns comes as the app's note");
|
|
1814
1855
|
else if (!work.length && hand)
|
|
1815
1856
|
work.push("handed out to an executor of this session");
|
|
1816
1857
|
if (work.length) {
|
|
@@ -2079,7 +2120,7 @@ function taskBranchesHere(io, slug, number) {
|
|
|
2079
2120
|
const refs = [...git("for-each-ref", "--format=%(refname)", "refs/heads/"), ...git("ls-remote", "--heads", "origin").map((l) => l.split("\t")[1] ?? "")];
|
|
2080
2121
|
return [...new Set(refs.map((r) => r.trim().replace(/^refs\/heads\//, "")).filter((b) => isTaskBranch(b, number)))].sort();
|
|
2081
2122
|
}
|
|
2082
|
-
function cmdTaskAssignment(io, slug, number, o = {
|
|
2123
|
+
function cmdTaskAssignment(io, slug, number, o = {}) {
|
|
2083
2124
|
const issue = loadIssue(io, slug, number);
|
|
2084
2125
|
const file = handedOutFile(io);
|
|
2085
2126
|
const own = pinnedTask(io)?.task;
|
|
@@ -2096,7 +2137,7 @@ function cmdTaskAssignment(io, slug, number, o = { cloud: false }) {
|
|
|
2096
2137
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
2097
2138
|
writeFileSync(path.join(path.dirname(file), ".gitignore"), `*
|
|
2098
2139
|
`);
|
|
2099
|
-
const where = o.cloud ? CLOUD_HANDOUT : worktree;
|
|
2140
|
+
const where = o.cloud ? CLOUD_HANDOUT : o.session ? SESSION_HANDOUT : worktree;
|
|
2100
2141
|
appendFileSync(file, `${slug}#${number}${where ? ` ${where}` : ""}
|
|
2101
2142
|
`);
|
|
2102
2143
|
}
|
|
@@ -2114,11 +2155,17 @@ function cmdTaskAssignment(io, slug, number, o = { cloud: false }) {
|
|
|
2114
2155
|
inProgress: N.status.inProgress,
|
|
2115
2156
|
done: N.lastLine.done,
|
|
2116
2157
|
questions: N.sections.questions,
|
|
2117
|
-
branches: restart.map((b) => `\`${b}\``).join(", ")
|
|
2158
|
+
branches: restart.map((b) => `\`${b}\``).join(", "),
|
|
2159
|
+
doneQuestions: N.lastLine.questions,
|
|
2160
|
+
remaining: N.lastLine.remaining
|
|
2118
2161
|
};
|
|
2119
|
-
const
|
|
2162
|
+
const resume = restart.length ? [N.queue.resume] : [];
|
|
2163
|
+
const lines = o.session ? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.sessionFound, N.queue.sessionQuestion, N.queue.sessionEnd] : [N.queue.lead, ...resume, N.queue.subagent, N.queue.session, N.queue.wait, N.queue.git, N.queue.subagents, N.queue.live, N.queue.refresh, N.queue.found, N.queue.question, N.queue.answer];
|
|
2120
2164
|
for (const line of lines)
|
|
2121
2165
|
io.out(locale_default.fill(line, vars));
|
|
2166
|
+
if (o.session) {
|
|
2167
|
+
io.out(`Next: the lines above — the executor session's prompt as is: start_session, initiation "user_asked", cwd — the folder of the ${slug} checkout on this machine (not a worktree), ` + `use_worktree true, background — the first line above; keep the session's id it returns. ` + `The end of each of its turns comes as the app's note, like an executor's answer — task queue again: in a queue of ${slug} the task is =; ${SESSION_ARCHIVE}.`);
|
|
2168
|
+
}
|
|
2122
2169
|
return 0;
|
|
2123
2170
|
}
|
|
2124
2171
|
function actualizeChecks() {
|
|
@@ -2308,7 +2355,8 @@ function cmdTaskClose(io, slug, number, o) {
|
|
|
2308
2355
|
const prs = [...issue.prs, ...fresh].map((p) => `#${p.number} ${p.state === "OPEN" ? "open" : "closed without a merge"}`).join(", ");
|
|
2309
2356
|
const what = prs ? `PR ${prs}` : `the task is ${issue.state === "OPEN" ? "open" : "closed without being done"}, no merged PR`;
|
|
2310
2357
|
const checked = found ? `; checked: the PR–task link, the branch <type>/${number}-<slug> and «Closes #${number}» in the body of the last ${RECENT_PRS} PRs into ${found.base}` : "";
|
|
2311
|
-
|
|
2358
|
+
const noPr = !prs && issue.state === "OPEN" ? `; a task without a PR (a data edit, a reconciliation, research) — close the issue as completed with a result comment: gh issue close ${number} --repo ${slug} --reason completed --comment "<result>", then task close ${number} again` : "";
|
|
2359
|
+
throw new GhError(`#${number} is too early to close: ${what}${checked} — close after the PR is merged or the task is closed${noPr}`);
|
|
2312
2360
|
}
|
|
2313
2361
|
if (issue.parent) {
|
|
2314
2362
|
const epic = loadTaskText(io, slug, issue.parent.number);
|
|
@@ -3121,7 +3169,7 @@ that can't be continued, in the worktree of its task's branch.
|
|
|
3121
3169
|
Examples:
|
|
3122
3170
|
github task resume`,
|
|
3123
3171
|
"task queue": `github task queue [--epic N] [--type Task|Bug] [--priority Urgent,High] [--repo owner/repo]
|
|
3124
|
-
github task queue --assignment <N> [--cloud] [--repo owner/repo]
|
|
3172
|
+
github task queue --assignment <N> [--cloud | --session] [--repo owner/repo]
|
|
3125
3173
|
|
|
3126
3174
|
The dispatcher session's queue: ready tasks (open, not an epic, not in work, no open blockers — other repositories'
|
|
3127
3175
|
too —, no external blocker label) by Priority, then number, each with its executor's prompt; the rest — in work (=: In
|
|
@@ -3140,13 +3188,17 @@ such a subagent at work or HEAD moved in the last ${QUIET_MINUTES} min — code
|
|
|
3140
3188
|
dispatcher wait, no hand-out. --cloud — printed by the dispatcher: the brief of a cloud executor (claude.ai/code) —
|
|
3141
3189
|
whole, # only at its task — and the dispatcher's steps: a cloud session can't message a local one, so the dispatcher
|
|
3142
3190
|
waits for the task's closing (ci-wait issue N, background) and closes it locally; the hand-out has no worktree, and the
|
|
3143
|
-
queue's = line names that wait.
|
|
3191
|
+
queue's = line names that wait. --session — printed by the dispatcher: the prompt of an app executor session
|
|
3192
|
+
(start_session in the folder of another project, its own worktree) — a session by its project's canon and mode, named
|
|
3193
|
+
by the task, waiting in the background, ending with the canon's last line — and how to start it; the hand-out has no
|
|
3194
|
+
worktree, the task is = in the queue.
|
|
3144
3195
|
|
|
3145
3196
|
Examples:
|
|
3146
3197
|
github task queue --epic 40
|
|
3147
3198
|
github task queue --type Bug --priority Urgent,High
|
|
3148
3199
|
github task queue --assignment 42
|
|
3149
|
-
github task queue --assignment 42 --cloud
|
|
3200
|
+
github task queue --assignment 42 --cloud
|
|
3201
|
+
github task queue --assignment 42 --session`,
|
|
3150
3202
|
"task close": `github task close <N> [--no-git] [--repo owner/repo]
|
|
3151
3203
|
|
|
3152
3204
|
After the PR merge and the deploy (or a close without a PR) in one call: the actual (est fact --write), Status Done, the
|
|
@@ -3299,6 +3351,7 @@ function main(argv, io) {
|
|
|
3299
3351
|
"no-git": { type: "boolean", default: false },
|
|
3300
3352
|
assignment: { type: "boolean", default: false },
|
|
3301
3353
|
cloud: { type: "boolean", default: false },
|
|
3354
|
+
session: { type: "boolean", default: false },
|
|
3302
3355
|
auto: { type: "boolean", default: false }
|
|
3303
3356
|
}
|
|
3304
3357
|
});
|
|
@@ -3324,10 +3377,14 @@ function main(argv, io) {
|
|
|
3324
3377
|
if (cmd === "queue") {
|
|
3325
3378
|
if (values.cloud && !values.assignment)
|
|
3326
3379
|
throw new GhError("--cloud — only with --assignment <N>: the brief of one task's cloud executor");
|
|
3380
|
+
if (values.session && !values.assignment)
|
|
3381
|
+
throw new GhError("--session — only with --assignment <N>: the prompt of one task's app executor session");
|
|
3382
|
+
if (values.cloud && values.session)
|
|
3383
|
+
throw new GhError("--cloud or --session, not both: a cloud executor or an app session");
|
|
3327
3384
|
if (values.assignment) {
|
|
3328
3385
|
if (positionals.length !== 1)
|
|
3329
3386
|
throw new GhError("task queue --assignment <N>: one task number");
|
|
3330
|
-
return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud });
|
|
3387
|
+
return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud, session: values.session });
|
|
3331
3388
|
}
|
|
3332
3389
|
if (positionals.length)
|
|
3333
3390
|
throw new GhError(`extra arguments: ${positionals.join(" ")} — the queue is chosen by --epic, --type and --priority`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
locale_default
|
|
3
|
-
} from "./harness-
|
|
3
|
+
} from "./harness-wqrt5gpa.js";
|
|
4
4
|
import {
|
|
5
5
|
invariant2,
|
|
6
6
|
codeOnly2,
|
|
7
7
|
envNamesInCode2
|
|
8
|
-
} from "./harness-
|
|
8
|
+
} from "./harness-ay62ws50.js";
|
|
9
9
|
|
|
10
10
|
// skills/spec/scripts/architecture.ts
|
|
11
11
|
import { readdirSync, readFileSync } from "node:fs";
|
|
@@ -91,6 +91,7 @@ var en_default = {
|
|
|
91
91
|
git: "- git — in separate calls, without `cd`, `&&` and `|`: worktree isolation refuses a compound command with git;",
|
|
92
92
|
subagents: "- writing subagents — launch as needed, no limit: each gets its own worktree and «{task} #{number}» at the start of its prompt (`est` links its work by it); bring their commits into your branch, remove their worktrees and branches yourself after the merge — what didn't go, name in the answer;",
|
|
93
93
|
live: "- while any of your subagents works, don't answer: wait for their answers — a background subagent wakes you with its answer; an answer with live subagents looks like a failure, and the dispatcher would give the task to a second executor next to you. Can't wait — answer `waits for subagents: <which and what they do>`: it isn't `failed`, there will be no second executor;",
|
|
94
|
+
refresh: '- the epic refresh — the subagent `task actualize` prints, without a worktree of its own (in yours) and not in the background (`run_in_background: false`): its answer — in the same turn, then `{script} task close {number}`, and only then the answer to the dispatcher: once you answer, a worktree without changes is removed, and a background subagent is left without a directory; a task without a PR (a data edit, a reconciliation, research) — first close the issue as completed with a result comment: `gh issue close {number} --repo {repo} --reason completed --comment "<result>"`;',
|
|
94
95
|
found: "- work found outside the task — `{script} task new …` (work of the task's epic — with `--epic`: the dispatcher picks it up) — name it in the answer: whether to queue it is the owner's call;",
|
|
95
96
|
question: "- a question without a clear recommendation — into «{questions}» of the task; don't start the code that depends on it and answer the dispatcher: the owner's answer comes back here.",
|
|
96
97
|
answer: "The answer to the dispatcher — one line: `merged #<PR>` · `question: <the question, options, recommendation>` · `waits for subagents: <which and what they do>` · `failed: <what stopped and where>`.",
|
|
@@ -100,7 +101,14 @@ var en_default = {
|
|
|
100
101
|
cloudDoc: "- the cloud — by `docs/cloud-sessions.md` of the canon, read it first: of the github skill's scripts `task show`, `task answer` and `task new` work there (over REST), the rest refuse, the scenarios section — by editing the description over REST, project fields aren't set from the cloud; the branch is the one the cloud assigned, don't rename it;",
|
|
101
102
|
cloudClose: "- after the merge — its section «Closing a task from the cloud»: the «{actual}» comment of `est fact {number}` and a «{state}» comment with what the cloud can't do — both into the task;",
|
|
102
103
|
cloudDispatcher: "- the dispatcher waits for the task to close and closes it locally — the actual, the status, the merged branch, archiving this session: don't message it, don't archive the session;",
|
|
103
|
-
cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task."
|
|
104
|
+
cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task.",
|
|
105
|
+
sessionLead: "{task} #{number} «{title}» in {repo}{epic}: you are an executor session of the dispatcher session — do the task whole by the canon of this project, in its mode (`check` prints it; outside auto — commit, push and merge on the owner's «yes»): the task's questions, `{script} task status {number} \"{inProgress}\" --branch <type>/{number}-<slug>`, the branch from a fresh origin/main, code and tests, PR, green CI, `pr premerge`, merge, deploy, the epic refresh, `{script} task close {number}`.",
|
|
106
|
+
sessionIs: "You are a session, not a subagent:",
|
|
107
|
+
sessionName: "- name the session `#{number} {title}` (`set_session_title`) once the task is taken — `est` links the session to the task by it;",
|
|
108
|
+
sessionWait: "- wait for CI and the deploy with a background command — `{waitCi} pr <PR>` (after the merge — `merged <PR>`): its completion wakes the session; no `Monitor`, watcher loop or `ScheduleWakeup` on top;",
|
|
109
|
+
sessionFound: "- work found outside the task — `{script} task new …` and the question «queue it?» by the canon; work of the task's epic — with `--epic` and without an executor of yours: the dispatcher's queue picks it up;",
|
|
110
|
+
sessionQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session; don't start the code that depends on it: the answer comes here, from the owner or the dispatcher;",
|
|
111
|
+
sessionEnd: "- the end — the canon's last line («{done}» · «{doneQuestions} …» · «{remaining} …»), in auto mode after «{done}» — archiving the session by the canon: the dispatcher reads the end of each of your turns as your answer."
|
|
104
112
|
},
|
|
105
113
|
est: {
|
|
106
114
|
answer: "Answer",
|
|
@@ -541,6 +549,7 @@ var ru_default = {
|
|
|
541
549
|
git: "- git — отдельными вызовами, без `cd`, `&&` и `|`: составную команду с git изоляция worktree не пропускает;",
|
|
542
550
|
subagents: "- пишущих субагентов запускай по необходимости, без лимита: каждому — свой worktree и «{task} #{number}» в начале промпта (по нему `est` привяжет его работу); их коммиты перенеси в свою ветку, их worktree и ветки после мержа убери сам — что не убралось, назови в ответе;",
|
|
543
551
|
live: "- пока работает хоть один твой субагент, ответ не отдавай: жди их ответов — фоновый субагент будит тебя своим ответом; ответ при живых субагентах выглядит провалом, и диспетчер отдал бы задачу второму исполнителю рядом с тобой. Ждать нельзя — ответь `ждёт субагентов: <какие и что делают>`: это не «сломалось», второго исполнителя не будет;",
|
|
552
|
+
refresh: '- актуализация эпика — субагент, которого печатает `task actualize`, без своего worktree (в твоём) и не в фоне (`run_in_background: false`): его ответ — в том же ходе, затем `{script} task close {number}`, и только потом ответ диспетчеру: стоит тебе ответить, worktree без изменений убирают, и фоновый субагент остаётся без каталога; у задачи без PR (правка данных, сверка, исследование) — сначала закрой issue как выполненную с итоговым комментарием: `gh issue close {number} --repo {repo} --reason completed --comment "<итог>"`;',
|
|
544
553
|
found: "- найденная работа вне задачи — `{script} task new …` (работа эпика задачи — с `--epic`: диспетчер её подхватит) — назови её в ответе: ставить ли её в очередь, решает владелец;",
|
|
545
554
|
question: "- вопрос без ясной рекомендации — в «{questions}» задачи; код, который от него зависит, не начинай и ответь диспетчеру: ответ владельца придёт сюда же.",
|
|
546
555
|
answer: "Ответ диспетчеру — одной строкой: `влито #<PR>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
|
|
@@ -550,7 +559,14 @@ var ru_default = {
|
|
|
550
559
|
cloudDoc: "- облако — по `docs/cloud-sessions.md` канона, прочитай его первым: из скриптов скилла github там работают `task show`, `task answer` и `task new` (через REST), остальные отказывают, раздел сценариев — правкой описания через REST, поля проекта из облака не ставятся; ветка — та, что назначило облако, не переименовывай;",
|
|
551
560
|
cloudClose: "- после мержа — его раздел «Closing a task from the cloud»: комментарий «{actual}» от `est fact {number}` и комментарий «{state}» с тем, что из облака не сделать, — оба в задачу;",
|
|
552
561
|
cloudDispatcher: "- диспетчер ждёт закрытия задачи и закрывает её локально — факт, статус, влитая ветка, архив этой сессии: сообщать ему не нужно, сессию не архивируй;",
|
|
553
|
-
cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу."
|
|
562
|
+
cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу.",
|
|
563
|
+
sessionLead: '{task} #{number} «{title}» в {repo}{epic}: ты сессия-исполнитель сессии-диспетчера — сделай задачу целиком по канону этого проекта, в его режиме (его печатает `check`; не в auto — коммит, пуш и мерж по «да» владельца): вопросы задачи, `{script} task status {number} "{inProgress}" --branch <тип>/{number}-<slug>`, ветка от свежего origin/main, код и тесты, PR, зелёный CI, `pr premerge`, мерж, деплой, актуализация эпика, `{script} task close {number}`.',
|
|
564
|
+
sessionIs: "Ты сессия, а не субагент:",
|
|
565
|
+
sessionName: "- назови сессию `#{number} {title}` (`set_session_title`), как только взял задачу, — по имени `est` привяжет сессию к задаче;",
|
|
566
|
+
sessionWait: "- CI и деплой жди фоновой командой — `{waitCi} pr <PR>` (после мержа — `merged <PR>`): её завершение будит сессию; без `Monitor`, цикла-наблюдателя и `ScheduleWakeup` поверх;",
|
|
567
|
+
sessionFound: "- найденная работа вне задачи — `{script} task new …` и вопрос «ставить в очередь?» по канону; работа эпика задачи — с `--epic` и без твоего исполнителя: её подхватит очередь диспетчера;",
|
|
568
|
+
sessionQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии; код, который от него зависит, не начинай: ответ придёт сюда же — от владельца или диспетчера;",
|
|
569
|
+
sessionEnd: "- конец — последняя строка канона («{done}» · «{doneQuestions} …» · «{remaining} …»), в режиме auto после «{done}» — архивация сессии по канону: конец каждого твоего хода диспетчер читает как твой ответ."
|
|
554
570
|
},
|
|
555
571
|
est: {
|
|
556
572
|
answer: "Ответ",
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
envNamesInCode2,
|
|
24
24
|
dotenvNames2,
|
|
25
25
|
envVars2
|
|
26
|
-
} from "./harness-
|
|
27
|
-
import"./harness-
|
|
26
|
+
} from "./harness-ay62ws50.js";
|
|
27
|
+
import"./harness-wqrt5gpa.js";
|
|
28
28
|
export {
|
|
29
29
|
ENV_SERVICE2 as ENV_SERVICE,
|
|
30
30
|
codeFiles2 as codeFiles,
|