@miroshnik/elves 2026.1019.1 → 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 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
- the epic refresh in parallel; **then `github task close`**. Others'
204
- branches, stacks and unmerged work I don't touch.
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` skill (`canon.md`, `checks.md`).
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: it carries the same preamble (≈ 100k
286
- tokens), and its price is quadratic in turns. The assignment has the task
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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  locale_default
4
- } from "../../spec/scripts/harness-wqrt5gpa.js";
4
+ } from "../../spec/scripts/harness-tdg4dnq3.js";
5
5
 
6
6
  // skills/github/scripts/github.ts
7
7
  import { spawnSync } from "node:child_process";
@@ -1787,11 +1787,16 @@ function cloudBrief(io, slug, issue) {
1787
1787
  state: N.comments.state,
1788
1788
  questions: N.sections.questions
1789
1789
  };
1790
- 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])
1791
1791
  io.out(locale_default.fill(line, vars));
1792
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`);
1793
1793
  }
1794
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
+ }
1795
1800
  function cmdTaskQueue(io, slug, o) {
1796
1801
  const want = o.type === undefined ? undefined : typeAliases()[o.type.toLowerCase()];
1797
1802
  if (o.type !== undefined && (!want || want === N.issueTypes.epic))
@@ -1817,9 +1822,7 @@ function cmdTaskQueue(io, slug, o) {
1817
1822
  io.out("○ the search gave 100 open tasks — the queue sees only them: narrow it with --epic");
1818
1823
  numbers = found.filter((x) => x?.state === "OPEN").map((x) => x.number);
1819
1824
  }
1820
- const [owner, name] = slug.split("/");
1821
- const recent = (graphql(io, Q.RecentPrs, { owner, name })?.repository?.pullRequests?.nodes ?? []).filter((p) => p.state === "OPEN");
1822
- 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);
1823
1826
  const handed = handedOut(io, slug);
1824
1827
  const states = new Map;
1825
1828
  const isOpen = (n) => {
@@ -2160,7 +2163,7 @@ function cmdTaskAssignment(io, slug, number, o = {}) {
2160
2163
  remaining: N.lastLine.remaining
2161
2164
  };
2162
2165
  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];
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];
2164
2167
  for (const line of lines)
2165
2168
  io.out(locale_default.fill(line, vars));
2166
2169
  if (o.session) {
@@ -2307,6 +2310,7 @@ function cmdTaskActualize(io, slug, number, o = { assignment: false }) {
2307
2310
  refreshMechanics(io, slug, { number, merged: prs.length > 0, epic, open });
2308
2311
  if (!open.length)
2309
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;
2310
2314
  const vars = { task: N.subagentPrompt.task, epicWord: N.subagentPrompt.epic, number, epic: epic.number, repo: slug };
2311
2315
  const lead = locale_default.fill(N.actualize.lead, vars);
2312
2316
  if (!o.assignment) {
@@ -2324,8 +2328,18 @@ function cmdTaskActualize(io, slug, number, o = { assignment: false }) {
2324
2328
  lines.push(locale_default.fill(N.actualize.files, { pr: p.number }), ...files.map(fileLine));
2325
2329
  }
2326
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
+ };
2327
2341
  for (const t of open)
2328
- 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));
2329
2343
  const copies = linkCopies(epic, open);
2330
2344
  if (copies.length)
2331
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-wqrt5gpa.js";
3
+ } from "./harness-tdg4dnq3.js";
4
4
  import {
5
5
  invariant2,
6
6
  codeOnly2,
7
7
  envNamesInCode2
8
- } from "./harness-ay62ws50.js";
8
+ } from "./harness-bpyj4s91.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-wqrt5gpa.js";
3
+ } from "./harness-tdg4dnq3.js";
4
4
  var __create = Object.create;
5
5
  var __getProtoOf = Object.getPrototypeOf;
6
6
  var __defProp = Object.defineProperty;
@@ -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}» — после двоеточия что поправил, а править нечего — «блок актуален». По нему `task close {number}` видит, что актуализация закончена: без него закрытие откажет.",
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-ay62ws50.js";
27
- import"./harness-wqrt5gpa.js";
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.1",
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": "672760816ee853bd6c6cedf7bd917a123ee0fb41"
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}» — после двоеточия что поправил, а править нечего — «блок актуален». По нему `task close {number}` видит, что актуализация закончена: без него закрытие откажет.",
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>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
@@ -246,12 +246,20 @@ line `○ refresh not needed`, no subagent. Five checks:
246
246
  of subtasks in the epic, an order, "⛔" or "after #N" marks — is removed.
247
247
  5. **Repetition is a standard.** A decision that, in what shipped, repeats an
248
248
  existing one in two or more places — a `Standard · …` issue
