@miroshnik/elves 2026.1018.2 → 2026.1019.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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) and the SessionStart hook; without a terminal too — a cloud session's project hook
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); a thin project behind — update in this session's task (the package version in
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 hook; a project — install of the latest release: ${PACKAGE} in package.json 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 and the hook.
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-qzb932c8.js";
4
+ } from "../../spec/scripts/harness-wqrt5gpa.js";
5
5
 
6
6
  // skills/github/scripts/github.ts
7
7
  import { spawnSync } from "node:child_process";
@@ -11,12 +11,14 @@ import path from "node:path";
11
11
  import { parseArgs } from "node:util";
12
12
  var N = locale_default.names(locale_default.projectLanguage());
13
13
  var canonViews = () => [
14
- { name: N.views.board, layout: "BOARD_LAYOUT" },
15
- { name: N.views.table, layout: "TABLE_LAYOUT" },
14
+ { name: N.views.board, layout: "BOARD_LAYOUT", columns: ["TITLE", STATUS, "LABELS", "SUB_ISSUES_PROGRESS"] },
15
+ { name: N.views.table, layout: "TABLE_LAYOUT", columns: ["TITLE", STATUS, "LABELS", "ASSIGNEES", "SUB_ISSUES_PROGRESS", N.fields.estimate], groupBy: STATUS },
16
16
  { name: N.views.roadmap, layout: "ROADMAP_LAYOUT" }
17
17
  ];
18
+ var BUILTIN_COLUMNS = { TITLE: "Title", ASSIGNEES: "Assignees", LABELS: "Labels", SUB_ISSUES_PROGRESS: "Sub-issues progress" };
18
19
  var STATUS = "Status";
19
20
  var statusOptions = () => [N.status.backlog, N.status.inProgress, N.status.done];
21
+ var STATUS_COLORS = ["GREEN", "YELLOW", "PURPLE"];
20
22
  var numberFields = () => [N.fields.estimate, N.fields.actual, N.fields.tokens, N.fields.cost];
21
23
  var issueTypes = () => [N.issueTypes.task, N.issueTypes.bug, N.issueTypes.epic];
22
24
  var PRIORITY = "Priority";
@@ -36,6 +38,12 @@ var ISSUE_LABEL_LIMIT = 100;
36
38
  var WORKFLOW_ADDED = "Item added to project";
37
39
  var WORKFLOW_CLOSED = "Item closed";
38
40
  var WORKFLOW_AUTO_ADD = "Auto-add to project";
41
+ var WORKFLOW_MERGED = "Pull request merged";
42
+ var WORKFLOW_AUTO_CLOSE = "Auto-close issue";
43
+ var WORKFLOW_SUB_ISSUES = "Auto-add sub-issues to project";
44
+ var WORKFLOW_ARCHIVE = "Auto-archive items";
45
+ var WORKFLOWS = [WORKFLOW_ADDED, WORKFLOW_CLOSED, WORKFLOW_MERGED, WORKFLOW_AUTO_CLOSE, WORKFLOW_AUTO_ADD, WORKFLOW_SUB_ISSUES, WORKFLOW_ARCHIVE];
46
+ var ARCHIVE_FILTER = "is:issue is:closed updated:<@today-2w";
39
47
  var DEFAULT_TEMPLATE = "miroshnik/6";
40
48
  var RULESET = "elves";
41
49
  var FORMER_RULESET = "ai-dev";
@@ -122,6 +130,8 @@ var Q = {
122
130
  id number name layout filter
123
131
  sortByFields(first: 5) { nodes { direction field { ... on ProjectV2FieldCommon { name } } } }
124
132
  verticalGroupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } }
133
+ groupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } }
134
+ fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name dataType } } }
125
135
  }
126
136
  }
127
137
  fields(first: 100) {
@@ -650,7 +660,9 @@ function loadProject(io, id, withItems = true) {
650
660
  layout: v.layout,
651
661
  filter: v.filter ?? null,
652
662
  sortBy: (v.sortByFields?.nodes ?? []).map((s) => s.field?.name).filter(Boolean),
653
- columnsBy: (v.verticalGroupByFields?.nodes ?? []).map((f) => f?.name).filter(Boolean)
663
+ columnsBy: (v.verticalGroupByFields?.nodes ?? []).map((f) => f?.name).filter(Boolean),
664
+ groupBy: (v.groupByFields?.nodes ?? []).map((f) => f?.name).filter(Boolean),
665
+ columns: (v.fields?.nodes ?? []).filter((f) => f?.id).map((f) => ({ id: f.id, name: f.name, dataType: f.dataType }))
654
666
  })),
