@miroshnik/elves 2026.1019.0 → 2026.1019.2
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 +8 -7
- package/README.md +2 -2
- package/bin/elves.ts +159 -7
- package/dist/github/scripts/github.js +71 -20
- package/dist/spec/scripts/architecture.js +2 -2
- package/dist/spec/scripts/{harness-ay62ws50.js → harness-bpyj4s91.js} +1 -1
- package/dist/spec/scripts/{harness-wqrt5gpa.js → harness-tdg4dnq3.js} +14 -4
- package/dist/spec/scripts/harness.js +2 -2
- package/package.json +2 -2
- package/skills/github/locales/en.json +7 -2
- package/skills/github/locales/ru.json +7 -2
- package/skills/github/reference.md +68 -30
- package/skills/github/scripts/github.ts +105 -23
- package/skills/github/scripts/locale.ts +10 -0
package/AGENTS.md
CHANGED
|
@@ -199,9 +199,9 @@ it.
|
|
|
199
199
|
PR's CI — fast checks on the merge with it; red — I fix the root cause;
|
|
200
200
|
`main` red by itself (exit code 3) — no merge and no fix in the PR: one bug
|
|
201
201
|
for it, I wait for it to close.
|
|
202
|
-
- A merge that deploys — I wait for the deploy on the merge SHA (`ci-wait`)
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
- A merge that deploys — I wait for the deploy on the merge SHA (`ci-wait`)
|
|
203
|
+
**before `github task close`**. Others' branches, stacks and unmerged work
|
|
204
|
+
I don't touch.
|
|
205
205
|
- Mechanics — the `github` skill, "Git, PRs and merging — mechanics";
|
|
206
206
|
waiting for checks and deploys — `docs/pr-checks.md`.
|
|
207
207
|
|
|
@@ -222,8 +222,9 @@ folder's `exceptions/`, with an `#issue`, a reason and a ratchet. Test names
|
|
|
222
222
|
are statements in the project language; a code task gets the scenarios
|
|
223
223
|
section (future `it` names) before code. The PR body is `spec-diff` output;
|
|
224
224
|
docs are generated (`spec-doc`) and published by CI to the `spec` branch. A
|
|
225
|
-
recurring decision — a "Standard · …" issue and a rule in `tests/standards
|
|
226
|
-
The full canon and wiring — the `spec`
|
|
225
|
+
recurring decision — a "Standard · …" issue and a rule in `tests/standards/`;
|
|
226
|
+
repeats in a PR's files — fixed in it. The full canon and wiring — the `spec`
|
|
227
|
+
skill (`canon.md`, `checks.md`).
|
|
227
228
|
|
|
228
229
|
## Tests and code
|
|
229
230
|
|
|
@@ -282,8 +283,8 @@ The full canon and wiring — the `spec` skill (`canon.md`, `checks.md`).
|
|
|
282
283
|
skill) at `<service>.<N>.<project>.localhost`, not on a host port; others'
|
|
283
284
|
stacks and busy ports I don't touch — `docs/parallel-checkouts.md`.
|
|
284
285
|
- **A subagent gets a unit of ~50 turns** (one decision's folder, one file,
|
|
285
|
-
one spec), not a 200-turn zone:
|
|
286
|
-
|
|
286
|
+
one spec), not a 200-turn zone: with the same ≈ 100k-token preamble its
|
|
287
|
+
price is quadratic in turns. The assignment has the task
|
|
287
288
|
number after the word for "task" or at the start, the epic after the word
|
|
288
289
|
for "epic" ("Language") — so `est` links its work. **Fewer turns:** a file
|
|
289
290
|
in one full read, independent tool calls in one turn.
|
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-tdg4dnq3.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;
|
|
@@ -1750,11 +1787,16 @@ function cloudBrief(io, slug, issue) {
|
|
|
1750
1787
|
state: N.comments.state,
|
|
1751
1788
|
questions: N.sections.questions
|
|
1752
1789
|
};
|
|
1753
|
-
for (const line of [N.queue.cloud, N.queue.cloudDoc, N.queue.cloudClose, N.queue.cloudDispatcher, N.queue.cloudQuestion])
|
|
1790
|
+
for (const line of [N.queue.cloud, N.queue.cloudDoc, N.queue.repeats, N.queue.cloudClose, N.queue.cloudDispatcher, N.queue.cloudQuestion])
|
|
1754
1791
|
io.out(locale_default.fill(line, vars));
|
|
1755
1792
|
io.out(`Next: the lines above — the cloud session's first prompt as is, the session started from the project folder (the cloud starts from main); ` + `then as a background command: bash ${waitCi()} issue ${number} — a cloud session can't message a local one (docs/cloud-sessions.md), the task's closing is the signal: ` + `PASS — bun --no-env-file ${import.meta.path} task close ${number} (the actual with --write, ${STATUS}, the merged branch), then archive the executor's session in an auto project where the agent has the tool; ` + `TIMEOUT — the task's ${q(N.sections.questions)} and its ${q(N.comments.state)} comment, the executor's session, then the same wait again`);
|
|
1756
1793
|
}
|
|
1757
1794
|
var SESSION_ARCHIVE = "an executor session whose task closed — the dispatcher archives it in an auto project where the agent has the tool, " + "not one still working in the background or awaiting the owner's answer; outside auto the owner closes it";
|
|
1795
|
+
function openPrOf(io, slug) {
|
|
1796
|
+
const [owner, name] = slug.split("/");
|
|
1797
|
+
const recent = (graphql(io, Q.RecentPrs, { owner, name })?.repository?.pullRequests?.nodes ?? []).filter((p) => p.state === "OPEN");
|
|
1798
|
+
return (i) => i.prs.find((p) => p.state === "OPEN")?.number ?? recent.find((p) => isTaskBranch(p.headRefName ?? "", i.number) || closingInBody(p.body ?? "").includes(i.number))?.number;
|
|
1799
|
+
}
|
|
1758
1800
|
function cmdTaskQueue(io, slug, o) {
|
|
1759
1801
|
const want = o.type === undefined ? undefined : typeAliases()[o.type.toLowerCase()];
|
|
1760
1802
|
if (o.type !== undefined && (!want || want === N.issueTypes.epic))
|
|
@@ -1780,9 +1822,7 @@ function cmdTaskQueue(io, slug, o) {
|
|
|
1780
1822
|
io.out("○ the search gave 100 open tasks — the queue sees only them: narrow it with --epic");
|
|
1781
1823
|
numbers = found.filter((x) => x?.state === "OPEN").map((x) => x.number);
|
|
1782
1824
|
}
|
|
1783
|
-
const
|
|
1784
|
-
const recent = (graphql(io, Q.RecentPrs, { owner, name })?.repository?.pullRequests?.nodes ?? []).filter((p) => p.state === "OPEN");
|
|
1785
|
-
const openPr = (i) => i.prs.find((p) => p.state === "OPEN")?.number ?? recent.find((p) => isTaskBranch(p.headRefName ?? "", i.number) || closingInBody(p.body ?? "").includes(i.number))?.number;
|
|
1825
|
+
const openPr = openPrOf(io, slug);
|
|
1786
1826
|
const handed = handedOut(io, slug);
|
|
1787
1827
|
const states = new Map;
|
|
1788
1828
|
const isOpen = (n) => {
|
|
@@ -2123,7 +2163,7 @@ function cmdTaskAssignment(io, slug, number, o = {}) {
|
|
|
2123
2163
|
remaining: N.lastLine.remaining
|
|
2124
2164
|
};
|
|
2125
2165
|
const resume = restart.length ? [N.queue.resume] : [];
|
|
2126
|
-
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];
|
|
2166
|
+
const lines = o.session ? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.repeats, 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.repeats, N.queue.found, N.queue.question, N.queue.answer];
|
|
2127
2167
|
for (const line of lines)
|
|
2128
2168
|
io.out(locale_default.fill(line, vars));
|
|
2129
2169
|
if (o.session) {
|
|
@@ -2270,6 +2310,7 @@ function cmdTaskActualize(io, slug, number, o = { assignment: false }) {
|
|
|
2270
2310
|
refreshMechanics(io, slug, { number, merged: prs.length > 0, epic, open });
|
|
2271
2311
|
if (!open.length)
|
|
2272
2312
|
return io.out(`○ refresh not needed: epic #${epic.number} has no open tasks besides #${number}. ${later}`), 0;
|
|
2313
|
+
const ctx = o.assignment ? taskContext(io, slug) : null;
|
|
2273
2314
|
const vars = { task: N.subagentPrompt.task, epicWord: N.subagentPrompt.epic, number, epic: epic.number, repo: slug };
|
|
2274
2315
|
const lead = locale_default.fill(N.actualize.lead, vars);
|
|
2275
2316
|
if (!o.assignment) {
|
|
@@ -2287,8 +2328,18 @@ function cmdTaskActualize(io, slug, number, o = { assignment: false }) {
|
|
|
2287
2328
|
lines.push(locale_default.fill(N.actualize.files, { pr: p.number }), ...files.map(fileLine));
|
|
2288
2329
|
}
|
|
2289
2330
|
lines.push("", locale_default.fill(N.actualize.epic, { ...vars, title: epic.title }), textBlock(epic.body), "", N.actualize.openTasks);
|
|
2331
|
+
const openPr = openPrOf(io, slug);
|
|
2332
|
+
const target = (n) => {
|
|
2333
|
+
const i = loadIssue(io, slug, n);
|
|
2334
|
+
const status = itemIn(ctx, i)?.status ?? null;
|
|
2335
|
+
const pr = openPr(i);
|
|
2336
|
+
const work = [status === N.status.inProgress ? locale_default.fill(N.actualize.taskStatus, { status }) : "", pr === undefined ? "" : locale_default.fill(N.actualize.taskPr, { pr })].filter(Boolean);
|
|
2337
|
+
if (work.length)
|
|
2338
|
+
return [locale_default.fill(N.actualize.inWork, { work: work.join(", ") })];
|
|
2339
|
+
return status === N.status.backlog ? [locale_default.fill(N.actualize.backlog, { status: locale_default.fill(N.actualize.taskStatus, { status }) })] : [];
|
|
2340
|
+
};
|
|
2290
2341
|
for (const t of open)
|
|
2291
|
-
lines.push("", locale_default.fill(N.actualize.openTask, { number: t.number, title: t.title }), textBlock(t.body));
|
|
2342
|
+
lines.push("", locale_default.fill(N.actualize.openTask, { number: t.number, title: t.title }), ...target(t.number), textBlock(t.body));
|
|
2292
2343
|
const copies = linkCopies(epic, open);
|
|
2293
2344
|
if (copies.length)
|
|
2294
2345
|
lines.push("", N.actualize.copies, ...copies.map((c) => locale_default.fill(N.actualize.copy, c)));
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
locale_default
|
|
3
|
-
} from "./harness-
|
|
3
|
+
} from "./harness-tdg4dnq3.js";
|
|
4
4
|
import {
|
|
5
5
|
invariant2,
|
|
6
6
|
codeOnly2,
|
|
7
7
|
envNamesInCode2
|
|
8
|
-
} from "./harness-
|
|
8
|
+
} from "./harness-bpyj4s91.js";
|
|
9
9
|
|
|
10
10
|
// skills/spec/scripts/architecture.ts
|
|
11
11
|
import { readdirSync, readFileSync } from "node:fs";
|
|
@@ -64,6 +64,10 @@ var en_default = {
|
|
|
64
64
|
epic: "## Epic #{epic} «{title}»",
|
|
65
65
|
openTasks: "## The epic's open tasks — its sub-issues, not a list in the epic's description",
|
|
66
66
|
openTask: "### #{number} «{title}»",
|
|
67
|
+
backlog: "{status} — a repeat finding of the same kind (the same helper, module or standard) goes here as an item in «What to do» (and a scenario, if needed), not into a new task.",
|
|
68
|
+
inWork: "{work} — the task is already going: a repeat finding of the same kind isn't appended here but filed as a new task.",
|
|
69
|
+
taskStatus: "Status «{status}»",
|
|
70
|
+
taskPr: "PR #{pr} is open",
|
|
67
71
|
noBody: "(no description)",
|
|
68
72
|
copies: "## Copies of links in text — remove them: GitHub keeps the links (sub-issue, blocked by)",
|
|
69
73
|
copy: "- #{number}: «{line}»",
|
|
@@ -71,8 +75,8 @@ var en_default = {
|
|
|
71
75
|
commentAnswer: "- #{number}, question {k}: «{text}» — {url}",
|
|
72
76
|
checks: "## Checks by meaning — yours (canon — reference.md of the github skill, «Epic refresh on task close»); the mechanics — blocked by on done tasks, the questions label without open questions, milestones without open tasks — the script did",
|
|
73
77
|
scope: "You change only GitHub tasks — description, «{questions}», links, status; the github skill script — `{script}`: not done — `task drop <N>`, a new one — `task new … --branch-type <type> --epic {epic}` (it sets the estimate itself); an answer at a question — `task answer <N> <k> \"<answer>\"`. Don't touch code, branches or git; don't close #{number}, epic #{epic} or their milestones — task close does that after you.",
|
|
74
|
-
comment: "The result — always, as the last step: a comment on epic #{epic} (`gh issue comment {epic} --repo {repo}`), one line «{report}» — after the colon what you fixed, nothing to fix — «the block is current». By it `task close {number}` sees the refresh is over: without it the close refuses.",
|
|
75
|
-
answer: "Answer — fixes one per line with task numbers and questions to the owner, if any; otherwise «The block is current».",
|
|
78
|
+
comment: "The result — always, as the last step: a comment on epic #{epic} (`gh issue comment {epic} --repo {repo}`), one line «{report}» — after the colon what you fixed, a repeat finding — with where it went: «appended to #N» or «filed #M»; nothing to fix — «the block is current». By it `task close {number}` sees the refresh is over: without it the close refuses.",
|
|
79
|
+
answer: "Answer — fixes one per line with task numbers (a repeat finding — «appended to #N» or «filed #M») and questions to the owner, if any; otherwise «The block is current».",
|
|
76
80
|
linkWords: "blocked by|blocks|after|depends on|waits for",
|
|
77
81
|
report: "Refresh after #{number}, result: {text}",
|
|
78
82
|
mechanics: "Refresh after #{number}, script: {fixes}.",
|
|
@@ -92,6 +96,7 @@ var en_default = {
|
|
|
92
96
|
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
97
|
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
98
|
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>"`;',
|
|
99
|
+
repeats: "- before the PR — repeats in the files the task changes anyway: fix them in the same PR, don't file a task for them; a repeat outside the task's files is the epic refresh's;",
|
|
95
100
|
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;",
|
|
96
101
|
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.",
|
|
97
102
|
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>`.",
|
|
@@ -522,6 +527,10 @@ var ru_default = {
|
|
|
522
527
|
epic: "## Эпик #{epic} «{title}»",
|
|
523
528
|
openTasks: "## Открытые задачи эпика — его подзадачи (sub-issues), а не список в описании эпика",
|
|
524
529
|
openTask: "### #{number} «{title}»",
|
|
530
|
+
backlog: "{status} — находку-повтор того же рода (тот же помощник, модуль или стандарт) дописать сюда пунктом в «Что сделать» (и сценарий, если нужен), новую задачу не заводить.",
|
|
531
|
+
inWork: "{work} — задача уже идёт: находку-повтор того же рода сюда не дописывать, а завести новой задачей.",
|
|
532
|
+
taskStatus: "Статус «{status}»",
|
|
533
|
+
taskPr: "PR #{pr} открыт",
|
|
525
534
|
noBody: "(описания нет)",
|
|
526
535
|
copies: "## Копии связей в тексте — убрать: связи держит GitHub (sub-issue, blocked by)",
|
|
527
536
|
copy: "- #{number}: «{line}»",
|
|
@@ -529,8 +538,8 @@ var ru_default = {
|
|
|
529
538
|
commentAnswer: "- #{number}, вопрос {k}: «{text}» — {url}",
|
|
530
539
|
checks: "## Проверки по смыслу — твои (канон — reference.md скилла github, «Epic refresh on task close»); механику — blocked by на выполненных задачах, метку вопросов без открытых вопросов, milestone без открытых задач — сделал скрипт",
|
|
531
540
|
scope: 'Меняешь только задачи GitHub — описание, «{questions}», связи, статус; скрипт скилла github — `{script}`: без выполнения — `task drop <N>`, новая — `task new … --branch-type <тип> --epic {epic}` (оценку ставит сама); ответ у вопроса — `task answer <N> <k> "<ответ>"`. Код, ветки и git не трогать; #{number}, эпик #{epic} и их milestone не закрывать — это сделает task close после тебя.',
|
|
532
|
-
comment: "Итог — всегда, последним шагом: комментарий к эпику #{epic} (`gh issue comment {epic} --repo {repo}`) одной строкой «{report}» — после двоеточия что поправил,
|
|
533
|
-
answer: "Ответ — правки по строке с номерами задач и вопросы владельцу, если есть; иначе «Блок актуален».",
|
|
541
|
+
comment: "Итог — всегда, последним шагом: комментарий к эпику #{epic} (`gh issue comment {epic} --repo {repo}`) одной строкой «{report}» — после двоеточия что поправил, находку-повтор — с тем, куда она ушла: «дописано в #N» или «заведена #M»; править нечего — «блок актуален». По нему `task close {number}` видит, что актуализация закончена: без него закрытие откажет.",
|
|
542
|
+
answer: "Ответ — правки по строке с номерами задач (находка-повтор — «дописано в #N» или «заведена #M») и вопросы владельцу, если есть; иначе «Блок актуален».",
|
|
534
543
|
linkWords: "после|зависит от|заблокирована|заблокирован|заблокировано|блокирует|ждёт|ждет",
|
|
535
544
|
report: "Актуализация после #{number}, итог: {text}",
|
|
536
545
|
mechanics: "Актуализация после #{number}, скрипт: {fixes}.",
|
|
@@ -550,6 +559,7 @@ var ru_default = {
|
|
|
550
559
|
subagents: "- пишущих субагентов запускай по необходимости, без лимита: каждому — свой worktree и «{task} #{number}» в начале промпта (по нему `est` привяжет его работу); их коммиты перенеси в свою ветку, их worktree и ветки после мержа убери сам — что не убралось, назови в ответе;",
|
|
551
560
|
live: "- пока работает хоть один твой субагент, ответ не отдавай: жди их ответов — фоновый субагент будит тебя своим ответом; ответ при живых субагентах выглядит провалом, и диспетчер отдал бы задачу второму исполнителю рядом с тобой. Ждать нельзя — ответь `ждёт субагентов: <какие и что делают>`: это не «сломалось», второго исполнителя не будет;",
|
|
552
561
|
refresh: '- актуализация эпика — субагент, которого печатает `task actualize`, без своего worktree (в твоём) и не в фоне (`run_in_background: false`): его ответ — в том же ходе, затем `{script} task close {number}`, и только потом ответ диспетчеру: стоит тебе ответить, worktree без изменений убирают, и фоновый субагент остаётся без каталога; у задачи без PR (правка данных, сверка, исследование) — сначала закрой issue как выполненную с итоговым комментарием: `gh issue close {number} --repo {repo} --reason completed --comment "<итог>"`;',
|
|
562
|
+
repeats: "- перед PR — повторы в файлах, которые задача и так правит: исправь в том же PR, новую задачу на них не заводи; повтор вне файлов задачи — дело актуализации эпика;",
|
|
553
563
|
found: "- найденная работа вне задачи — `{script} task new …` (работа эпика задачи — с `--epic`: диспетчер её подхватит) — назови её в ответе: ставить ли её в очередь, решает владелец;",
|
|
554
564
|
question: "- вопрос без ясной рекомендации — в «{questions}» задачи; код, который от него зависит, не начинай и ответь диспетчеру: ответ владельца придёт сюда же.",
|
|
555
565
|
answer: "Ответ диспетчеру — одной строкой: `влито #<PR>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
envNamesInCode2,
|
|
24
24
|
dotenvNames2,
|
|
25
25
|
envVars2
|
|
26
|
-
} from "./harness-
|
|
27
|
-
import"./harness-
|
|
26
|
+
} from "./harness-bpyj4s91.js";
|
|
27
|
+
import"./harness-tdg4dnq3.js";
|
|
28
28
|
export {
|
|
29
29
|
ENV_SERVICE2 as ENV_SERVICE,
|
|
30
30
|
codeFiles2 as codeFiles,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miroshnik/elves",
|
|
3
|
-
"version": "2026.1019.
|
|
3
|
+
"version": "2026.1019.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Elves — a development flow with AI agents: shared rules and skills for any agent; install — bunx @miroshnik/elves install -g; the installer and skill scripts — TypeScript under Bun, tests — bun test in tests/",
|
|
6
6
|
"repository": {
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"eslint-plugin-boundaries": "^7.2.0",
|
|
72
72
|
"knip": "^6.41.0"
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "006208c32b61d184886a5b77dbdffa39bba751f2"
|
|
75
75
|
}
|
|
@@ -60,6 +60,10 @@
|
|
|
60
60
|
"epic": "## Epic #{epic} «{title}»",
|
|
61
61
|
"openTasks": "## The epic's open tasks — its sub-issues, not a list in the epic's description",
|
|
62
62
|
"openTask": "### #{number} «{title}»",
|
|
63
|
+
"backlog": "{status} — a repeat finding of the same kind (the same helper, module or standard) goes here as an item in «What to do» (and a scenario, if needed), not into a new task.",
|
|
64
|
+
"inWork": "{work} — the task is already going: a repeat finding of the same kind isn't appended here but filed as a new task.",
|
|
65
|
+
"taskStatus": "Status «{status}»",
|
|
66
|
+
"taskPr": "PR #{pr} is open",
|
|
63
67
|
"noBody": "(no description)",
|
|
64
68
|
"copies": "## Copies of links in text — remove them: GitHub keeps the links (sub-issue, blocked by)",
|
|
65
69
|
"copy": "- #{number}: «{line}»",
|
|
@@ -67,8 +71,8 @@
|
|
|
67
71
|
"commentAnswer": "- #{number}, question {k}: «{text}» — {url}",
|
|
68
72
|
"checks": "## Checks by meaning — yours (canon — reference.md of the github skill, «Epic refresh on task close»); the mechanics — blocked by on done tasks, the questions label without open questions, milestones without open tasks — the script did",
|
|
69
73
|
"scope": "You change only GitHub tasks — description, «{questions}», links, status; the github skill script — `{script}`: not done — `task drop <N>`, a new one — `task new … --branch-type <type> --epic {epic}` (it sets the estimate itself); an answer at a question — `task answer <N> <k> \"<answer>\"`. Don't touch code, branches or git; don't close #{number}, epic #{epic} or their milestones — task close does that after you.",
|
|
70
|
-
"comment": "The result — always, as the last step: a comment on epic #{epic} (`gh issue comment {epic} --repo {repo}`), one line «{report}» — after the colon what you fixed, nothing to fix — «the block is current». By it `task close {number}` sees the refresh is over: without it the close refuses.",
|
|
71
|
-
"answer": "Answer — fixes one per line with task numbers and questions to the owner, if any; otherwise «The block is current».",
|
|
74
|
+
"comment": "The result — always, as the last step: a comment on epic #{epic} (`gh issue comment {epic} --repo {repo}`), one line «{report}» — after the colon what you fixed, a repeat finding — with where it went: «appended to #N» or «filed #M»; nothing to fix — «the block is current». By it `task close {number}` sees the refresh is over: without it the close refuses.",
|
|
75
|
+
"answer": "Answer — fixes one per line with task numbers (a repeat finding — «appended to #N» or «filed #M») and questions to the owner, if any; otherwise «The block is current».",
|
|
72
76
|
"linkWords": "blocked by|blocks|after|depends on|waits for",
|
|
73
77
|
"report": "Refresh after #{number}, result: {text}",
|
|
74
78
|
"mechanics": "Refresh after #{number}, script: {fixes}.",
|
|
@@ -88,6 +92,7 @@
|
|
|
88
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;",
|
|
89
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;",
|
|
90
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>\"`;",
|
|
95
|
+
"repeats": "- before the PR — repeats in the files the task changes anyway: fix them in the same PR, don't file a task for them; a repeat outside the task's files is the epic refresh's;",
|
|
91
96
|
"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;",
|
|
92
97
|
"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.",
|
|
93
98
|
"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>`.",
|
|
@@ -60,6 +60,10 @@
|
|
|
60
60
|
"epic": "## Эпик #{epic} «{title}»",
|
|
61
61
|
"openTasks": "## Открытые задачи эпика — его подзадачи (sub-issues), а не список в описании эпика",
|
|
62
62
|
"openTask": "### #{number} «{title}»",
|
|
63
|
+
"backlog": "{status} — находку-повтор того же рода (тот же помощник, модуль или стандарт) дописать сюда пунктом в «Что сделать» (и сценарий, если нужен), новую задачу не заводить.",
|
|
64
|
+
"inWork": "{work} — задача уже идёт: находку-повтор того же рода сюда не дописывать, а завести новой задачей.",
|
|
65
|
+
"taskStatus": "Статус «{status}»",
|
|
66
|
+
"taskPr": "PR #{pr} открыт",
|
|
63
67
|
"noBody": "(описания нет)",
|
|
64
68
|
"copies": "## Копии связей в тексте — убрать: связи держит GitHub (sub-issue, blocked by)",
|
|
65
69
|
"copy": "- #{number}: «{line}»",
|
|
@@ -67,8 +71,8 @@
|
|
|
67
71
|
"commentAnswer": "- #{number}, вопрос {k}: «{text}» — {url}",
|
|
68
72
|
"checks": "## Проверки по смыслу — твои (канон — reference.md скилла github, «Epic refresh on task close»); механику — blocked by на выполненных задачах, метку вопросов без открытых вопросов, milestone без открытых задач — сделал скрипт",
|
|
69
73
|
"scope": "Меняешь только задачи GitHub — описание, «{questions}», связи, статус; скрипт скилла github — `{script}`: без выполнения — `task drop <N>`, новая — `task new … --branch-type <тип> --epic {epic}` (оценку ставит сама); ответ у вопроса — `task answer <N> <k> \"<ответ>\"`. Код, ветки и git не трогать; #{number}, эпик #{epic} и их milestone не закрывать — это сделает task close после тебя.",
|
|
70
|
-
"comment": "Итог — всегда, последним шагом: комментарий к эпику #{epic} (`gh issue comment {epic} --repo {repo}`) одной строкой «{report}» — после двоеточия что поправил,
|
|
71
|
-
"answer": "Ответ — правки по строке с номерами задач и вопросы владельцу, если есть; иначе «Блок актуален».",
|
|
74
|
+
"comment": "Итог — всегда, последним шагом: комментарий к эпику #{epic} (`gh issue comment {epic} --repo {repo}`) одной строкой «{report}» — после двоеточия что поправил, находку-повтор — с тем, куда она ушла: «дописано в #N» или «заведена #M»; править нечего — «блок актуален». По нему `task close {number}` видит, что актуализация закончена: без него закрытие откажет.",
|
|
75
|
+
"answer": "Ответ — правки по строке с номерами задач (находка-повтор — «дописано в #N» или «заведена #M») и вопросы владельцу, если есть; иначе «Блок актуален».",
|
|
72
76
|
"linkWords": "после|зависит от|заблокирована|заблокирован|заблокировано|блокирует|ждёт|ждет",
|
|
73
77
|
"report": "Актуализация после #{number}, итог: {text}",
|
|
74
78
|
"mechanics": "Актуализация после #{number}, скрипт: {fixes}.",
|
|
@@ -88,6 +92,7 @@
|
|
|
88
92
|
"subagents": "- пишущих субагентов запускай по необходимости, без лимита: каждому — свой worktree и «{task} #{number}» в начале промпта (по нему `est` привяжет его работу); их коммиты перенеси в свою ветку, их worktree и ветки после мержа убери сам — что не убралось, назови в ответе;",
|
|
89
93
|
"live": "- пока работает хоть один твой субагент, ответ не отдавай: жди их ответов — фоновый субагент будит тебя своим ответом; ответ при живых субагентах выглядит провалом, и диспетчер отдал бы задачу второму исполнителю рядом с тобой. Ждать нельзя — ответь `ждёт субагентов: <какие и что делают>`: это не «сломалось», второго исполнителя не будет;",
|
|
90
94
|
"refresh": "- актуализация эпика — субагент, которого печатает `task actualize`, без своего worktree (в твоём) и не в фоне (`run_in_background: false`): его ответ — в том же ходе, затем `{script} task close {number}`, и только потом ответ диспетчеру: стоит тебе ответить, worktree без изменений убирают, и фоновый субагент остаётся без каталога; у задачи без PR (правка данных, сверка, исследование) — сначала закрой issue как выполненную с итоговым комментарием: `gh issue close {number} --repo {repo} --reason completed --comment \"<итог>\"`;",
|
|
95
|
+
"repeats": "- перед PR — повторы в файлах, которые задача и так правит: исправь в том же PR, новую задачу на них не заводи; повтор вне файлов задачи — дело актуализации эпика;",
|
|
91
96
|
"found": "- найденная работа вне задачи — `{script} task new …` (работа эпика задачи — с `--epic`: диспетчер её подхватит) — назови её в ответе: ставить ли её в очередь, решает владелец;",
|
|
92
97
|
"question": "- вопрос без ясной рекомендации — в «{questions}» задачи; код, который от него зависит, не начинай и ответь диспетчеру: ответ владельца придёт сюда же.",
|
|
93
98
|
"answer": "Ответ диспетчеру — одной строкой: `влито #<PR>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
|
|
@@ -97,9 +97,15 @@ Every repository has **exactly one** GitHub Project linked to the repo
|
|
|
97
97
|
per epic/area (an area = an epic inside the shared project). All tasks are
|
|
98
98
|
added to the project right on creation (`github task new`). Required:
|
|
99
99
|
|
|
100
|
-
- **Three views**, with exactly these names
|
|
101
|
-
|
|
102
|
-
-
|
|
100
|
+
- **Three views**, with exactly these names, as in the template:
|
|
101
|
+
- `Board` (BOARD_LAYOUT, columns by `Status`) — cards show Title, Status,
|
|
102
|
+
Labels, Sub-issues progress;
|
|
103
|
+
- `Table` (TABLE_LAYOUT, grouped by `Status`) — columns Title, Status,
|
|
104
|
+
Labels, Assignees, Sub-issues progress, «Estimate, h»;
|
|
105
|
+
- `Roadmap` (ROADMAP_LAYOUT).
|
|
106
|
+
- **The `Status` field** with options `Backlog` → `In progress` → `Done`,
|
|
107
|
+
colored `GREEN`, `YELLOW`, `PURPLE` (a new GitHub project's colors);
|
|
108
|
+
option descriptions are the project's own.
|
|
103
109
|
I change the status **myself, immediately**, without waiting to be asked
|
|
104
110
|
(`github task status <N> <status>`):
|
|
105
111
|
- a new task — I set Backlog on creation;
|
|
@@ -118,12 +124,15 @@ added to the project right on creation (`github task new`). Required:
|
|
|
118
124
|
(those closed as not planned don't count — they are no longer in the
|
|
119
125
|
project).
|
|
120
126
|
Safety net — the project's built-in workflows "Item added to project" →
|
|
121
|
-
Backlog, "Item closed"
|
|
122
|
-
the
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
Backlog, "Item closed" and "Pull request merged" → Done, "Auto-close issue"
|
|
128
|
+
(Done → the issue is closed), "Auto-add to project" (new open issues of
|
|
129
|
+
the repository get into the project by themselves) and "Auto-add sub-issues
|
|
130
|
+
to project": the status gets set even if there is no one to set it (a cloud
|
|
131
|
+
session, another agent, closing from the GitHub UI). The agent sets the
|
|
132
|
+
status anyway; a task closed as not planned it removes from the project —
|
|
133
|
+
otherwise the workflow puts it into Done. "Auto-archive items" (`is:issue
|
|
134
|
+
is:closed updated:<@today-2w`) takes a task closed and untouched for two
|
|
135
|
+
weeks off the board and the table.
|
|
127
136
|
- **The `Estimate, h` field** (Number) — the agent's active hours
|
|
128
137
|
(`AGENTS.md`, section "Estimate and actual — the `est` skill"). Set **only
|
|
129
138
|
by `github task new`** (the `est` script inside) — from analogs with
|
|
@@ -149,8 +158,11 @@ added to the project right on creation (`github task new`). Required:
|
|
|
149
158
|
unblocked. A personal account has no issue fields — no priority is kept.
|
|
150
159
|
`github task new` sets it (`--priority`, `Medium` by default).
|
|
151
160
|
|
|
152
|
-
Nothing extra: there are no other fields (Iteration, Readiness…) or
|
|
153
|
-
until they are added here.
|
|
161
|
+
Nothing extra: there are no other fields (Iteration, Readiness…), views or
|
|
162
|
+
columns until they are added here. What the public API doesn't read — the
|
|
163
|
+
slice by a field, the sub-issue hierarchy, column widths, the project's
|
|
164
|
+
description and README — isn't in the canon: a copy of the template carries
|
|
165
|
+
it, nothing checks it. Readiness — the status and *blocked by*; stages —
|
|
154
166
|
milestones (the `Milestone` field).
|
|
155
167
|
|
|
156
168
|
The project — the `github` skill: `project check` once per session before
|
|
@@ -234,12 +246,20 @@ line `○ refresh not needed`, no subagent. Five checks:
|
|
|
234
246
|
of subtasks in the epic, an order, "⛔" or "after #N" marks — is removed.
|
|
235
247
|
5. **Repetition is a standard.** A decision that, in what shipped, repeats an
|
|
236
248
|
existing one in two or more places — a `Standard · …` issue
|
|
237
|
-
(a rule in `tests/standards/` and a refactoring)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
the
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
(a rule in `tests/standards/` and a refactoring); a repeat in the files a
|
|
250
|
+
task changes anyway is fixed in its PR by its executor, without a task.
|
|
251
|
+
First — an open task of the epic of the same kind (the same helper, module
|
|
252
|
+
or standard) in Backlog: the finding is an item in its «What to do» (and a
|
|
253
|
+
scenario, if needed), not a new task; one In progress or with an open PR is
|
|
254
|
+
already going — a new task (the assignment names each one's status). Then
|
|
255
|
+
a search among the open `Standard · …`: one exists — don't file another; an
|
|
256
|
+
extension of a neighboring one — a comment in it. The result names where
|
|
257
|
+
the finding went: «appended to #N» or «filed #M». A standard's priority —
|
|
258
|
+
by the cost of the mistake it prevents; standards of one area — as an epic:
|
|
259
|
+
otherwise they are born faster than they are worked off (#636: seven tasks
|
|
260
|
+
in a day on one helper's copies, each refresh finding the next). Something
|
|
261
|
+
deferred as "not in this PR" — an issue or dropped explicitly, not
|
|
262
|
+
forgotten.
|
|
243
263
|
|
|
244
264
|
What was fixed — one comment on the epic, the history of decisions in one
|
|
245
265
|
place; it is the subagent's last step, always: nothing to fix — «the block is
|
|
@@ -313,16 +333,23 @@ description is rewritten.
|
|
|
313
333
|
|
|
314
334
|
1. The project is linked to the repository, it is the only one, its title =
|
|
315
335
|
the repository name.
|
|
316
|
-
2. Views `Board` (BOARD, columns by `Status`), `Table` (TABLE
|
|
317
|
-
(ROADMAP), filters empty, no others
|
|
318
|
-
|
|
336
|
+
2. Views `Board` (BOARD, columns by `Status`), `Table` (TABLE, grouped by
|
|
337
|
+
`Status`), `Roadmap` (ROADMAP), filters empty, no others; the columns of
|
|
338
|
+
`Board` and `Table` — the canon's set (a built-in field by its type, the
|
|
339
|
+
rest by name), `fix` sets them in the template's order. The roadmap's
|
|
340
|
+
columns aren't compared: the API gives it the default ones.
|
|
341
|
+
3. `Status`: `Backlog` → `In progress` → `Done`, colors `GREEN`, `YELLOW`,
|
|
342
|
+
`PURPLE`; descriptions aren't compared.
|
|
319
343
|
4. Fields «Estimate, h», «Actual, h», «Tokens, M», «Cost, $» (Number), no
|
|
320
344
|
custom fields beyond the canon.
|
|
321
345
|
5. Organization only: `Priority` — the organization's issue field, connected,
|
|
322
346
|
`Table` and `Board` are sorted by it; issue types — exactly «Task»,
|
|
323
347
|
«Bug», «Epic». A personal account has none — a `➖` line.
|
|
324
|
-
6.
|
|
325
|
-
"
|
|
348
|
+
6. The template's workflows are on: "Item added to project" (→ Backlog),
|
|
349
|
+
"Item closed" and "Pull request merged" (→ Done), "Auto-close issue",
|
|
350
|
+
"Auto-add to project" (`is:issue is:open`), "Auto-add sub-issues to
|
|
351
|
+
project", "Auto-archive items" (`is:issue is:closed updated:<@today-2w`).
|
|
352
|
+
Others aren't checked.
|
|
326
353
|
7. Closed tasks — in Done; those closed without being done (not planned,
|
|
327
354
|
duplicate) — outside the project.
|
|
328
355
|
8. The repository's open tasks — in the project, each has a `Status`.
|
|
@@ -379,7 +406,8 @@ description is rewritten.
|
|
|
379
406
|
### fix: who does what
|
|
380
407
|
|
|
381
408
|
- **API — the script itself:** linking, a template copy or a new project,
|
|
382
|
-
missing views and fields, view layout, removing a filter,
|
|
409
|
+
missing views and fields, view layout and columns, removing a filter,
|
|
410
|
+
`Status` options and colors,
|
|
383
411
|
the `Priority` issue field, task statuses, adding open tasks, removing tasks
|
|
384
412
|
closed without being done from the project, decision labels (creation,
|
|
385
413
|
color), the default-branch ruleset (creation, editing its own), merge
|
|
@@ -409,14 +437,19 @@ description is rewritten.
|
|
|
409
437
|
deleted (options were replaced without ids): pick it again.
|
|
410
438
|
- "Item added to project": trigger `issue, pull request` (default), Set
|
|
411
439
|
value → `Status: Backlog`.
|
|
412
|
-
- "Item closed": Set value → `Status: Done`.
|
|
440
|
+
- "Item closed", "Pull request merged": Set value → `Status: Done`.
|
|
441
|
+
- "Auto-close issue": `Status: Done` → close the issue.
|
|
413
442
|
- "Auto-add to project": the repository — this project's, filter
|
|
414
443
|
`is:issue is:open`, Enter — the "See N existing items" counter should
|
|
415
444
|
update. Without `is:open` an updated closed task (a not planned one too)
|
|
416
445
|
comes back into the project. Auto-add doesn't add already existing tasks —
|
|
417
446
|
`fix` adds them.
|
|
418
|
-
-
|
|
419
|
-
|
|
447
|
+
- "Auto-add sub-issues to project": nothing to set — Save and turn on.
|
|
448
|
+
- "Auto-archive items": filter `is:issue is:closed updated:<@today-2w`,
|
|
449
|
+
Enter.
|
|
450
|
+
- **Sorting by Priority, board columns, table grouping:** the view → the
|
|
451
|
+
`View` button on the right → `Sort by: Priority` / `Column by: Status` /
|
|
452
|
+
`Group by: Status` → `Save view` → the dialog
|
|
420
453
|
"Save display options for …?" → `Save`. Escape closes the dialog without
|
|
421
454
|
saving: the sorting stays only in the URL (`?sortedBy…`), and the tab's
|
|
422
455
|
unsaved marker lies — `check` verifies the result.
|
|
@@ -435,8 +468,9 @@ description is rewritten.
|
|
|
435
468
|
Done and leaves a step to check the target in the UI.
|
|
436
469
|
- **`Status` options are renamed keeping the same id**
|
|
437
470
|
(`updateProjectV2Field`, `singleSelectOptions[].id`): task values and the
|
|
438
|
-
"Item closed" target are kept. Replacing options without
|
|
439
|
-
task statuses vanish, "Item closed" sets the deleted
|
|
471
|
+
"Item closed" target are kept, recoloring too. Replacing options without
|
|
472
|
+
ids gives new ids — task statuses vanish, "Item closed" sets the deleted
|
|
473
|
+
`Done`.
|
|
440
474
|
- **The template** — the elves project (`miroshnik/6`): everything the API
|
|
441
475
|
lacks is configured there once. A copy (`copyProjectV2`) carries over views
|
|
442
476
|
with their settings, fields and configured workflows, except auto-add; it
|
|
@@ -444,12 +478,16 @@ description is rewritten.
|
|
|
444
478
|
is unavailable (someone else's account) or `--template none` — the project is
|
|
445
479
|
created from scratch (`createProjectV2`) and brought up via the API:
|
|
446
480
|
`Todo / In Progress / Done` → the canon with the same ids, `View 1` →
|
|
447
|
-
`Table`, the rest is created.
|
|
481
|
+
`Table`, the rest is created; the table's grouping is a UI step.
|
|
448
482
|
- **The view filter is empty:** `iteration:@current` without an Iteration field
|
|
449
483
|
silently hides tasks from the board.
|
|
450
484
|
- **`Priority` is the organization's issue field** (`isIssueField`), not a
|
|
451
485
|
project field of the same name. The API reads the sorting (`sortByFields`)
|
|
452
|
-
but doesn't set it; the board grouping too
|
|
486
|
+
but doesn't set it; the board grouping and the table grouping too
|
|
487
|
+
(`verticalGroupByFields`, `groupByFields`).
|
|
488
|
+
- **A view's columns are set** (`updateProjectV2View`,
|
|
489
|
+
`configuration.visibleFieldIds` in order) and read (`fields` of the view; a
|
|
490
|
+
roadmap without configured columns gives the default ones).
|
|
453
491
|
- **A project that has been used** — it has tasks.
|
|
454
492
|
|
|
455
493
|
## Task and PR commands — details
|
|
@@ -57,13 +57,27 @@ type Any = any;
|
|
|
57
57
|
*/
|
|
58
58
|
let N: Names = locale.names(locale.projectLanguage());
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Views as in the template: columns by `columnKey` in the template's order, the table grouped by Status. The roadmap's
|
|
62
|
+
* columns aren't compared: the API gives it the default ones, it has no configured columns.
|
|
63
|
+
*/
|
|
64
|
+
interface CanonView {
|
|
65
|
+
name: string;
|
|
66
|
+
layout: string;
|
|
67
|
+
columns?: string[];
|
|
68
|
+
groupBy?: string;
|
|
69
|
+
}
|
|
70
|
+
const canonViews = (): CanonView[] => [
|
|
71
|
+
{ name: N.views.board, layout: "BOARD_LAYOUT", columns: ["TITLE", STATUS, "LABELS", "SUB_ISSUES_PROGRESS"] },
|
|
72
|
+
{ name: N.views.table, layout: "TABLE_LAYOUT", columns: ["TITLE", STATUS, "LABELS", "ASSIGNEES", "SUB_ISSUES_PROGRESS", N.fields.estimate], groupBy: STATUS },
|
|
63
73
|
{ name: N.views.roadmap, layout: "ROADMAP_LAYOUT" },
|
|
64
74
|
];
|
|
75
|
+
/** A built-in column's name when the project has no such field yet (a column key is its type). */
|
|
76
|
+
const BUILTIN_COLUMNS: Record<string, string> = { TITLE: "Title", ASSIGNEES: "Assignees", LABELS: "Labels", SUB_ISSUES_PROGRESS: "Sub-issues progress" };
|
|
65
77
|
export const STATUS = "Status";
|
|
66
78
|
const statusOptions = () => [N.status.backlog, N.status.inProgress, N.status.done];
|
|
79
|
+
/** The template's colors of the Status options, in order — GitHub's colors of a new project's Todo / In Progress / Done. */
|
|
80
|
+
const STATUS_COLORS = ["GREEN", "YELLOW", "PURPLE"];
|
|
67
81
|
const numberFields = () => [N.fields.estimate, N.fields.actual, N.fields.tokens, N.fields.cost];
|
|
68
82
|
const issueTypes = () => [N.issueTypes.task, N.issueTypes.bug, N.issueTypes.epic];
|
|
69
83
|
export const PRIORITY = "Priority";
|
|
@@ -98,6 +112,14 @@ const ISSUE_LABEL_LIMIT = 100;
|
|
|
98
112
|
export const WORKFLOW_ADDED = "Item added to project";
|
|
99
113
|
export const WORKFLOW_CLOSED = "Item closed";
|
|
100
114
|
export const WORKFLOW_AUTO_ADD = "Auto-add to project";
|
|
115
|
+
const WORKFLOW_MERGED = "Pull request merged";
|
|
116
|
+
const WORKFLOW_AUTO_CLOSE = "Auto-close issue";
|
|
117
|
+
const WORKFLOW_SUB_ISSUES = "Auto-add sub-issues to project";
|
|
118
|
+
const WORKFLOW_ARCHIVE = "Auto-archive items";
|
|
119
|
+
/** The template's workflows, all on. */
|
|
120
|
+
const WORKFLOWS = [WORKFLOW_ADDED, WORKFLOW_CLOSED, WORKFLOW_MERGED, WORKFLOW_AUTO_CLOSE, WORKFLOW_AUTO_ADD, WORKFLOW_SUB_ISSUES, WORKFLOW_ARCHIVE];
|
|
121
|
+
/** The template's auto-archive filter: a task closed and untouched for two weeks leaves the board and the table. */
|
|
122
|
+
const ARCHIVE_FILTER = "is:issue is:closed updated:<@today-2w";
|
|
101
123
|
/** The template: a new project is its copy (views, fields, configured workflows except auto-add). */
|
|
102
124
|
export const DEFAULT_TEMPLATE = "miroshnik/6";
|
|
103
125
|
/**
|
|
@@ -203,6 +225,9 @@ interface View {
|
|
|
203
225
|
filter: string | null;
|
|
204
226
|
sortBy: string[];
|
|
205
227
|
columnsBy: string[];
|
|
228
|
+
groupBy: string[];
|
|
229
|
+
/** The visible fields (`fields` of the view). */
|
|
230
|
+
columns: { id: string; name: string; dataType: string }[];
|
|
206
231
|
}
|
|
207
232
|
interface Option {
|
|
208
233
|
id: string;
|
|
@@ -388,6 +413,8 @@ export const Q = {
|
|
|
388
413
|
id number name layout filter
|
|
389
414
|
sortByFields(first: 5) { nodes { direction field { ... on ProjectV2FieldCommon { name } } } }
|
|
390
415
|
verticalGroupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } }
|
|
416
|
+
groupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } }
|
|
417
|
+
fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name dataType } } }
|
|
391
418
|
}
|
|
392
419
|
}
|
|
393
420
|
fields(first: 100) {
|
|
@@ -1014,6 +1041,8 @@ function loadProject(io: Io, id: string, withItems = true): Project {
|
|
|
1014
1041
|
filter: v.filter ?? null,
|
|
1015
1042
|
sortBy: (v.sortByFields?.nodes ?? []).map((s: Any) => s.field?.name).filter(Boolean),
|
|
1016
1043
|
columnsBy: (v.verticalGroupByFields?.nodes ?? []).map((f: Any) => f?.name).filter(Boolean),
|
|
1044
|
+
groupBy: (v.groupByFields?.nodes ?? []).map((f: Any) => f?.name).filter(Boolean),
|
|
1045
|
+
columns: (v.fields?.nodes ?? []).filter((f: Any) => f?.id).map((f: Any) => ({ id: f.id, name: f.name, dataType: f.dataType })),
|
|
1017
1046
|
})),
|
|
1018
1047
|
fields: p.fields.nodes
|
|
1019
1048
|
.filter((f: Any) => f && f.id)
|
|
@@ -1066,8 +1095,17 @@ export function analyze(s: State): Check[] {
|
|
|
1066
1095
|
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 } }] });
|
|
1067
1096
|
}
|
|
1068
1097
|
|
|
1098
|
+
// Columns: a built-in field by its type, the rest by name; a numeric field in another dictionary language is the same
|
|
1099
|
+
// column — the Fields item renames it keeping the id.
|
|
1100
|
+
const fieldAlias = fieldAliases();
|
|
1101
|
+
const columnKey = (f: { name: string; dataType: string }) => (BUILTIN_FIELDS.has(f.dataType) ? f.dataType : (fieldAlias[f.name] ?? f.name));
|
|
1102
|
+
const columnName = (key: string) => p.fields.find((f) => columnKey(f) === key)?.name ?? BUILTIN_COLUMNS[key] ?? key;
|
|
1103
|
+
// a field the project doesn't have yet is skipped: the step's text changes when it appears, and the next round sets it
|
|
1104
|
+
const columnFields = (keys: string[]) => keys.map((k) => p.fields.find((f) => columnKey(f) === k)).filter((f): f is Field => !!f);
|
|
1105
|
+
const configuration = (keys: string[]) => ({ visibleFieldIds: columnFields(keys).map((f) => f.id) });
|
|
1106
|
+
|
|
1069
1107
|
// Views: by name; missing — rename an extra one of the same layout or create one; extra ones — delete.
|
|
1070
|
-
const views = add("views", `Views ${canonViews().map((v) => q(v.name)).join(", ")} — the right layout, no filter, no others`);
|
|
1108
|
+
const views = add("views", `Views ${canonViews().map((v) => q(v.name)).join(", ")} — the right layout, columns and grouping, no filter, no others`);
|
|
1071
1109
|
const canonNames = new Set<string>(canonViews().map((v) => v.name));
|
|
1072
1110
|
const spare = p.views.filter((v) => !canonNames.has(v.name));
|
|
1073
1111
|
for (const c of canonViews()) {
|
|
@@ -1078,7 +1116,8 @@ export function analyze(s: State): Check[] {
|
|
|
1078
1116
|
spare.splice(spare.indexOf(cand), 1);
|
|
1079
1117
|
views(`missing ${q(c.name)}`, guarded(`rename view ${q(cand.name)} → ${q(c.name)}`, { op: "UpdateView", input: { viewId: cand.id, name: c.name } }));
|
|
1080
1118
|
} else {
|
|
1081
|
-
|
|
1119
|
+
const input = { projectId: p.id, name: c.name, layout: c.layout, ...(c.columns ? { configuration: configuration(c.columns) } : {}) };
|
|
1120
|
+
views(`missing ${q(c.name)}`, api(`create view ${q(c.name)} (${c.layout})`, { op: "CreateView", input }));
|
|
1082
1121
|
}
|
|
1083
1122
|
continue;
|
|
1084
1123
|
}
|
|
@@ -1087,14 +1126,29 @@ export function analyze(s: State): Check[] {
|
|
|
1087
1126
|
if (c.layout === "BOARD_LAYOUT" && v.layout === c.layout && v.columnsBy[0] !== STATUS) {
|
|
1088
1127
|
views(`${q(c.name)} columns not by Status`, ui(`${q(c.name)}: View options → Column by → Status → Save`, `${p.url}/views/${v.number}`));
|
|
1089
1128
|
}
|
|
1129
|
+
if (c.columns) {
|
|
1130
|
+
const have = v.columns.map(columnKey);
|
|
1131
|
+
const missing = c.columns.filter((k) => !have.includes(k)).map((k) => q(columnName(k)));
|
|
1132
|
+
const extra = v.columns.filter((f) => !c.columns!.includes(columnKey(f))).map((f) => q(f.name));
|
|
1133
|
+
if (missing.length || extra.length) {
|
|
1134
|
+
const why = [missing.length ? `missing ${missing.join(", ")}` : "", extra.length ? `extra ${extra.join(", ")}` : ""].filter(Boolean).join("; ");
|
|
1135
|
+
const set = columnFields(c.columns).map((f) => f.name).join(", ");
|
|
1136
|
+
views(`${q(c.name)} columns: ${why}`, api(`${q(c.name)}: columns ${set}`, { op: "UpdateView", input: { viewId: v.id, configuration: configuration(c.columns) } }));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// the API reads the grouping but doesn't set it
|
|
1140
|
+
if (c.groupBy && v.layout === c.layout && v.groupBy[0] !== c.groupBy) {
|
|
1141
|
+
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}`));
|
|
1142
|
+
}
|
|
1090
1143
|
}
|
|
1091
1144
|
for (const v of spare) views(`extra ${q(v.name)}`, guarded(`delete view ${q(v.name)}`, { op: "DeleteView", input: { viewId: v.id } }));
|
|
1092
1145
|
|
|
1093
|
-
// Status: options by name or by a known English name — with the same id; missing ones — new
|
|
1094
|
-
|
|
1146
|
+
// Status: options by name or by a known English name — with the same id; missing ones — new; colors — the template's,
|
|
1147
|
+
// descriptions are the project's own.
|
|
1148
|
+
const status = add("status", `${STATUS}: ${statusOptions().join(" → ")} (${STATUS_COLORS.join(", ")})`);
|
|
1095
1149
|
const sf = p.fields.find((f) => f.name === STATUS && f.options);
|
|
1096
1150
|
if (!sf) {
|
|
1097
|
-
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:
|
|
1151
|
+
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: "" })) } }));
|
|
1098
1152
|
} else if (sf.options!.map((o) => o.name).join("\n") !== statusOptions().join("\n")) {
|
|
1099
1153
|
const left = [...sf.options!];
|
|
1100
1154
|
const take = (pred: (o: Option) => boolean) => {
|
|
@@ -1102,18 +1156,21 @@ export function analyze(s: State): Check[] {
|
|
|
1102
1156
|
return i < 0 ? null : left.splice(i, 1)[0]!;
|
|
1103
1157
|
};
|
|
1104
1158
|
const aliases = statusAliases();
|
|
1105
|
-
const next = statusOptions().map((name) => {
|
|
1159
|
+
const next = statusOptions().map((name, i) => {
|
|
1106
1160
|
const o = take((x) => x.name === name) ?? take((x) => aliases[x.name.toLowerCase()] === name);
|
|
1107
|
-
return o ? { id: o.id, name, color:
|
|
1161
|
+
return o ? { id: o.id, name, color: STATUS_COLORS[i]!, description: o.description ?? "" } : { name, color: STATUS_COLORS[i]!, description: "" };
|
|
1108
1162
|
});
|
|
1109
1163
|
const drop = left.length ? `; delete ${left.map((o) => q(o.name)).join(", ")}` : "";
|
|
1110
1164
|
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 } }));
|
|
1165
|
+
} else if (sf.options!.some((o, i) => o.color !== STATUS_COLORS[i])) {
|
|
1166
|
+
// recoloring keeps the ids — the tasks' values and the workflows' targets, no confirmation needed
|
|
1167
|
+
const next = sf.options!.map((o, i) => ({ id: o.id, name: o.name, color: STATUS_COLORS[i]!, description: o.description ?? "" }));
|
|
1168
|
+
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 } }));
|
|
1111
1169
|
}
|
|
1112
1170
|
|
|
1113
1171
|
// Fields: four numeric ones; a field in another dictionary language is renamed with the same id (the tasks' values are
|
|
1114
1172
|
// kept); no custom fields beyond the canon; in an organization Priority is an issue field.
|
|
1115
1173
|
const fields = add("fields", `Fields ${numberFields().map(q).join(", ")} (Number), no other custom ones`);
|
|
1116
|
-
const fieldAlias = fieldAliases();
|
|
1117
1174
|
const renamed = new Set<Field>();
|
|
1118
1175
|
for (const name of numberFields()) {
|
|
1119
1176
|
const f = p.fields.find((x) => x.name === name);
|
|
@@ -1177,14 +1234,18 @@ export function analyze(s: State): Check[] {
|
|
|
1177
1234
|
}
|
|
1178
1235
|
|
|
1179
1236
|
// Workflows: the API doesn't turn them on or show the target — only the UI. An unconfigured one isn't in the list.
|
|
1180
|
-
const wf = add("workflows", `Workflows ${
|
|
1237
|
+
const wf = add("workflows", `Workflows ${WORKFLOWS.map(q).join(", ")} are on`);
|
|
1181
1238
|
const how: Record<string, string> = {
|
|
1182
1239
|
[WORKFLOW_ADDED]: `Edit → Set value → ${STATUS}: ${N.status.backlog}`,
|
|
1183
1240
|
[WORKFLOW_CLOSED]: `Edit → Set value → ${STATUS}: ${N.status.done}`,
|
|
1241
|
+
[WORKFLOW_MERGED]: `Edit → Set value → ${STATUS}: ${N.status.done}`,
|
|
1242
|
+
[WORKFLOW_AUTO_CLOSE]: `Edit → ${STATUS}: ${N.status.done}`,
|
|
1184
1243
|
// is:open — otherwise an updated closed task (not planned too) comes back into the project
|
|
1185
1244
|
[WORKFLOW_AUTO_ADD]: `Edit → repository ${s.repo.name}, filter is:issue is:open (Enter)`,
|
|
1245
|
+
[WORKFLOW_SUB_ISSUES]: "Edit",
|
|
1246
|
+
[WORKFLOW_ARCHIVE]: `Edit → filter ${ARCHIVE_FILTER} (Enter)`,
|
|
1186
1247
|
};
|
|
1187
|
-
for (const name of
|
|
1248
|
+
for (const name of WORKFLOWS) {
|
|
1188
1249
|
const w = p.workflows.find((x) => x.name === name);
|
|
1189
1250
|
if (w?.enabled) continue;
|
|
1190
1251
|
// an unconfigured workflow isn't in the API — a link to the list, it's in the menu on the left there
|
|
@@ -2354,7 +2415,7 @@ function cloudBrief(io: Io, slug: string, issue: Issue): void {
|
|
|
2354
2415
|
state: N.comments.state,
|
|
2355
2416
|
questions: N.sections.questions,
|
|
2356
2417
|
};
|
|
2357
|
-
for (const line of [N.queue.cloud, N.queue.cloudDoc, N.queue.cloudClose, N.queue.cloudDispatcher, N.queue.cloudQuestion]) io.out(locale.fill(line, vars));
|
|
2418
|
+
for (const line of [N.queue.cloud, N.queue.cloudDoc, N.queue.repeats, N.queue.cloudClose, N.queue.cloudDispatcher, N.queue.cloudQuestion]) io.out(locale.fill(line, vars));
|
|
2358
2419
|
io.out(
|
|
2359
2420
|
`Next: the lines above — the cloud session's first prompt as is, the session started from the project folder (the cloud starts from main); ` +
|
|
2360
2421
|
`then as a background command: bash ${waitCi()} issue ${number} — a cloud session can't message a local one (docs/cloud-sessions.md), the task's closing is the signal: ` +
|
|
@@ -2375,6 +2436,17 @@ const SESSION_ARCHIVE =
|
|
|
2375
2436
|
"an executor session whose task closed — the dispatcher archives it in an auto project where the agent has the tool, " +
|
|
2376
2437
|
"not one still working in the background or awaiting the owner's answer; outside auto the owner closes it";
|
|
2377
2438
|
|
|
2439
|
+
/**
|
|
2440
|
+
* A task's open PR: linked to it by GitHub, or one of the repository's recent ones by the task branch or `Closes #N` in
|
|
2441
|
+
* its body. The queue's «=» (the executor waits for its CI) and the epic refresh's «already going» (#636).
|
|
2442
|
+
*/
|
|
2443
|
+
function openPrOf(io: Io, slug: string): (i: Issue) => number | undefined {
|
|
2444
|
+
const [owner, name] = slug.split("/");
|
|
2445
|
+
const recent = (graphql(io, Q.RecentPrs, { owner, name })?.repository?.pullRequests?.nodes ?? []).filter((p: Any) => p.state === "OPEN");
|
|
2446
|
+
return (i) =>
|
|
2447
|
+
i.prs.find((p) => p.state === "OPEN")?.number ?? recent.find((p: Any) => isTaskBranch(p.headRefName ?? "", i.number) || closingInBody(p.body ?? "").includes(i.number))?.number;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2378
2450
|
/**
|
|
2379
2451
|
* The dispatcher's queue: its ready tasks go to background executors (subagents), K at a time; the queue is re-read
|
|
2380
2452
|
* after each answer, so tasks that appeared meanwhile are picked up. A session with its own task dispatches too (#546:
|
|
@@ -2405,10 +2477,7 @@ export function cmdTaskQueue(io: Io, slug: string, o: QueueFilter): number {
|
|
|
2405
2477
|
if (found.length >= 100) io.out("○ the search gave 100 open tasks — the queue sees only them: narrow it with --epic");
|
|
2406
2478
|
numbers = found.filter((x) => x?.state === "OPEN").map((x) => x.number);
|
|
2407
2479
|
}
|
|
2408
|
-
const
|
|
2409
|
-
const recent = (graphql(io, Q.RecentPrs, { owner, name })?.repository?.pullRequests?.nodes ?? []).filter((p: Any) => p.state === "OPEN");
|
|
2410
|
-
const openPr = (i: Issue) =>
|
|
2411
|
-
i.prs.find((p) => p.state === "OPEN")?.number ?? recent.find((p: Any) => isTaskBranch(p.headRefName ?? "", i.number) || closingInBody(p.body ?? "").includes(i.number))?.number;
|
|
2480
|
+
const openPr = openPrOf(io, slug);
|
|
2412
2481
|
const handed = handedOut(io, slug);
|
|
2413
2482
|
/** Issue states the queue has read; a hand-out's task outside the queue is read when its worktree is up for removal. */
|
|
2414
2483
|
const states = new Map<number, string>();
|
|
@@ -2869,8 +2938,8 @@ export function cmdTaskAssignment(io: Io, slug: string, number: number, o: { clo
|
|
|
2869
2938
|
const resume = restart.length ? [N.queue.resume] : [];
|
|
2870
2939
|
// the epic refresh in the same turn (`refresh`, #628) — the subagent's: a session runs it by the canon, with the deploy's wait
|
|
2871
2940
|
const lines = o.session
|
|
2872
|
-
? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.sessionFound, N.queue.sessionQuestion, N.queue.sessionEnd]
|
|
2873
|
-
: [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];
|
|
2941
|
+
? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.repeats, N.queue.sessionFound, N.queue.sessionQuestion, N.queue.sessionEnd]
|
|
2942
|
+
: [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.repeats, N.queue.found, N.queue.question, N.queue.answer];
|
|
2874
2943
|
for (const line of lines) io.out(locale.fill(line, vars));
|
|
2875
2944
|
if (o.session) {
|
|
2876
2945
|
io.out(
|
|
@@ -3063,8 +3132,9 @@ const textBlock = (body: string) => (body ? `~~~~markdown\n${body}\n~~~~` : N.ac
|
|
|
3063
3132
|
* its open tasks and the PR files run to tens of KB, and the session at peak context would copy them into the subagent
|
|
3064
3133
|
* call as output tokens. In it — the task and its PR with its files, the epic and its open tasks with their texts, the
|
|
3065
3134
|
* copies of links and the answers in comments the script found, the checks by meaning quoted from reference.md as is,
|
|
3066
|
-
* what may be changed and the answer form;
|
|
3067
|
-
*
|
|
3135
|
+
* what may be changed and the answer form; under each open task — where a repeat finding of check 5 goes, by its Status
|
|
3136
|
+
* and open PR (#636); in the project language (the `actualize` keys). No epic, or no open tasks in it besides this one
|
|
3137
|
+
* — the checks by meaning have nothing to go through: a "not needed" line, no subagent.
|
|
3068
3138
|
*/
|
|
3069
3139
|
export function cmdTaskActualize(io: Io, slug: string, number: number, o: { assignment: boolean } = { assignment: false }): number {
|
|
3070
3140
|
const issue = loadIssue(io, slug, number);
|
|
@@ -3076,6 +3146,8 @@ export function cmdTaskActualize(io: Io, slug: string, number: number, o: { assi
|
|
|
3076
3146
|
const open = epic.subIssues.filter((x) => x.state === "OPEN" && x.number !== number).map((x) => loadTaskText(io, slug, x.number));
|
|
3077
3147
|
if (!o.assignment) refreshMechanics(io, slug, { number, merged: prs.length > 0, epic, open });
|
|
3078
3148
|
if (!open.length) return io.out(`○ refresh not needed: epic #${epic.number} has no open tasks besides #${number}. ${later}`), 0;
|
|
3149
|
+
// the board with its Status (and its language) — the assignment names where a repeat finding goes (#636)
|
|
3150
|
+
const ctx = o.assignment ? taskContext(io, slug) : null;
|
|
3079
3151
|
const vars = { task: N.subagentPrompt.task, epicWord: N.subagentPrompt.epic, number, epic: epic.number, repo: slug };
|
|
3080
3152
|
const lead = locale.fill(N.actualize.lead, vars);
|
|
3081
3153
|
if (!o.assignment) {
|
|
@@ -3094,7 +3166,17 @@ export function cmdTaskActualize(io: Io, slug: string, number: number, o: { assi
|
|
|
3094
3166
|
if (files.length) lines.push(locale.fill(N.actualize.files, { pr: p.number }), ...files.map(fileLine));
|
|
3095
3167
|
}
|
|
3096
3168
|
lines.push("", locale.fill(N.actualize.epic, { ...vars, title: epic.title }), textBlock(epic.body), "", N.actualize.openTasks);
|
|
3097
|
-
|
|
3169
|
+
const openPr = openPrOf(io, slug);
|
|
3170
|
+
/** Where a repeat finding of check 5 goes (#636): a task in Backlog takes it as an item, one already going doesn't. */
|
|
3171
|
+
const target = (n: number): string[] => {
|
|
3172
|
+
const i = loadIssue(io, slug, n);
|
|
3173
|
+
const status = itemIn(ctx!, i)?.status ?? null;
|
|
3174
|
+
const pr = openPr(i);
|
|
3175
|
+
const work = [status === N.status.inProgress ? locale.fill(N.actualize.taskStatus, { status }) : "", pr === undefined ? "" : locale.fill(N.actualize.taskPr, { pr })].filter(Boolean);
|
|
3176
|
+
if (work.length) return [locale.fill(N.actualize.inWork, { work: work.join(", ") })];
|
|
3177
|
+
return status === N.status.backlog ? [locale.fill(N.actualize.backlog, { status: locale.fill(N.actualize.taskStatus, { status }) })] : [];
|
|
3178
|
+
};
|
|
3179
|
+
for (const t of open) lines.push("", locale.fill(N.actualize.openTask, { number: t.number, title: t.title }), ...target(t.number), textBlock(t.body));
|
|
3098
3180
|
const copies = linkCopies(epic, open);
|
|
3099
3181
|
if (copies.length) lines.push("", N.actualize.copies, ...copies.map((c) => locale.fill(N.actualize.copy, c)));
|
|
3100
3182
|
const answers = commentAnswers([epic, ...open]);
|
|
@@ -73,6 +73,14 @@ export interface Names {
|
|
|
73
73
|
epic: string;
|
|
74
74
|
openTasks: string;
|
|
75
75
|
openTask: string;
|
|
76
|
+
/**
|
|
77
|
+
* Where a repeat finding of check 5 goes, under an open task's heading (#636): a task in Backlog takes it as an item
|
|
78
|
+
* (`backlog`), one already going — In progress or with an open PR — doesn't (`inWork`); `taskStatus`, `taskPr` — why.
|
|
79
|
+
*/
|
|
80
|
+
backlog: string;
|
|
81
|
+
inWork: string;
|
|
82
|
+
taskStatus: string;
|
|
83
|
+
taskPr: string;
|
|
76
84
|
noBody: string;
|
|
77
85
|
copies: string;
|
|
78
86
|
copy: string;
|
|
@@ -104,6 +112,8 @@ export interface Names {
|
|
|
104
112
|
live: string;
|
|
105
113
|
/** The epic refresh — in the same turn, before `task close` and the answer; a task without a PR closed first (#628). */
|
|
106
114
|
refresh: string;
|
|
115
|
+
/** Repeats in the files the task changes anyway — fixed in its PR, not a new task (#636); every kind of executor. */
|
|
116
|
+
repeats: string;
|
|
107
117
|
found: string;
|
|
108
118
|
question: string;
|
|
109
119
|
answer: string;
|