@miroshnik/elves 2026.1018.2 → 2026.1019.0
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/dist/github/scripts/github.js +32 -12
- package/dist/spec/scripts/architecture.js +2 -2
- package/dist/spec/scripts/{harness-v9s1v9w3.js → harness-ay62ws50.js} +1 -1
- package/dist/spec/scripts/{harness-qzb932c8.js → harness-wqrt5gpa.js} +18 -2
- package/dist/spec/scripts/harness.js +2 -2
- package/package.json +3 -2
- package/skills/ci-wait/SKILL.md +3 -1
- package/skills/ci-wait/scripts/wait-ci.sh +8 -3
- package/skills/github/SKILL.md +7 -7
- package/skills/github/locales/en.json +9 -1
- package/skills/github/locales/ru.json +9 -1
- package/skills/github/reference.md +28 -5
- package/skills/github/scripts/github.ts +57 -16
- package/skills/github/scripts/locale.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
3
|
locale_default
|
|
4
|
-
} from "../../spec/scripts/harness-
|
|
4
|
+
} from "../../spec/scripts/harness-wqrt5gpa.js";
|
|
5
5
|
|
|
6
6
|
// skills/github/scripts/github.ts
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
@@ -1280,6 +1280,7 @@ var handedOutFile = (io) => {
|
|
|
1280
1280
|
return file && `${file}.tasks`;
|
|
1281
1281
|
};
|
|
1282
1282
|
var CLOUD_HANDOUT = "@cloud";
|
|
1283
|
+
var SESSION_HANDOUT = "@session";
|
|
1283
1284
|
function handedOut(io, slug) {
|
|
1284
1285
|
const file = handedOutFile(io);
|
|
1285
1286
|
return file ? handOutsIn(file, slug) : [];
|
|
@@ -1292,7 +1293,8 @@ function handOutsIn(file, slug) {
|
|
|
1292
1293
|
return lines.filter((l) => l.startsWith(`${slug}#`)).map((l) => {
|
|
1293
1294
|
const [number, ...rest] = l.slice(slug.length + 1).split(" ");
|
|
1294
1295
|
const worktree = rest.join(" ").trim();
|
|
1295
|
-
|
|
1296
|
+
const executor = worktree === CLOUD_HANDOUT ? "cloud" : worktree === SESSION_HANDOUT ? "session" : "subagent";
|
|
1297
|
+
return { number: Number(number), worktree: executor === "subagent" ? worktree || null : null, executor };
|
|
1296
1298
|
}).filter((h) => Number.isInteger(h.number) && h.number > 0);
|
|
1297
1299
|
}
|
|
1298
1300
|
function realPath(p) {
|
|
@@ -1809,8 +1811,10 @@ function cmdTaskQueue(io, slug, o) {
|
|
|
1809
1811
|
const pr = openPr(i);
|
|
1810
1812
|
const work = [status === N.status.inProgress ? q(N.status.inProgress) : "", pr === undefined ? "" : `open PR #${pr} https://github.com/${slug}/pull/${pr}`].filter(Boolean);
|
|
1811
1813
|
const hand = handed.find((h) => h.number === i.number);
|
|
1812
|
-
if (!work.length && hand?.cloud)
|
|
1814
|
+
if (!work.length && hand?.executor === "cloud")
|
|
1813
1815
|
work.push(`handed out to a cloud executor of this session, its wait — a background command: bash ${waitCi()} issue ${i.number}`);
|
|
1816
|
+
else if (!work.length && hand?.executor === "session")
|
|
1817
|
+
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
1818
|
else if (!work.length && hand)
|
|
1815
1819
|
work.push("handed out to an executor of this session");
|
|
1816
1820
|
if (work.length) {
|
|
@@ -2079,7 +2083,7 @@ function taskBranchesHere(io, slug, number) {
|
|
|
2079
2083
|
const refs = [...git("for-each-ref", "--format=%(refname)", "refs/heads/"), ...git("ls-remote", "--heads", "origin").map((l) => l.split("\t")[1] ?? "")];
|
|
2080
2084
|
return [...new Set(refs.map((r) => r.trim().replace(/^refs\/heads\//, "")).filter((b) => isTaskBranch(b, number)))].sort();
|
|
2081
2085
|
}
|
|
2082
|
-
function cmdTaskAssignment(io, slug, number, o = {
|
|
2086
|
+
function cmdTaskAssignment(io, slug, number, o = {}) {
|
|
2083
2087
|
const issue = loadIssue(io, slug, number);
|
|
2084
2088
|
const file = handedOutFile(io);
|
|
2085
2089
|
const own = pinnedTask(io)?.task;
|
|
@@ -2096,7 +2100,7 @@ function cmdTaskAssignment(io, slug, number, o = { cloud: false }) {
|
|
|
2096
2100
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
2097
2101
|
writeFileSync(path.join(path.dirname(file), ".gitignore"), `*
|
|
2098
2102
|
`);
|
|
2099
|
-
const where = o.cloud ? CLOUD_HANDOUT : worktree;
|
|
2103
|
+
const where = o.cloud ? CLOUD_HANDOUT : o.session ? SESSION_HANDOUT : worktree;
|
|
2100
2104
|
appendFileSync(file, `${slug}#${number}${where ? ` ${where}` : ""}
|
|
2101
2105
|
`);
|
|
2102
2106
|
}
|
|
@@ -2114,11 +2118,17 @@ function cmdTaskAssignment(io, slug, number, o = { cloud: false }) {
|
|
|
2114
2118
|
inProgress: N.status.inProgress,
|
|
2115
2119
|
done: N.lastLine.done,
|
|
2116
2120
|
questions: N.sections.questions,
|
|
2117
|
-
branches: restart.map((b) => `\`${b}\``).join(", ")
|
|
2121
|
+
branches: restart.map((b) => `\`${b}\``).join(", "),
|
|
2122
|
+
doneQuestions: N.lastLine.questions,
|
|
2123
|
+
remaining: N.lastLine.remaining
|
|
2118
2124
|
};
|
|
2119
|
-
const
|
|
2125
|
+
const resume = restart.length ? [N.queue.resume] : [];
|
|
2126
|
+
const lines = o.session ? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.sessionFound, N.queue.sessionQuestion, N.queue.sessionEnd] : [N.queue.lead, ...resume, N.queue.subagent, N.queue.session, N.queue.wait, N.queue.git, N.queue.subagents, N.queue.live, N.queue.refresh, N.queue.found, N.queue.question, N.queue.answer];
|
|
2120
2127
|
for (const line of lines)
|
|
2121
2128
|
io.out(locale_default.fill(line, vars));
|
|
2129
|
+
if (o.session) {
|
|
2130
|
+
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}.`);
|
|
2131
|
+
}
|
|
2122
2132
|
return 0;
|
|
2123
2133
|
}
|
|
2124
2134
|
function actualizeChecks() {
|
|
@@ -2308,7 +2318,8 @@ function cmdTaskClose(io, slug, number, o) {
|
|
|
2308
2318
|
const prs = [...issue.prs, ...fresh].map((p) => `#${p.number} ${p.state === "OPEN" ? "open" : "closed without a merge"}`).join(", ");
|
|
2309
2319
|
const what = prs ? `PR ${prs}` : `the task is ${issue.state === "OPEN" ? "open" : "closed without being done"}, no merged PR`;
|
|
2310
2320
|
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
|
-
|
|
2321
|
+
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` : "";
|
|
2322
|
+
throw new GhError(`#${number} is too early to close: ${what}${checked} — close after the PR is merged or the task is closed${noPr}`);
|
|
2312
2323
|
}
|
|
2313
2324
|
if (issue.parent) {
|
|
2314
2325
|
const epic = loadTaskText(io, slug, issue.parent.number);
|
|
@@ -3121,7 +3132,7 @@ that can't be continued, in the worktree of its task's branch.
|
|
|
3121
3132
|
Examples:
|
|
3122
3133
|
github task resume`,
|
|
3123
3134
|
"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]
|
|
3135
|
+
github task queue --assignment <N> [--cloud | --session] [--repo owner/repo]
|
|
3125
3136
|
|
|
3126
3137
|
The dispatcher session's queue: ready tasks (open, not an epic, not in work, no open blockers — other repositories'
|
|
3127
3138
|
too —, no external blocker label) by Priority, then number, each with its executor's prompt; the rest — in work (=: In
|
|
@@ -3140,13 +3151,17 @@ such a subagent at work or HEAD moved in the last ${QUIET_MINUTES} min — code
|
|
|
3140
3151
|
dispatcher wait, no hand-out. --cloud — printed by the dispatcher: the brief of a cloud executor (claude.ai/code) —
|
|
3141
3152
|
whole, # only at its task — and the dispatcher's steps: a cloud session can't message a local one, so the dispatcher
|
|
3142
3153
|
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.
|
|
3154
|
+
queue's = line names that wait. --session — printed by the dispatcher: the prompt of an app executor session
|
|
3155
|
+
(start_session in the folder of another project, its own worktree) — a session by its project's canon and mode, named
|
|
3156
|
+
by the task, waiting in the background, ending with the canon's last line — and how to start it; the hand-out has no
|
|
3157
|
+
worktree, the task is = in the queue.
|
|
3144
3158
|
|
|
3145
3159
|
Examples:
|
|
3146
3160
|
github task queue --epic 40
|
|
3147
3161
|
github task queue --type Bug --priority Urgent,High
|
|
3148
3162
|
github task queue --assignment 42
|
|
3149
|
-
github task queue --assignment 42 --cloud
|
|
3163
|
+
github task queue --assignment 42 --cloud
|
|
3164
|
+
github task queue --assignment 42 --session`,
|
|
3150
3165
|
"task close": `github task close <N> [--no-git] [--repo owner/repo]
|
|
3151
3166
|
|
|
3152
3167
|
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 +3314,7 @@ function main(argv, io) {
|
|
|
3299
3314
|
"no-git": { type: "boolean", default: false },
|
|
3300
3315
|
assignment: { type: "boolean", default: false },
|
|
3301
3316
|
cloud: { type: "boolean", default: false },
|
|
3317
|
+
session: { type: "boolean", default: false },
|
|
3302
3318
|
auto: { type: "boolean", default: false }
|
|
3303
3319
|
}
|
|
3304
3320
|
});
|
|
@@ -3324,10 +3340,14 @@ function main(argv, io) {
|
|
|
3324
3340
|
if (cmd === "queue") {
|
|
3325
3341
|
if (values.cloud && !values.assignment)
|
|
3326
3342
|
throw new GhError("--cloud — only with --assignment <N>: the brief of one task's cloud executor");
|
|
3343
|
+
if (values.session && !values.assignment)
|
|
3344
|
+
throw new GhError("--session — only with --assignment <N>: the prompt of one task's app executor session");
|
|
3345
|
+
if (values.cloud && values.session)
|
|
3346
|
+
throw new GhError("--cloud or --session, not both: a cloud executor or an app session");
|
|
3327
3347
|
if (values.assignment) {
|
|
3328
3348
|
if (positionals.length !== 1)
|
|
3329
3349
|
throw new GhError("task queue --assignment <N>: one task number");
|
|
3330
|
-
return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud });
|
|
3350
|
+
return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud, session: values.session });
|
|
3331
3351
|
}
|
|
3332
3352
|
if (positionals.length)
|
|
3333
3353
|
throw new GhError(`extra arguments: ${positionals.join(" ")} — the queue is chosen by --epic, --type and --priority`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
locale_default
|
|
3
|
-
} from "./harness-
|
|
3
|
+
} from "./harness-wqrt5gpa.js";
|
|
4
4
|
import {
|
|
5
5
|
invariant2,
|
|
6
6
|
codeOnly2,
|
|
7
7
|
envNamesInCode2
|
|
8
|
-
} from "./harness-
|
|
8
|
+
} from "./harness-ay62ws50.js";
|
|
9
9
|
|
|
10
10
|
// skills/spec/scripts/architecture.ts
|
|
11
11
|
import { readdirSync, readFileSync } from "node:fs";
|
|
@@ -91,6 +91,7 @@ var en_default = {
|
|
|
91
91
|
git: "- git — in separate calls, without `cd`, `&&` and `|`: worktree isolation refuses a compound command with git;",
|
|
92
92
|
subagents: "- writing subagents — launch as needed, no limit: each gets its own worktree and «{task} #{number}» at the start of its prompt (`est` links its work by it); bring their commits into your branch, remove their worktrees and branches yourself after the merge — what didn't go, name in the answer;",
|
|
93
93
|
live: "- while any of your subagents works, don't answer: wait for their answers — a background subagent wakes you with its answer; an answer with live subagents looks like a failure, and the dispatcher would give the task to a second executor next to you. Can't wait — answer `waits for subagents: <which and what they do>`: it isn't `failed`, there will be no second executor;",
|
|
94
|
+
refresh: '- the epic refresh — the subagent `task actualize` prints, without a worktree of its own (in yours) and not in the background (`run_in_background: false`): its answer — in the same turn, then `{script} task close {number}`, and only then the answer to the dispatcher: once you answer, a worktree without changes is removed, and a background subagent is left without a directory; a task without a PR (a data edit, a reconciliation, research) — first close the issue as completed with a result comment: `gh issue close {number} --repo {repo} --reason completed --comment "<result>"`;',
|
|
94
95
|
found: "- work found outside the task — `{script} task new …` (work of the task's epic — with `--epic`: the dispatcher picks it up) — name it in the answer: whether to queue it is the owner's call;",
|
|
95
96
|
question: "- a question without a clear recommendation — into «{questions}» of the task; don't start the code that depends on it and answer the dispatcher: the owner's answer comes back here.",
|
|
96
97
|
answer: "The answer to the dispatcher — one line: `merged #<PR>` · `question: <the question, options, recommendation>` · `waits for subagents: <which and what they do>` · `failed: <what stopped and where>`.",
|
|
@@ -100,7 +101,14 @@ var en_default = {
|
|
|
100
101
|
cloudDoc: "- the cloud — by `docs/cloud-sessions.md` of the canon, read it first: of the github skill's scripts `task show`, `task answer` and `task new` work there (over REST), the rest refuse, the scenarios section — by editing the description over REST, project fields aren't set from the cloud; the branch is the one the cloud assigned, don't rename it;",
|
|
101
102
|
cloudClose: "- after the merge — its section «Closing a task from the cloud»: the «{actual}» comment of `est fact {number}` and a «{state}» comment with what the cloud can't do — both into the task;",
|
|
102
103
|
cloudDispatcher: "- the dispatcher waits for the task to close and closes it locally — the actual, the status, the merged branch, archiving this session: don't message it, don't archive the session;",
|
|
103
|
-
cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task."
|
|
104
|
+
cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task.",
|
|
105
|
+
sessionLead: "{task} #{number} «{title}» in {repo}{epic}: you are an executor session of the dispatcher session — do the task whole by the canon of this project, in its mode (`check` prints it; outside auto — commit, push and merge on the owner's «yes»): the task's questions, `{script} task status {number} \"{inProgress}\" --branch <type>/{number}-<slug>`, the branch from a fresh origin/main, code and tests, PR, green CI, `pr premerge`, merge, deploy, the epic refresh, `{script} task close {number}`.",
|
|
106
|
+
sessionIs: "You are a session, not a subagent:",
|
|
107
|
+
sessionName: "- name the session `#{number} {title}` (`set_session_title`) once the task is taken — `est` links the session to the task by it;",
|
|
108
|
+
sessionWait: "- wait for CI and the deploy with a background command — `{waitCi} pr <PR>` (after the merge — `merged <PR>`): its completion wakes the session; no `Monitor`, watcher loop or `ScheduleWakeup` on top;",
|
|
109
|
+
sessionFound: "- work found outside the task — `{script} task new …` and the question «queue it?» by the canon; work of the task's epic — with `--epic` and without an executor of yours: the dispatcher's queue picks it up;",
|
|
110
|
+
sessionQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session; don't start the code that depends on it: the answer comes here, from the owner or the dispatcher;",
|
|
111
|
+
sessionEnd: "- the end — the canon's last line («{done}» · «{doneQuestions} …» · «{remaining} …»), in auto mode after «{done}» — archiving the session by the canon: the dispatcher reads the end of each of your turns as your answer."
|
|
104
112
|
},
|
|
105
113
|
est: {
|
|
106
114
|
answer: "Answer",
|
|
@@ -541,6 +549,7 @@ var ru_default = {
|
|
|
541
549
|
git: "- git — отдельными вызовами, без `cd`, `&&` и `|`: составную команду с git изоляция worktree не пропускает;",
|
|
542
550
|
subagents: "- пишущих субагентов запускай по необходимости, без лимита: каждому — свой worktree и «{task} #{number}» в начале промпта (по нему `est` привяжет его работу); их коммиты перенеси в свою ветку, их worktree и ветки после мержа убери сам — что не убралось, назови в ответе;",
|
|
543
551
|
live: "- пока работает хоть один твой субагент, ответ не отдавай: жди их ответов — фоновый субагент будит тебя своим ответом; ответ при живых субагентах выглядит провалом, и диспетчер отдал бы задачу второму исполнителю рядом с тобой. Ждать нельзя — ответь `ждёт субагентов: <какие и что делают>`: это не «сломалось», второго исполнителя не будет;",
|
|
552
|
+
refresh: '- актуализация эпика — субагент, которого печатает `task actualize`, без своего worktree (в твоём) и не в фоне (`run_in_background: false`): его ответ — в том же ходе, затем `{script} task close {number}`, и только потом ответ диспетчеру: стоит тебе ответить, worktree без изменений убирают, и фоновый субагент остаётся без каталога; у задачи без PR (правка данных, сверка, исследование) — сначала закрой issue как выполненную с итоговым комментарием: `gh issue close {number} --repo {repo} --reason completed --comment "<итог>"`;',
|
|
544
553
|
found: "- найденная работа вне задачи — `{script} task new …` (работа эпика задачи — с `--epic`: диспетчер её подхватит) — назови её в ответе: ставить ли её в очередь, решает владелец;",
|
|
545
554
|
question: "- вопрос без ясной рекомендации — в «{questions}» задачи; код, который от него зависит, не начинай и ответь диспетчеру: ответ владельца придёт сюда же.",
|
|
546
555
|
answer: "Ответ диспетчеру — одной строкой: `влито #<PR>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
|
|
@@ -550,7 +559,14 @@ var ru_default = {
|
|
|
550
559
|
cloudDoc: "- облако — по `docs/cloud-sessions.md` канона, прочитай его первым: из скриптов скилла github там работают `task show`, `task answer` и `task new` (через REST), остальные отказывают, раздел сценариев — правкой описания через REST, поля проекта из облака не ставятся; ветка — та, что назначило облако, не переименовывай;",
|
|
551
560
|
cloudClose: "- после мержа — его раздел «Closing a task from the cloud»: комментарий «{actual}» от `est fact {number}` и комментарий «{state}» с тем, что из облака не сделать, — оба в задачу;",
|
|
552
561
|
cloudDispatcher: "- диспетчер ждёт закрытия задачи и закрывает её локально — факт, статус, влитая ветка, архив этой сессии: сообщать ему не нужно, сессию не архивируй;",
|
|
553
|
-
cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу."
|
|
562
|
+
cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу.",
|
|
563
|
+
sessionLead: '{task} #{number} «{title}» в {repo}{epic}: ты сессия-исполнитель сессии-диспетчера — сделай задачу целиком по канону этого проекта, в его режиме (его печатает `check`; не в auto — коммит, пуш и мерж по «да» владельца): вопросы задачи, `{script} task status {number} "{inProgress}" --branch <тип>/{number}-<slug>`, ветка от свежего origin/main, код и тесты, PR, зелёный CI, `pr premerge`, мерж, деплой, актуализация эпика, `{script} task close {number}`.',
|
|
564
|
+
sessionIs: "Ты сессия, а не субагент:",
|
|
565
|
+
sessionName: "- назови сессию `#{number} {title}` (`set_session_title`), как только взял задачу, — по имени `est` привяжет сессию к задаче;",
|
|
566
|
+
sessionWait: "- CI и деплой жди фоновой командой — `{waitCi} pr <PR>` (после мержа — `merged <PR>`): её завершение будит сессию; без `Monitor`, цикла-наблюдателя и `ScheduleWakeup` поверх;",
|
|
567
|
+
sessionFound: "- найденная работа вне задачи — `{script} task new …` и вопрос «ставить в очередь?» по канону; работа эпика задачи — с `--epic` и без твоего исполнителя: её подхватит очередь диспетчера;",
|
|
568
|
+
sessionQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии; код, который от него зависит, не начинай: ответ придёт сюда же — от владельца или диспетчера;",
|
|
569
|
+
sessionEnd: "- конец — последняя строка канона («{done}» · «{doneQuestions} …» · «{remaining} …»), в режиме auto после «{done}» — архивация сессии по канону: конец каждого твоего хода диспетчер читает как твой ответ."
|
|
554
570
|
},
|
|
555
571
|
est: {
|
|
556
572
|
answer: "Ответ",
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
envNamesInCode2,
|
|
24
24
|
dotenvNames2,
|
|
25
25
|
envVars2
|
|
26
|
-
} from "./harness-
|
|
27
|
-
import"./harness-
|
|
26
|
+
} from "./harness-ay62ws50.js";
|
|
27
|
+
import"./harness-wqrt5gpa.js";
|
|
28
28
|
export {
|
|
29
29
|
ENV_SERVICE2 as ENV_SERVICE,
|
|
30
30
|
codeFiles2 as codeFiles,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miroshnik/elves",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1019.0",
|
|
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": {
|
|
@@ -66,9 +66,10 @@
|
|
|
66
66
|
"@playwright/test": "^1.63.0",
|
|
67
67
|
"@types/bun": "^1.4.2",
|
|
68
68
|
"@types/node": "^26.6.2",
|
|
69
|
+
"corepack": "^0.36.0",
|
|
69
70
|
"eslint": "^10.11.0",
|
|
70
71
|
"eslint-plugin-boundaries": "^7.2.0",
|
|
71
72
|
"knip": "^6.41.0"
|
|
72
73
|
},
|
|
73
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "42576112e74dfdc383f64b0edb5e08a24ff4afa2"
|
|
74
75
|
}
|
package/skills/ci-wait/SKILL.md
CHANGED
|
@@ -173,7 +173,9 @@ What to do next:
|
|
|
173
173
|
(a preview deploy) won't change it, so FAIL comes at once.
|
|
174
174
|
- PASS counts only when a snapshot without pending repeats for two polls in
|
|
175
175
|
a row; `--expect N` — the minimum number of checks, below which we keep
|
|
176
|
-
waiting.
|
|
176
|
+
waiting. A snapshot without pending on the deadline gets its confirming
|
|
177
|
+
poll past it, at most one `--interval` (#627): otherwise the last poll,
|
|
178
|
+
which lands on the deadline, gave TIMEOUT at "N/N done".
|
|
177
179
|
- Without branch protection `gh pr merge --auto` merges without waiting for
|
|
178
180
|
CI — so `--auto` doesn't replace waiting.
|
|
179
181
|
- One wait at a time: don't start a second `ci-wait` on the same PR in
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
# The spec publish job (spec-publish) is not a check: neither awaited nor a FAIL — only by --context.
|
|
13
13
|
# An empty list of checks — pending; PASS counts when a snapshot without pending repeats
|
|
14
14
|
# two polls in a row (late-registering checks don't slip through) and there are no fewer checks
|
|
15
|
-
# than --expect
|
|
15
|
+
# than --expect; a snapshot without pending on the deadline gets its confirming poll past it, at most one
|
|
16
|
+
# interval. The first failed check — FAIL right away; --wait-all — FAIL when all have finished.
|
|
16
17
|
# --interval is the first interval: it grows with the wait — a tenth of the elapsed time, up to 5 min.
|
|
17
18
|
# REST only: the GraphQL quota is shared by all sessions; a REST rate limit — waiting until the reset, and that wait
|
|
18
19
|
# doesn't count toward --timeout (up to an hour). A gh failure (also in requests before the first poll) — retry, ERROR
|
|
@@ -370,8 +371,12 @@ while :; do
|
|
|
370
371
|
echo "$(ts) $label: gh error ($errors): ${out:0:200}" >&2
|
|
371
372
|
[ "$errors" -ge 5 ] && finish "ERROR gh: ${out:0:200}" 3
|
|
372
373
|
fi
|
|
374
|
+
# the deadline doesn't take away an outcome already seen (#627): the last poll lands on the deadline (the nap is cut to
|
|
375
|
+
# it), and a snapshot without pending there awaited its confirming poll — TIMEOUT at "N/N done". Such a snapshot gets
|
|
376
|
+
# its confirming poll at the first interval, past the deadline too, but at most one interval past it
|
|
377
|
+
limit=$deadline; [ -n "$prev_terminal" ] && limit=$((deadline + interval))
|
|
373
378
|
now=$(date +%s)
|
|
374
|
-
[ "$now" -ge "$
|
|
375
|
-
[ "$nap" -gt $((
|
|
379
|
+
[ "$now" -ge "$limit" ] && finish "TIMEOUT after $(dur $((now - start))) ($full)" 2
|
|
380
|
+
[ "$nap" -gt $((limit - now)) ] && nap=$((limit - now))
|
|
376
381
|
sleep "$nap"
|
|
377
382
|
done
|
package/skills/github/SKILL.md
CHANGED
|
@@ -148,26 +148,26 @@ session, its own task stays pinned; one started for it — in the project
|
|
|
148
148
|
folder, not a worktree: executors (subagents, cloud sessions) start from
|
|
149
149
|
`main`. `task queue` prints ready
|
|
150
150
|
tasks, each with an indented line — its executor's prompt. In Claude Code: an executor — `Agent` with
|
|
151
|
-
`isolation: "worktree"` and `run_in_background: true`, the prompt —
|
|
152
|
-
|
|
151
|
+
`isolation: "worktree"` and `run_in_background: true`, the prompt — as
|
|
152
|
+
is; at most 3 subagents and 5 cloud sessions at once, the
|
|
153
153
|
owner's number replaces either; another project's task — an app session in
|
|
154
154
|
its folder (`start_session`, own worktree);
|
|
155
155
|
`=` — a task in work (In progress, an open PR, handed out to an executor of
|
|
156
156
|
this session), not launched again. Before each launch — `get_usage`: the 5-hour limit at 85% and more —
|
|
157
157
|
no new executors, the running ones finish, then `Remaining: N tasks, the
|
|
158
|
-
limit`. After each executor's answer — `task queue` again: it
|
|
159
|
-
finished executors' worktrees (`+`)
|
|
158
|
+
limit`. After each executor's answer — `task queue` again: it alone
|
|
159
|
+
removes finished executors' worktrees (`+`), not `git worktree remove`; its last line names the step:
|
|
160
160
|
an executor session whose task closed — the dispatcher archives it in an
|
|
161
161
|
auto project, not one still working in the background or awaiting the
|
|
162
162
|
owner's answer; outside auto the owner closes it. A question — to the
|
|
163
|
-
owner in text; the answer — ✅ in the task
|
|
163
|
+
owner in text; the answer — ✅ in the task and to the same
|
|
164
164
|
executor with `SendMessage`. After the app restarts — each stopped executor
|
|
165
165
|
is continued with `SendMessage` by its id, not replaced (`task resume`). Waits for subagents — it answers again. Failed — a
|
|
166
166
|
task comment, no restart (the owner's restart:
|
|
167
167
|
`--assignment` refuses while the previous executor lives); external
|
|
168
168
|
blockers — `reference.md`, "Dependencies". Queue empty and nothing running — the summary, the last
|
|
169
|
-
line (a session with its own task — after it) and, in auto mode, archiving. The executor's side (`--assignment
|
|
170
|
-
|
|
169
|
+
line (a session with its own task — after it) and, in auto mode, archiving. The executor's side (`--assignment`;
|
|
170
|
+
`--cloud`, `--session`) — `reference.md`, "task queue".
|
|
171
171
|
|
|
172
172
|
## Git, PRs and merging — mechanics
|
|
173
173
|
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"git": "- git — in separate calls, without `cd`, `&&` and `|`: worktree isolation refuses a compound command with git;",
|
|
88
88
|
"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
89
|
"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
|
+
"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>\"`;",
|
|
90
91
|
"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;",
|
|
91
92
|
"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.",
|
|
92
93
|
"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>`.",
|
|
@@ -96,7 +97,14 @@
|
|
|
96
97
|
"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;",
|
|
97
98
|
"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;",
|
|
98
99
|
"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;",
|
|
99
|
-
"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."
|
|
100
|
+
"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.",
|
|
101
|
+
"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}`.",
|
|
102
|
+
"sessionIs": "You are a session, not a subagent:",
|
|
103
|
+
"sessionName": "- name the session `#{number} {title}` (`set_session_title`) once the task is taken — `est` links the session to the task by it;",
|
|
104
|
+
"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;",
|
|
105
|
+
"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;",
|
|
106
|
+
"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;",
|
|
107
|
+
"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."
|
|
100
108
|
},
|
|
101
109
|
"est": {
|
|
102
110
|
"answer": "Answer",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"git": "- git — отдельными вызовами, без `cd`, `&&` и `|`: составную команду с git изоляция worktree не пропускает;",
|
|
88
88
|
"subagents": "- пишущих субагентов запускай по необходимости, без лимита: каждому — свой worktree и «{task} #{number}» в начале промпта (по нему `est` привяжет его работу); их коммиты перенеси в свою ветку, их worktree и ветки после мержа убери сам — что не убралось, назови в ответе;",
|
|
89
89
|
"live": "- пока работает хоть один твой субагент, ответ не отдавай: жди их ответов — фоновый субагент будит тебя своим ответом; ответ при живых субагентах выглядит провалом, и диспетчер отдал бы задачу второму исполнителю рядом с тобой. Ждать нельзя — ответь `ждёт субагентов: <какие и что делают>`: это не «сломалось», второго исполнителя не будет;",
|
|
90
|
+
"refresh": "- актуализация эпика — субагент, которого печатает `task actualize`, без своего worktree (в твоём) и не в фоне (`run_in_background: false`): его ответ — в том же ходе, затем `{script} task close {number}`, и только потом ответ диспетчеру: стоит тебе ответить, worktree без изменений убирают, и фоновый субагент остаётся без каталога; у задачи без PR (правка данных, сверка, исследование) — сначала закрой issue как выполненную с итоговым комментарием: `gh issue close {number} --repo {repo} --reason completed --comment \"<итог>\"`;",
|
|
90
91
|
"found": "- найденная работа вне задачи — `{script} task new …` (работа эпика задачи — с `--epic`: диспетчер её подхватит) — назови её в ответе: ставить ли её в очередь, решает владелец;",
|
|
91
92
|
"question": "- вопрос без ясной рекомендации — в «{questions}» задачи; код, который от него зависит, не начинай и ответь диспетчеру: ответ владельца придёт сюда же.",
|
|
92
93
|
"answer": "Ответ диспетчеру — одной строкой: `влито #<PR>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
|
|
@@ -96,7 +97,14 @@
|
|
|
96
97
|
"cloudDoc": "- облако — по `docs/cloud-sessions.md` канона, прочитай его первым: из скриптов скилла github там работают `task show`, `task answer` и `task new` (через REST), остальные отказывают, раздел сценариев — правкой описания через REST, поля проекта из облака не ставятся; ветка — та, что назначило облако, не переименовывай;",
|
|
97
98
|
"cloudClose": "- после мержа — его раздел «Closing a task from the cloud»: комментарий «{actual}» от `est fact {number}` и комментарий «{state}» с тем, что из облака не сделать, — оба в задачу;",
|
|
98
99
|
"cloudDispatcher": "- диспетчер ждёт закрытия задачи и закрывает её локально — факт, статус, влитая ветка, архив этой сессии: сообщать ему не нужно, сессию не архивируй;",
|
|
99
|
-
"cloudQuestion": "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу."
|
|
100
|
+
"cloudQuestion": "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу.",
|
|
101
|
+
"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}`.",
|
|
102
|
+
"sessionIs": "Ты сессия, а не субагент:",
|
|
103
|
+
"sessionName": "- назови сессию `#{number} {title}` (`set_session_title`), как только взял задачу, — по имени `est` привяжет сессию к задаче;",
|
|
104
|
+
"sessionWait": "- CI и деплой жди фоновой командой — `{waitCi} pr <PR>` (после мержа — `merged <PR>`): её завершение будит сессию; без `Monitor`, цикла-наблюдателя и `ScheduleWakeup` поверх;",
|
|
105
|
+
"sessionFound": "- найденная работа вне задачи — `{script} task new …` и вопрос «ставить в очередь?» по канону; работа эпика задачи — с `--epic` и без твоего исполнителя: её подхватит очередь диспетчера;",
|
|
106
|
+
"sessionQuestion": "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии; код, который от него зависит, не начинай: ответ придёт сюда же — от владельца или диспетчера;",
|
|
107
|
+
"sessionEnd": "- конец — последняя строка канона («{done}» · «{doneQuestions} …» · «{remaining} …»), в режиме auto после «{done}» — архивация сессии по канону: конец каждого твоего хода диспетчер читает как твой ответ."
|
|
100
108
|
},
|
|
101
109
|
"est": {
|
|
102
110
|
"answer": "Ответ",
|
|
@@ -727,7 +727,10 @@ its epic refresh still ran in its directory, and the queue removed it): such a
|
|
|
727
727
|
subagent — `parentAgentId` without its own `worktreePath` in Claude Code's
|
|
728
728
|
subagent metadata of this session and of the sessions with hand-outs
|
|
729
729
|
(`dispatchers/*.tasks`), at any depth; at work — its transcript
|
|
730
|
-
(`agent-<id>.jsonl`) written in the last 15 minutes.
|
|
730
|
+
(`agent-<id>.jsonl`) written in the last 15 minutes. An executor's worktree is
|
|
731
|
+
removed only by the queue, not by the dispatcher's `git worktree remove` after
|
|
732
|
+
a clean `git status` (#628): a live subagent without a worktree of its own
|
|
733
|
+
leaves no trace in `git status`, and its directory goes from under it.
|
|
731
734
|
A session started to dispatch works in the project folder, not a worktree
|
|
732
735
|
(#588): the app starts a cloud session from the branch in the starting
|
|
733
736
|
session's record (`get_session` → `branch`), not from git — a session from
|
|
@@ -771,7 +774,14 @@ branches removed by the executor after the merge, no answer while any of its
|
|
|
771
774
|
subagents works (#565: an answer with live subagents reads as a failure — the
|
|
772
775
|
executor of #545 answered failed, its subagents' answers woke it, and it merged
|
|
773
776
|
the task while a second executor, launched on the owner's restart, worked on the
|
|
774
|
-
same branch),
|
|
777
|
+
same branch), the epic refresh — the subagent of `task actualize` without a
|
|
778
|
+
worktree of its own and not in the background: its answer in the same turn,
|
|
779
|
+
then `task close`, then the answer (#628: an executor of a task without
|
|
780
|
+
commits answered «waits for subagents» while its refresh ran, and its
|
|
781
|
+
worktree, without changes, was removed under the refresh subagent once it
|
|
782
|
+
answered), a task without a PR — first the issue closed as completed with a
|
|
783
|
+
result comment (`gh issue close N --reason completed --comment …`: `task
|
|
784
|
+
close` needs it closed), found work — `task new`,
|
|
775
785
|
named in the answer (whether to queue it is the owner's call); the answer — one
|
|
776
786
|
line: merged, a question, waits for subagents (it can't wait — not a failure) or
|
|
777
787
|
failed.
|
|
@@ -815,7 +825,13 @@ The app's sessions are executors too: another project's task — an app session
|
|
|
815
825
|
in its folder (`start_session` with its own worktree), while a subagent works
|
|
816
826
|
in a worktree of the dispatcher's repository. Such a session works as a session
|
|
817
827
|
of its project, by its mode; the end of its turn comes as the app's note, like
|
|
818
|
-
an answer, — `task queue` again.
|
|
828
|
+
an answer, — `task queue` again. Its prompt — `--assignment N --session`,
|
|
829
|
+
called by the dispatcher itself (#624): the lines before `Next:` as is — a
|
|
830
|
+
session, not a subagent, so the subagent's lines don't apply: by the canon of its
|
|
831
|
+
project and its mode, named `#N <title>`, CI and the deploy waited for with a
|
|
832
|
+
background command, the canon's last line at the end; `Next:` — `start_session`
|
|
833
|
+
with `cwd` the project folder and `use_worktree` true. The hand-out — without a
|
|
834
|
+
worktree (`@session`): the task is `=` in the queue. Archiving, app and cloud executors alike
|
|
819
835
|
(#621): an executor session whose task closed — the dispatcher archives it in
|
|
820
836
|
an auto project, not one still working in the background or awaiting the
|
|
821
837
|
owner's answer; outside auto the owner closes it. Closed — `task close` passed:
|
|
@@ -888,7 +904,11 @@ the background the subagent with the assignment, if a refresh is needed; a wait
|
|
|
888
904
|
notification with code 0 — `task close N` right away, don't read the output
|
|
889
905
|
file (not 0 — the `RESULT:` line in it); the subagent hasn't reported yet —
|
|
890
906
|
close refuses before any change: close again after its answer; then the last
|
|
891
|
-
line of the answer and archiving — in one turn.
|
|
907
|
+
line of the answer and archiving — in one turn. A dispatcher's executor runs
|
|
908
|
+
the subagent not in the background and waits for its answer in the same turn,
|
|
909
|
+
before `task close` and its own answer (its assignment, #628): once it answers,
|
|
910
|
+
its worktree without changes is removed, and a background subagent is left
|
|
911
|
+
without a directory.
|
|
892
912
|
|
|
893
913
|
### task close
|
|
894
914
|
|
|
@@ -900,7 +920,10 @@ then the task's PR is searched among the default branch's recent PRs by the
|
|
|
900
920
|
branch `<type>/<N>-<slug>` and "Closes #N" in the body: merged — the task is
|
|
901
921
|
closed with the comment «Closed by PR #M: GitHub didn't link the PR to the
|
|
902
922
|
task», none — a refusal
|
|
903
|
-
listing what was checked (no manual `gh issue close` needed).
|
|
923
|
+
listing what was checked (no manual `gh issue close` needed). No PR at all and
|
|
924
|
+
the task open — the refusal also names the step a task without commits (a data
|
|
925
|
+
edit, a reconciliation, research) lacks: `gh issue close N --reason completed
|
|
926
|
+
--comment "<result>"`, then `task close N` again (#628). The epic has
|
|
904
927
|
open tasks besides this one and no report of the refresh on it — a comment
|
|
905
928
|
with «Refresh after #N, result:» in any language of the dictionary, case and
|
|
906
929
|
markup aside; the script's own «Refresh after #N, script: …» comes before the
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* github task close — after the merge and the deploy: the actual, "Done", the epic and the milestone, the task's
|
|
22
22
|
* stacks (task-env) and the task's merged branches removed
|
|
23
23
|
* github task queue — the dispatcher's ready tasks with the prompts of their executors (subagents);
|
|
24
|
-
* --assignment — an executor's whole assignment
|
|
25
|
-
*
|
|
24
|
+
* --assignment — an executor's whole assignment (--cloud, --session — the prompt of a cloud
|
|
25
|
+
* executor or an app executor session, printed by the dispatcher)
|
|
26
|
+
* github pr labels — decision labels for the task from "Closes #N" by the PR diff, for its epic — the union
|
|
26
27
|
* github pr premerge — before the merge: the default branch moved after the PR's CI — typecheck and test:spec on
|
|
27
28
|
* the merge with it
|
|
28
29
|
*
|
|
@@ -1773,23 +1774,29 @@ const isDispatcher = (io: Io) => {
|
|
|
1773
1774
|
* with `SendMessage` (`task resume`) — dropped, it would make a task not yet In progress ready for a second executor.
|
|
1774
1775
|
* The worktree — the checkout the executor called its assignment in (#533): the queue keeps it while the task is open. A
|
|
1775
1776
|
* cloud executor's (`--assignment N --cloud`, #609) — `CLOUD_HANDOUT` in its place: the dispatcher printed its brief,
|
|
1776
|
-
* there is no worktree, and the queue names the wait for the task's closing.
|
|
1777
|
+
* there is no worktree, and the queue names the wait for the task's closing. An app executor session's (`--assignment N
|
|
1778
|
+
* --session`, #624) — `SESSION_HANDOUT`: the dispatcher printed its prompt, the app makes the session's worktree later.
|
|
1777
1779
|
*/
|
|
1778
1780
|
const handedOutFile = (io: Io) => {
|
|
1779
1781
|
const file = dispatcherFile(io);
|
|
1780
1782
|
return file && `${file}.tasks`;
|
|
1781
1783
|
};
|
|
1782
1784
|
|
|
1783
|
-
/** A hand-out: the task and its executor's worktree; the assignment called outside a checkout or a
|
|
1785
|
+
/** A hand-out: the task and its executor's worktree; the assignment called outside a checkout, a cloud or a session one — null. */
|
|
1784
1786
|
interface HandOut {
|
|
1785
1787
|
number: number;
|
|
1786
1788
|
worktree: string | null;
|
|
1787
|
-
/**
|
|
1788
|
-
|
|
1789
|
+
/**
|
|
1790
|
+
* Who executes it: a subagent of this session; a cloud session (#609) — the dispatcher waits for the task's closing; an
|
|
1791
|
+
* app session (#624) — the end of each of its turns comes as the app's note.
|
|
1792
|
+
*/
|
|
1793
|
+
executor: "subagent" | "cloud" | "session";
|
|
1789
1794
|
}
|
|
1790
1795
|
|
|
1791
1796
|
/** A cloud executor's mark in place of the worktree: a worktree is an absolute path. */
|
|
1792
1797
|
const CLOUD_HANDOUT = "@cloud";
|
|
1798
|
+
/** An app executor session's mark in place of the worktree (#624): the app makes the session's worktree after the hand-out. */
|
|
1799
|
+
const SESSION_HANDOUT = "@session";
|
|
1793
1800
|
|
|
1794
1801
|
/** The repository's tasks handed out to this dispatcher's executors (`handedOutFile`); a line without a worktree — `null`. */
|
|
1795
1802
|
function handedOut(io: Io, slug: string): HandOut[] {
|
|
@@ -1806,7 +1813,8 @@ function handOutsIn(file: string, slug: string): HandOut[] {
|
|
|
1806
1813
|
.map((l) => {
|
|
1807
1814
|
const [number, ...rest] = l.slice(slug.length + 1).split(" ");
|
|
1808
1815
|
const worktree = rest.join(" ").trim();
|
|
1809
|
-
|
|
1816
|
+
const executor: HandOut["executor"] = worktree === CLOUD_HANDOUT ? "cloud" : worktree === SESSION_HANDOUT ? "session" : "subagent";
|
|
1817
|
+
return { number: Number(number), worktree: executor === "subagent" ? worktree || null : null, executor };
|
|
1810
1818
|
})
|
|
1811
1819
|
.filter((h) => Number.isInteger(h.number) && h.number > 0);
|
|
1812
1820
|
}
|
|
@@ -2429,7 +2437,9 @@ export function cmdTaskQueue(io: Io, slug: string, o: QueueFilter): number {
|
|
|
2429
2437
|
const work = [status === N.status.inProgress ? q(N.status.inProgress) : "", pr === undefined ? "" : `open PR #${pr} https://github.com/${slug}/pull/${pr}`].filter(Boolean);
|
|
2430
2438
|
const hand = handed.find((h) => h.number === i.number);
|
|
2431
2439
|
// a cloud executor's wait for the task's closing dies with the app's process: the line names it, task resume — the queue
|
|
2432
|
-
if (!work.length && hand?.cloud) work.push(`handed out to a cloud executor of this session, its wait — a background command: bash ${waitCi()} issue ${i.number}`);
|
|
2440
|
+
if (!work.length && hand?.executor === "cloud") work.push(`handed out to a cloud executor of this session, its wait — a background command: bash ${waitCi()} issue ${i.number}`);
|
|
2441
|
+
// an app executor session (#624): the app continues the dispatcher at the end of each of its turns
|
|
2442
|
+
else if (!work.length && hand?.executor === "session") 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");
|
|
2433
2443
|
else if (!work.length && hand) work.push("handed out to an executor of this session");
|
|
2434
2444
|
if (work.length) {
|
|
2435
2445
|
running.push(`= ${label} — ${work.join(", ")}`);
|
|
@@ -2812,8 +2822,14 @@ function taskBranchesHere(io: Io, slug: string, number: number): string[] {
|
|
|
2812
2822
|
* queue` refuses in the cloud; `#` only at its task (`est` links a cloud session by the only `#N` of its first
|
|
2813
2823
|
* prompt — docs/cloud-sessions.md, "The brief"); no restart line — the cloud assigns its own branch. A cloud session
|
|
2814
2824
|
* can't message a local one, so `Next:` gives the dispatcher the wait for the task's closing and the local close.
|
|
2825
|
+
* The epic refresh — in the same turn, not in the background, then `task close`, then the answer (#628): an executor of a
|
|
2826
|
+
* task without commits answered «waits for subagents» while its refresh ran, and its worktree without changes was
|
|
2827
|
+
* removed under the refresh subagent; a task without a PR is closed as completed first — `task close` needs it closed.
|
|
2828
|
+
* `session` — the prompt of an app executor session (`start_session`: another project's task, #624), printed by the
|
|
2829
|
+
* dispatcher: a session, not a subagent — by the canon of its project and its mode, named by the task, waits in the
|
|
2830
|
+
* background, the canon's last line; the subagent's lines say the opposite. `Next:` — how to start it.
|
|
2815
2831
|
*/
|
|
2816
|
-
export function cmdTaskAssignment(io: Io, slug: string, number: number, o: { cloud
|
|
2832
|
+
export function cmdTaskAssignment(io: Io, slug: string, number: number, o: { cloud?: boolean; session?: boolean } = {}): number {
|
|
2817
2833
|
const issue = loadIssue(io, slug, number);
|
|
2818
2834
|
const file = handedOutFile(io);
|
|
2819
2835
|
const own = pinnedTask(io)?.task;
|
|
@@ -2830,7 +2846,7 @@ export function cmdTaskAssignment(io: Io, slug: string, number: number, o: { clo
|
|
|
2830
2846
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
2831
2847
|
// the configuration directory may be a personal git checkout (ELVES_PRIVATE) — hand-outs aren't committed into it
|
|
2832
2848
|
writeFileSync(path.join(path.dirname(file), ".gitignore"), "*\n");
|
|
2833
|
-
const where = o.cloud ? CLOUD_HANDOUT : worktree;
|
|
2849
|
+
const where = o.cloud ? CLOUD_HANDOUT : o.session ? SESSION_HANDOUT : worktree;
|
|
2834
2850
|
appendFileSync(file, `${slug}#${number}${where ? ` ${where}` : ""}\n`);
|
|
2835
2851
|
}
|
|
2836
2852
|
if (o.cloud) return cloudBrief(io, slug, issue), 0;
|
|
@@ -2847,9 +2863,22 @@ export function cmdTaskAssignment(io: Io, slug: string, number: number, o: { clo
|
|
|
2847
2863
|
done: N.lastLine.done,
|
|
2848
2864
|
questions: N.sections.questions,
|
|
2849
2865
|
branches: restart.map((b) => `\`${b}\``).join(", "),
|
|
2866
|
+
doneQuestions: N.lastLine.questions,
|
|
2867
|
+
remaining: N.lastLine.remaining,
|
|
2850
2868
|
};
|
|
2851
|
-
const
|
|
2869
|
+
const resume = restart.length ? [N.queue.resume] : [];
|
|
2870
|
+
// the epic refresh in the same turn (`refresh`, #628) — the subagent's: a session runs it by the canon, with the deploy's wait
|
|
2871
|
+
const lines = o.session
|
|
2872
|
+
? [N.queue.sessionLead, ...resume, N.queue.sessionIs, N.queue.sessionName, N.queue.sessionWait, N.queue.sessionFound, N.queue.sessionQuestion, N.queue.sessionEnd]
|
|
2873
|
+
: [N.queue.lead, ...resume, N.queue.subagent, N.queue.session, N.queue.wait, N.queue.git, N.queue.subagents, N.queue.live, N.queue.refresh, N.queue.found, N.queue.question, N.queue.answer];
|
|
2852
2874
|
for (const line of lines) io.out(locale.fill(line, vars));
|
|
2875
|
+
if (o.session) {
|
|
2876
|
+
io.out(
|
|
2877
|
+
`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), ` +
|
|
2878
|
+
`use_worktree true, background — the first line above; keep the session's id it returns. ` +
|
|
2879
|
+
`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}.`,
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2853
2882
|
return 0;
|
|
2854
2883
|
}
|
|
2855
2884
|
|
|
@@ -3119,7 +3148,12 @@ export function cmdTaskClose(io: Io, slug: string, number: number, o: { git: boo
|
|
|
3119
3148
|
const prs = [...issue.prs, ...fresh].map((p) => `#${p.number} ${p.state === "OPEN" ? "open" : "closed without a merge"}`).join(", ");
|
|
3120
3149
|
const what = prs ? `PR ${prs}` : `the task is ${issue.state === "OPEN" ? "open" : "closed without being done"}, no merged PR`;
|
|
3121
3150
|
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}` : "";
|
|
3122
|
-
|
|
3151
|
+
// no PR at all and the task open — a task without commits closes its issue itself, and its executor didn't know (#628)
|
|
3152
|
+
const noPr =
|
|
3153
|
+
!prs && issue.state === "OPEN"
|
|
3154
|
+
? `; 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`
|
|
3155
|
+
: "";
|
|
3156
|
+
throw new GhError(`#${number} is too early to close: ${what}${checked} — close after the PR is merged or the task is closed${noPr}`);
|
|
3123
3157
|
}
|
|
3124
3158
|
if (issue.parent) {
|
|
3125
3159
|
const epic = loadTaskText(io, slug, issue.parent.number);
|
|
@@ -4151,7 +4185,7 @@ that can't be continued, in the worktree of its task's branch.
|
|
|
4151
4185
|
Examples:
|
|
4152
4186
|
github task resume`,
|
|
4153
4187
|
"task queue": `github task queue [--epic N] [--type Task|Bug] [--priority Urgent,High] [--repo owner/repo]
|
|
4154
|
-
github task queue --assignment <N> [--cloud] [--repo owner/repo]
|
|
4188
|
+
github task queue --assignment <N> [--cloud | --session] [--repo owner/repo]
|
|
4155
4189
|
|
|
4156
4190
|
The dispatcher session's queue: ready tasks (open, not an epic, not in work, no open blockers — other repositories'
|
|
4157
4191
|
too —, no external blocker label) by Priority, then number, each with its executor's prompt; the rest — in work (=: In
|
|
@@ -4170,13 +4204,17 @@ such a subagent at work or HEAD moved in the last ${QUIET_MINUTES} min — code
|
|
|
4170
4204
|
dispatcher wait, no hand-out. --cloud — printed by the dispatcher: the brief of a cloud executor (claude.ai/code) —
|
|
4171
4205
|
whole, # only at its task — and the dispatcher's steps: a cloud session can't message a local one, so the dispatcher
|
|
4172
4206
|
waits for the task's closing (ci-wait issue N, background) and closes it locally; the hand-out has no worktree, and the
|
|
4173
|
-
queue's = line names that wait.
|
|
4207
|
+
queue's = line names that wait. --session — printed by the dispatcher: the prompt of an app executor session
|
|
4208
|
+
(start_session in the folder of another project, its own worktree) — a session by its project's canon and mode, named
|
|
4209
|
+
by the task, waiting in the background, ending with the canon's last line — and how to start it; the hand-out has no
|
|
4210
|
+
worktree, the task is = in the queue.
|
|
4174
4211
|
|
|
4175
4212
|
Examples:
|
|
4176
4213
|
github task queue --epic 40
|
|
4177
4214
|
github task queue --type Bug --priority Urgent,High
|
|
4178
4215
|
github task queue --assignment 42
|
|
4179
|
-
github task queue --assignment 42 --cloud
|
|
4216
|
+
github task queue --assignment 42 --cloud
|
|
4217
|
+
github task queue --assignment 42 --session`,
|
|
4180
4218
|
"task close": `github task close <N> [--no-git] [--repo owner/repo]
|
|
4181
4219
|
|
|
4182
4220
|
After the PR merge and the deploy (or a close without a PR) in one call: the actual (est fact --write), Status Done, the
|
|
@@ -4347,6 +4385,7 @@ export function main(argv: string[], io: Io): number {
|
|
|
4347
4385
|
"no-git": { type: "boolean", default: false },
|
|
4348
4386
|
assignment: { type: "boolean", default: false },
|
|
4349
4387
|
cloud: { type: "boolean", default: false },
|
|
4388
|
+
session: { type: "boolean", default: false },
|
|
4350
4389
|
auto: { type: "boolean", default: false },
|
|
4351
4390
|
},
|
|
4352
4391
|
});
|
|
@@ -4368,9 +4407,11 @@ export function main(argv: string[], io: Io): number {
|
|
|
4368
4407
|
}
|
|
4369
4408
|
if (cmd === "queue") {
|
|
4370
4409
|
if (values.cloud && !values.assignment) throw new GhError("--cloud — only with --assignment <N>: the brief of one task's cloud executor");
|
|
4410
|
+
if (values.session && !values.assignment) throw new GhError("--session — only with --assignment <N>: the prompt of one task's app executor session");
|
|
4411
|
+
if (values.cloud && values.session) throw new GhError("--cloud or --session, not both: a cloud executor or an app session");
|
|
4371
4412
|
if (values.assignment) {
|
|
4372
4413
|
if (positionals.length !== 1) throw new GhError("task queue --assignment <N>: one task number");
|
|
4373
|
-
return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud! });
|
|
4414
|
+
return cmdTaskAssignment(io, slug, issueNumber(positionals[0], "task queue --assignment"), { cloud: values.cloud!, session: values.session! });
|
|
4374
4415
|
}
|
|
4375
4416
|
if (positionals.length) throw new GhError(`extra arguments: ${positionals.join(" ")} — the queue is chosen by --epic, --type and --priority`);
|
|
4376
4417
|
const priority = values.priority === undefined ? undefined : values.priority.split(",").map((x) => x.trim()).filter(Boolean);
|
|
@@ -102,6 +102,8 @@ export interface Names {
|
|
|
102
102
|
subagents: string;
|
|
103
103
|
/** The executor's answer waits for its own subagents (#565). */
|
|
104
104
|
live: string;
|
|
105
|
+
/** The epic refresh — in the same turn, before `task close` and the answer; a task without a PR closed first (#628). */
|
|
106
|
+
refresh: string;
|
|
105
107
|
found: string;
|
|
106
108
|
question: string;
|
|
107
109
|
answer: string;
|
|
@@ -114,6 +116,17 @@ export interface Names {
|
|
|
114
116
|
cloudClose: string;
|
|
115
117
|
cloudDispatcher: string;
|
|
116
118
|
cloudQuestion: string;
|
|
119
|
+
/**
|
|
120
|
+
* An app executor session's prompt (`--assignment N --session`, #624): a session of another project, not a subagent —
|
|
121
|
+
* by its canon and mode, named by the task, waits in the background, the canon's last line.
|
|
122
|
+
*/
|
|
123
|
+
sessionLead: string;
|
|
124
|
+
sessionIs: string;
|
|
125
|
+
sessionName: string;
|
|
126
|
+
sessionWait: string;
|
|
127
|
+
sessionFound: string;
|
|
128
|
+
sessionQuestion: string;
|
|
129
|
+
sessionEnd: string;
|
|
117
130
|
};
|
|
118
131
|
/** The est skill's "Estimate" and "Actual" comments and the lines a human appends to them. */
|
|
119
132
|
est: {
|