249
- (a rule in `tests/standards/` and a refactoring). First a search among the
250
- open `Standard · …`: one exists — don't file another; an extension of a
251
- neighboring one — a comment in it. A standard's priority — by the cost of
252
- the mistake it prevents; standards of one area — as an epic: otherwise
253
- they are born faster than they are worked off. Something deferred as "not
254
- in this PR" — an issue or dropped explicitly, not forgotten.
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.
255
263
 
256
264
  What was fixed — one comment on the epic, the history of decisions in one
257
265
  place; it is the subagent's last step, always: nothing to fix — «the block is
@@ -2415,7 +2415,7 @@ function cloudBrief(io: Io, slug: string, issue: Issue): void {
2415
2415
  state: N.comments.state,
2416
2416
  questions: N.sections.questions,
2417
2417
  };
2418
- 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));
2419
2419
  io.out(
2420
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); ` +
2421
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: ` +
@@ -2436,6 +2436,17 @@ const SESSION_ARCHIVE =
2436
2436
  "an executor session whose task closed — the dispatcher archives it in an auto project where the agent has the tool, " +
2437
2437
  "not one still working in the background or awaiting the owner's answer; outside auto the owner closes it";
2438
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
+
2439
2450
  /**
2440
2451
  * The dispatcher's queue: its ready tasks go to background executors (subagents), K at a time; the queue is re-read
2441
2452
  * after each answer, so tasks that appeared meanwhile are picked up. A session with its own task dispatches too (#546:
@@ -2466,10 +2477,7 @@ export function cmdTaskQueue(io: Io, slug: string, o: QueueFilter): number {
2466
2477
  if (found.length >= 100) io.out("○ the search gave 100 open tasks — the queue sees only them: narrow it with --epic");
2467
2478
  numbers = found.filter((x) => x?.state === "OPEN").map((x) => x.number);
2468
2479
  }
2469
- const [owner, name] = slug.split("/");
2470
- const recent = (graphql(io, Q.RecentPrs, { owner, name })?.repository?.pullRequests?.nodes ?? []).filter((p: Any) => p.state === "OPEN");
2471
- const openPr = (i: Issue) =>
2472
- 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);
2473
2481
  const handed = handedOut(io, slug);
2474
2482
  /** Issue states the queue has read; a hand-out's task outside the queue is read when its worktree is up for removal. */
2475
2483
  const states = new Map<number, string>();
@@ -2930,8 +2938,8 @@ export function cmdTaskAssignment(io: Io, slug: string, number: number, o: { clo
2930
2938
  const resume = restart.length ? [N.queue.resume] : [];
2931
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
2932
2940
  const lines = o.session
2933
- ? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.sessionFound, N.queue.sessionQuestion, N.queue.sessionEnd]
2934
- : [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];
2935
2943
  for (const line of lines) io.out(locale.fill(line, vars));
2936
2944
  if (o.session) {
2937
2945
  io.out(
@@ -3124,8 +3132,9 @@ const textBlock = (body: string) => (body ? `~~~~markdown\n${body}\n~~~~` : N.ac
3124
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
3125
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
3126
3134
  * copies of links and the answers in comments the script found, the checks by meaning quoted from reference.md as is,
3127
- * what may be changed and the answer form; in the project language (the `actualize` keys). No epic, or no open tasks in
3128
- * it besides this one — the checks by meaning have nothing to go through: a "not needed" line, no subagent.
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.
3129
3138
  */
3130
3139
  export function cmdTaskActualize(io: Io, slug: string, number: number, o: { assignment: boolean } = { assignment: false }): number {
3131
3140
  const issue = loadIssue(io, slug, number);
@@ -3137,6 +3146,8 @@ export function cmdTaskActualize(io: Io, slug: string, number: number, o: { assi
3137
3146
  const open = epic.subIssues.filter((x) => x.state === "OPEN" && x.number !== number).map((x) => loadTaskText(io, slug, x.number));
3138
3147
  if (!o.assignment) refreshMechanics(io, slug, { number, merged: prs.length > 0, epic, open });
3139
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;
3140
3151
  const vars = { task: N.subagentPrompt.task, epicWord: N.subagentPrompt.epic, number, epic: epic.number, repo: slug };
3141
3152
  const lead = locale.fill(N.actualize.lead, vars);
3142
3153
  if (!o.assignment) {
@@ -3155,7 +3166,17 @@ export function cmdTaskActualize(io: Io, slug: string, number: number, o: { assi
3155
3166
  if (files.length) lines.push(locale.fill(N.actualize.files, { pr: p.number }), ...files.map(fileLine));
3156
3167
  }
3157
3168
  lines.push("", locale.fill(N.actualize.epic, { ...vars, title: epic.title }), textBlock(epic.body), "", N.actualize.openTasks);
3158
- for (const t of open) lines.push("", locale.fill(N.actualize.openTask, { number: t.number, title: t.title }), textBlock(t.body));
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));
3159
3180
  const copies = linkCopies(epic, open);
3160
3181
  if (copies.length) lines.push("", N.actualize.copies, ...copies.map((c) => locale.fill(N.actualize.copy, c)));
3161
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;