655
667
  fields: p.fields.nodes.filter((f) => f && f.id).map((f) => ({ id: f.id, name: f.name, dataType: f.dataType, isIssueField: !!f.isIssueField, options: f.options ?? null })),
656
668
  workflows: p.workflows.nodes.filter(Boolean),
@@ -692,7 +704,12 @@ function analyze(s) {
692
704
  for (const other of s.repo.linked.filter((x) => x.id !== p.id)) {
693
705
  link(`another project is linked: ${q(other.title)} (${other.url})`, { kind: "confirm", text: `unlink project ${q(other.title)} from the repository`, mutations: [{ op: "UnlinkProject", input: { projectId: other.id, repositoryId: s.repo.id } }] });
694
706
  }
695
- const views = add("views", `Views ${canonViews().map((v) => q(v.name)).join(", ")} — the right layout, no filter, no others`);
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
- views(`missing ${q(c.name)}`, api(`create view ${q(c.name)} (${c.layout})`, { op: "CreateView", input: { projectId: p.id, name: c.name, layout: c.layout } }));
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: "GRAY", description: "" })) } }));
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: o.color, description: o.description ?? "" } : { name, color: "GRAY", description: "" };
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 ${[WORKFLOW_ADDED, WORKFLOW_CLOSED, WORKFLOW_AUTO_ADD].map(q).join(", ")} are on`);
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
- [WORKFLOW_AUTO_ADD]: `Edit → repository ${s.repo.name}, filter is:issue is:open (Enter)`
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 [WORKFLOW_ADDED, WORKFLOW_CLOSED, WORKFLOW_AUTO_ADD]) {
850
+ for (const name of WORKFLOWS) {
814
851
  const w = p.workflows.find((x) => x.name === name);
815
852
  if (w?.enabled)
816
853
  continue;
@@ -1280,6 +1317,7 @@ var handedOutFile = (io) => {
1280
1317
  return file && `${file}.tasks`;
1281
1318
  };
1282
1319
  var CLOUD_HANDOUT = "@cloud";
1320
+ var SESSION_HANDOUT = "@session";
1283
1321
  function handedOut(io, slug) {
1284
1322
  const file = handedOutFile(io);
1285
1323
  return file ? handOutsIn(file, slug) : [];
@@ -1292,7 +1330,8 @@ function handOutsIn(file, slug) {
1292
1330
  return lines.filter((l) => l.startsWith(`${slug}#`)).map((l) => {
1293
1331
  const [number, ...rest] = l.slice(slug.length + 1).split(" ");
1294
1332
  const worktree = rest.join(" ").trim();
1295
- return { number: Number(number), worktree: worktree === CLOUD_HANDOUT ? null : worktree || null, cloud: worktree === CLOUD_HANDOUT };
1333
+ const executor = worktree === CLOUD_HANDOUT ? "cloud" : worktree === SESSION_HANDOUT ? "session" : "subagent";
1334
+ return { number: Number(number), worktree: executor === "subagent" ? worktree || null : null, executor };
1296
1335
  }).filter((h) => Number.isInteger(h.number) && h.number > 0);
1297
1336
  }
1298
1337
  function realPath(p) {
@@ -1809,8 +1848,10 @@ function cmdTaskQueue(io, slug, o) {
1809
1848
  const pr = openPr(i);
1810
1849
  const work = [status === N.status.inProgress ? q(N.status.inProgress) : "", pr === undefined ? "" : `open PR #${pr} https://github.com/${slug}/pull/${pr}`].filter(Boolean);
1811
1850
  const hand = handed.find((h) => h.number === i.number);
1812
- if (!work.length && hand?.cloud)
1851
+ if (!work.length && hand?.executor === "cloud")
1813
1852
  work.push(`handed out to a cloud executor of this session, its wait — a background command: bash ${waitCi()} issue ${i.number}`);
1853
+ else if (!work.length && hand?.executor === "session")
1854
+ work.push("handed out to an app executor session of this session (start_session) — the end of each of its turns comes as the app's note");
1814
1855
  else if (!work.length && hand)
1815
1856
  work.push("handed out to an executor of this session");
1816
1857
  if (work.length) {
@@ -2079,7 +2120,7 @@ function taskBranchesHere(io, slug, number) {
2079
2120
  const refs = [...git("for-each-ref", "--format=%(refname)", "refs/heads/"), ...git("ls-remote", "--heads", "origin").map((l) => l.split("\t")[1] ?? "")];
2080
2121
  return [...new Set(refs.map((r) => r.trim().replace(/^refs\/heads\//, "")).filter((b) => isTaskBranch(b, number)))].sort();
2081
2122
  }
2082
- function cmdTaskAssignment(io, slug, number, o = { cloud: false }) {
2123
+ function cmdTaskAssignment(io, slug, number, o = {}) {
2083
2124
  const issue = loadIssue(io, slug, number);
2084
2125
  const file = handedOutFile(io);
2085
2126
  const own = pinnedTask(io)?.task;
@@ -2096,7 +2137,7 @@ function cmdTaskAssignment(io, slug, number, o = { cloud: false }) {
2096
2137
  mkdirSync(path.dirname(file), { recursive: true });
2097
2138
  writeFileSync(path.join(path.dirname(file), ".gitignore"), `*
2098
2139
  `);
2099
- const where = o.cloud ? CLOUD_HANDOUT : worktree;
2140
+ const where = o.cloud ? CLOUD_HANDOUT : o.session ? SESSION_HANDOUT : worktree;
2100
2141
  appendFileSync(file, `${slug}#${number}${where ? ` ${where}` : ""}
2101
2142
  `);
2102
2143
  }
@@ -2114,11 +2155,17 @@ function cmdTaskAssignment(io, slug, number, o = { cloud: false }) {
2114
2155
  inProgress: N.status.inProgress,
2115
2156
  done: N.lastLine.done,
2116
2157
  questions: N.sections.questions,
2117
- branches: restart.map((b) => `\`${b}\``).join(", ")
2158
+ branches: restart.map((b) => `\`${b}\``).join(", "),
2159
+ doneQuestions: N.lastLine.questions,
2160
+ remaining: N.lastLine.remaining
2118
2161
  };
2119
- const lines = [N.queue.lead, ...restart.length ? [N.queue.resume] : [], N.queue.subagent, N.queue.session, N.queue.wait, N.queue.git, N.queue.subagents, N.queue.live, N.queue.found, N.queue.question, N.queue.answer];
2162
+ const resume = restart.length ? [N.queue.resume] : [];
2163
+ const lines = o.session ? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.sessionFound, N.queue.sessionQuestion, N.queue.sessionEnd] : [N.queue.lead, ...resume, N.queue.subagent, N.queue.session, N.queue.wait, N.queue.git, N.queue.subagents, N.queue.live, N.queue.refresh, N.queue.found, N.queue.question, N.queue.answer];
2120
2164
  for (const line of lines)
2121
2165
  io.out(locale_default.fill(line, vars));
2166
+ if (o.session) {
2167
+ io.out(`Next: the lines above — the executor session's prompt as is: start_session, initiation "user_asked", cwd — the folder of the ${slug} checkout on this machine (not a worktree), ` + `use_worktree true, background — the first line above; keep the session's id it returns. ` + `The end of each of its turns comes as the app's note, like an executor's answer — task queue again: in a queue of ${slug} the task is =; ${SESSION_ARCHIVE}.`);
2168
+ }
2122
2169
  return 0;
2123
2170
  }
2124
2171
  function actualizeChecks() {
@@ -2308,7 +2355,8 @@ function cmdTaskClose(io, slug, number, o) {
2308
2355
  const prs = [...issue.prs, ...fresh].map((p) => `#${p.number} ${p.state === "OPEN" ? "open" : "closed without a merge"}`).join(", ");
2309
2356
  const what = prs ? `PR ${prs}` : `the task is ${issue.state === "OPEN" ? "open" : "closed without being done"}, no merged PR`;
2310
2357
  const checked = found ? `; checked: the PR–task link, the branch <type>/${number}-<slug> and «Closes #${number}» in the body of the last ${RECENT_PRS} PRs into ${found.base}` : "";
2311
- throw new GhError(`#${number} is too early to close: ${what}${checked} — close after the PR is merged or the task is closed`);
2358
+ const noPr = !prs && issue.state === "OPEN" ? `; a task without a PR (a data edit, a reconciliation, research) — close the issue as completed with a result comment: gh issue close ${number} --repo ${slug} --reason completed --comment "<result>", then task close ${number} again` : "";
2359
+ throw new GhError(`#${number} is too early to close: ${what}${checked} — close after the PR is merged or the task is closed${noPr}`);
2312
2360
  }
2313
2361
  if (issue.parent) {
2314
2362
  const epic = loadTaskText(io, slug, issue.parent.number);
@@ -3121,7 +3169,7 @@ that can't be continued, in the worktree of its task's branch.
3121
3169
  Examples:
3122
3170
  github task resume`,
3123
3171
  "task queue": `github task queue [--epic N] [--type Task|Bug] [--priority Urgent,High] [--repo owner/repo]
3124
- github task queue --assignment <N> [--cloud] [--repo owner/repo]
3172
+ github task queue --assignment <N> [--cloud | --session] [--repo owner/repo]
3125
3173
 
3126
3174
  The dispatcher session's queue: ready tasks (open, not an epic, not in work, no open blockers — other repositories'
3127
3175
  too —, no external blocker label) by Priority, then number, each with its executor's prompt; the rest — in work (=: In
@@ -3140,13 +3188,17 @@ such a subagent at work or HEAD moved in the last ${QUIET_MINUTES} min — code
3140
3188
  dispatcher wait, no hand-out. --cloud — printed by the dispatcher: the brief of a cloud executor (claude.ai/code) —
3141
3189
  whole, # only at its task — and the dispatcher's steps: a cloud session can't message a local one, so the dispatcher
3142
3190
  waits for the task's closing (ci-wait issue N, background) and closes it locally; the hand-out has no worktree, and the
3143
- queue's = line names that wait.
3191
+ queue's = line names that wait. --session — printed by the dispatcher: the prompt of an app executor session
3192
+ (start_session in the folder of another project, its own worktree) — a session by its project's canon and mode, named
3193
+ by the task, waiting in the background, ending with the canon's last line — and how to start it; the hand-out has no
3194
+ worktree, the task is = in the queue.
3144
3195
 
3145
3196
  Examples:
3146
3197
  github task queue --epic 40
3147
3198
  github task queue --type Bug --priority Urgent,High
3148
3199
  github task queue --assignment 42
3149
- github task queue --assignment 42 --cloud`,
3200
+ github task queue --assignment 42 --cloud
3201
+ github task queue --assignment 42 --session`,
3150
3202
  "task close": `github task close <N> [--no-git] [--repo owner/repo]
3151
3203
 
3152
3204
  After the PR merge and the deploy (or a close without a PR) in one call: the actual (est fact --write), Status Done, the
@@ -3299,6 +3351,7 @@ function main(argv, io) {
3299
3351
  "no-git": { type: "boolean", default: false },
3300
3352
  assignment: { type: "boolean", default: false },
3301
3353
  cloud: { type: "boolean", default: false },
3354
+ session: { type: "boolean", default: false },
3302
3355
  auto: { type: "boolean", default: false }
3303
3356
  }
3304
3357
  });
@@ -3324,10 +3377,14 @@ function main(argv, io) {
3324
3377
  if (cmd === "queue") {
3325
3378
  if (values.cloud && !values.assignment)
3326
3379
  throw new GhError("--cloud — only with --assignment <N>: the brief of one task's cloud executor");
3380
+ if (values.session && !values.assignment)
3381
+ throw new GhError("--session — only with --assignment <N>: the prompt of one task's app executor session");
3382
+ if (values.cloud && values.session)
3383
+ throw new GhError("--cloud or --session, not both: a cloud executor or an app session");
3327
3384
  if (values.assignment) {
3328
3385
  if (positionals.length !== 1)
3329
3386
  throw new GhError("task queue --assignment <N>: one task number");
3330
- return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud });
3387
+ return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud, session: values.session });
3331
3388
  }
3332
3389
  if (positionals.length)
3333
3390
  throw new GhError(`extra arguments: ${positionals.join(" ")} — the queue is chosen by --epic, --type and --priority`);
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  locale_default
3
- } from "./harness-qzb932c8.js";
3
+ } from "./harness-wqrt5gpa.js";
4
4
  import {
5
5
  invariant2,
6
6
  codeOnly2,
7
7
  envNamesInCode2
8
- } from "./harness-v9s1v9w3.js";
8
+ } from "./harness-ay62ws50.js";
9
9
 
10
10
  // skills/spec/scripts/architecture.ts
11
11
  import { readdirSync, readFileSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  locale_default
3
- } from "./harness-qzb932c8.js";
3
+ } from "./harness-wqrt5gpa.js";
4
4
  var __create = Object.create;
5
5
  var __getProtoOf = Object.getPrototypeOf;
6
6
  var __defProp = Object.defineProperty;
@@ -91,6 +91,7 @@ var en_default = {
91
91
  git: "- git — in separate calls, without `cd`, `&&` and `|`: worktree isolation refuses a compound command with git;",
92
92
  subagents: "- writing subagents — launch as needed, no limit: each gets its own worktree and «{task} #{number}» at the start of its prompt (`est` links its work by it); bring their commits into your branch, remove their worktrees and branches yourself after the merge — what didn't go, name in the answer;",
93
93
  live: "- while any of your subagents works, don't answer: wait for their answers — a background subagent wakes you with its answer; an answer with live subagents looks like a failure, and the dispatcher would give the task to a second executor next to you. Can't wait — answer `waits for subagents: <which and what they do>`: it isn't `failed`, there will be no second executor;",
94
+ refresh: '- the epic refresh — the subagent `task actualize` prints, without a worktree of its own (in yours) and not in the background (`run_in_background: false`): its answer — in the same turn, then `{script} task close {number}`, and only then the answer to the dispatcher: once you answer, a worktree without changes is removed, and a background subagent is left without a directory; a task without a PR (a data edit, a reconciliation, research) — first close the issue as completed with a result comment: `gh issue close {number} --repo {repo} --reason completed --comment "<result>"`;',
94
95
  found: "- work found outside the task — `{script} task new …` (work of the task's epic — with `--epic`: the dispatcher picks it up) — name it in the answer: whether to queue it is the owner's call;",
95
96
  question: "- a question without a clear recommendation — into «{questions}» of the task; don't start the code that depends on it and answer the dispatcher: the owner's answer comes back here.",
96
97
  answer: "The answer to the dispatcher — one line: `merged #<PR>` · `question: <the question, options, recommendation>` · `waits for subagents: <which and what they do>` · `failed: <what stopped and where>`.",
@@ -100,7 +101,14 @@ var en_default = {
100
101
  cloudDoc: "- the cloud — by `docs/cloud-sessions.md` of the canon, read it first: of the github skill's scripts `task show`, `task answer` and `task new` work there (over REST), the rest refuse, the scenarios section — by editing the description over REST, project fields aren't set from the cloud; the branch is the one the cloud assigned, don't rename it;",
101
102
  cloudClose: "- after the merge — its section «Closing a task from the cloud»: the «{actual}» comment of `est fact {number}` and a «{state}» comment with what the cloud can't do — both into the task;",
102
103
  cloudDispatcher: "- the dispatcher waits for the task to close and closes it locally — the actual, the status, the merged branch, archiving this session: don't message it, don't archive the session;",
103
- cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task."
104
+ cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task.",
105
+ sessionLead: "{task} #{number} «{title}» in {repo}{epic}: you are an executor session of the dispatcher session — do the task whole by the canon of this project, in its mode (`check` prints it; outside auto — commit, push and merge on the owner's «yes»): the task's questions, `{script} task status {number} \"{inProgress}\" --branch <type>/{number}-<slug>`, the branch from a fresh origin/main, code and tests, PR, green CI, `pr premerge`, merge, deploy, the epic refresh, `{script} task close {number}`.",
106
+ sessionIs: "You are a session, not a subagent:",
107
+ sessionName: "- name the session `#{number} {title}` (`set_session_title`) once the task is taken — `est` links the session to the task by it;",
108
+ sessionWait: "- wait for CI and the deploy with a background command — `{waitCi} pr <PR>` (after the merge — `merged <PR>`): its completion wakes the session; no `Monitor`, watcher loop or `ScheduleWakeup` on top;",
109
+ sessionFound: "- work found outside the task — `{script} task new …` and the question «queue it?» by the canon; work of the task's epic — with `--epic` and without an executor of yours: the dispatcher's queue picks it up;",
110
+ sessionQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session; don't start the code that depends on it: the answer comes here, from the owner or the dispatcher;",
111
+ sessionEnd: "- the end — the canon's last line («{done}» · «{doneQuestions} …» · «{remaining} …»), in auto mode after «{done}» — archiving the session by the canon: the dispatcher reads the end of each of your turns as your answer."
104
112
  },
105
113
  est: {
106
114
  answer: "Answer",
@@ -541,6 +549,7 @@ var ru_default = {
541
549
  git: "- git — отдельными вызовами, без `cd`, `&&` и `|`: составную команду с git изоляция worktree не пропускает;",
542
550
  subagents: "- пишущих субагентов запускай по необходимости, без лимита: каждому — свой worktree и «{task} #{number}» в начале промпта (по нему `est` привяжет его работу); их коммиты перенеси в свою ветку, их worktree и ветки после мержа убери сам — что не убралось, назови в ответе;",
543
551
  live: "- пока работает хоть один твой субагент, ответ не отдавай: жди их ответов — фоновый субагент будит тебя своим ответом; ответ при живых субагентах выглядит провалом, и диспетчер отдал бы задачу второму исполнителю рядом с тобой. Ждать нельзя — ответь `ждёт субагентов: <какие и что делают>`: это не «сломалось», второго исполнителя не будет;",
552
+ refresh: '- актуализация эпика — субагент, которого печатает `task actualize`, без своего worktree (в твоём) и не в фоне (`run_in_background: false`): его ответ — в том же ходе, затем `{script} task close {number}`, и только потом ответ диспетчеру: стоит тебе ответить, worktree без изменений убирают, и фоновый субагент остаётся без каталога; у задачи без PR (правка данных, сверка, исследование) — сначала закрой issue как выполненную с итоговым комментарием: `gh issue close {number} --repo {repo} --reason completed --comment "<итог>"`;',
544
553
  found: "- найденная работа вне задачи — `{script} task new …` (работа эпика задачи — с `--epic`: диспетчер её подхватит) — назови её в ответе: ставить ли её в очередь, решает владелец;",
545
554
  question: "- вопрос без ясной рекомендации — в «{questions}» задачи; код, который от него зависит, не начинай и ответь диспетчеру: ответ владельца придёт сюда же.",
546
555
  answer: "Ответ диспетчеру — одной строкой: `влито #<PR>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
@@ -550,7 +559,14 @@ var ru_default = {
550
559
  cloudDoc: "- облако — по `docs/cloud-sessions.md` канона, прочитай его первым: из скриптов скилла github там работают `task show`, `task answer` и `task new` (через REST), остальные отказывают, раздел сценариев — правкой описания через REST, поля проекта из облака не ставятся; ветка — та, что назначило облако, не переименовывай;",
551
560
  cloudClose: "- после мержа — его раздел «Closing a task from the cloud»: комментарий «{actual}» от `est fact {number}` и комментарий «{state}» с тем, что из облака не сделать, — оба в задачу;",
552
561
  cloudDispatcher: "- диспетчер ждёт закрытия задачи и закрывает её локально — факт, статус, влитая ветка, архив этой сессии: сообщать ему не нужно, сессию не архивируй;",
553
- cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу."
562
+ cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу.",
563
+ sessionLead: '{task} #{number} «{title}» в {repo}{epic}: ты сессия-исполнитель сессии-диспетчера — сделай задачу целиком по канону этого проекта, в его режиме (его печатает `check`; не в auto — коммит, пуш и мерж по «да» владельца): вопросы задачи, `{script} task status {number} "{inProgress}" --branch <тип>/{number}-<slug>`, ветка от свежего origin/main, код и тесты, PR, зелёный CI, `pr premerge`, мерж, деплой, актуализация эпика, `{script} task close {number}`.',
564
+ sessionIs: "Ты сессия, а не субагент:",
565
+ sessionName: "- назови сессию `#{number} {title}` (`set_session_title`), как только взял задачу, — по имени `est` привяжет сессию к задаче;",
566
+ sessionWait: "- CI и деплой жди фоновой командой — `{waitCi} pr <PR>` (после мержа — `merged <PR>`): её завершение будит сессию; без `Monitor`, цикла-наблюдателя и `ScheduleWakeup` поверх;",
567
+ sessionFound: "- найденная работа вне задачи — `{script} task new …` и вопрос «ставить в очередь?» по канону; работа эпика задачи — с `--epic` и без твоего исполнителя: её подхватит очередь диспетчера;",
568
+ sessionQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии; код, который от него зависит, не начинай: ответ придёт сюда же — от владельца или диспетчера;",
569
+ sessionEnd: "- конец — последняя строка канона («{done}» · «{doneQuestions} …» · «{remaining} …»), в режиме auto после «{done}» — архивация сессии по канону: конец каждого твоего хода диспетчер читает как твой ответ."
554
570
  },
555
571
  est: {
556
572
  answer: "Ответ",
@@ -23,8 +23,8 @@ import {
23
23
  envNamesInCode2,
24
24
  dotenvNames2,
25
25
  envVars2
26
- } from "./harness-v9s1v9w3.js";
27
- import"./harness-qzb932c8.js";
26
+ } from "./harness-ay62ws50.js";
27
+ import"./harness-wqrt5gpa.js";
28
28
  export {
29
29
  ENV_SERVICE2 as ENV_SERVICE,
30
30
  codeFiles2 as codeFiles,