@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  locale_default
4
- } from "../../spec/scripts/harness-qzb932c8.js";
4
+ } from "../../spec/scripts/harness-wqrt5gpa.js";
5
5
 
6
6
  // skills/github/scripts/github.ts
7
7
  import { spawnSync } from "node:child_process";
@@ -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
- return { number: Number(number), worktree: worktree === CLOUD_HANDOUT ? null : worktree || null, cloud: worktree === CLOUD_HANDOUT };
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 = { cloud: false }) {
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 lines = [N.queue.lead, ...restart.length ? [N.queue.resume] : [], N.queue.subagent, N.queue.session, N.queue.wait, N.queue.git, N.queue.subagents, N.queue.live, N.queue.found, N.queue.question, N.queue.answer];
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
- throw new GhError(`#${number} is too early to close: ${what}${checked} — close after the PR is merged or the task is closed`);
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-qzb932c8.js";
3
+ } from "./harness-wqrt5gpa.js";
4
4
  import {
5
5
  invariant2,
6
6
  codeOnly2,
7
7
  envNamesInCode2
8
- } from "./harness-v9s1v9w3.js";
8
+ } from "./harness-ay62ws50.js";
9
9
 
10
10
  // skills/spec/scripts/architecture.ts
11
11
  import { readdirSync, readFileSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  locale_default
3
- } from "./harness-qzb932c8.js";
3
+ } from "./harness-wqrt5gpa.js";
4
4
  var __create = Object.create;
5
5
  var __getProtoOf = Object.getPrototypeOf;
6
6
  var __defProp = Object.defineProperty;
@@ -91,6 +91,7 @@ var en_default = {
91
91
  git: "- git — in separate calls, without `cd`, `&&` and `|`: worktree isolation refuses a compound command with git;",
92
92
  subagents: "- writing subagents — launch as needed, no limit: each gets its own worktree and «{task} #{number}» at the start of its prompt (`est` links its work by it); bring their commits into your branch, remove their worktrees and branches yourself after the merge — what didn't go, name in the answer;",
93
93
  live: "- while any of your subagents works, don't answer: wait for their answers — a background subagent wakes you with its answer; an answer with live subagents looks like a failure, and the dispatcher would give the task to a second executor next to you. Can't wait — answer `waits for subagents: <which and what they do>`: it isn't `failed`, there will be no second executor;",
94
+ refresh: '- the epic refresh — the subagent `task actualize` prints, without a worktree of its own (in yours) and not in the background (`run_in_background: false`): its answer — in the same turn, then `{script} task close {number}`, and only then the answer to the dispatcher: once you answer, a worktree without changes is removed, and a background subagent is left without a directory; a task without a PR (a data edit, a reconciliation, research) — first close the issue as completed with a result comment: `gh issue close {number} --repo {repo} --reason completed --comment "<result>"`;',
94
95
  found: "- work found outside the task — `{script} task new …` (work of the task's epic — with `--epic`: the dispatcher picks it up) — name it in the answer: whether to queue it is the owner's call;",
95
96
  question: "- a question without a clear recommendation — into «{questions}» of the task; don't start the code that depends on it and answer the dispatcher: the owner's answer comes back here.",
96
97
  answer: "The answer to the dispatcher — one line: `merged #<PR>` · `question: <the question, options, recommendation>` · `waits for subagents: <which and what they do>` · `failed: <what stopped and where>`.",
@@ -100,7 +101,14 @@ var en_default = {
100
101
  cloudDoc: "- the cloud — by `docs/cloud-sessions.md` of the canon, read it first: of the github skill's scripts `task show`, `task answer` and `task new` work there (over REST), the rest refuse, the scenarios section — by editing the description over REST, project fields aren't set from the cloud; the branch is the one the cloud assigned, don't rename it;",
101
102
  cloudClose: "- after the merge — its section «Closing a task from the cloud»: the «{actual}» comment of `est fact {number}` and a «{state}» comment with what the cloud can't do — both into the task;",
102
103
  cloudDispatcher: "- the dispatcher waits for the task to close and closes it locally — the actual, the status, the merged branch, archiving this session: don't message it, don't archive the session;",
103
- cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task."
104
+ cloudQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session, don't start the code that depends on it; stopped (a question, a failure, a limit) — a «{state}» comment into the task.",
105
+ sessionLead: "{task} #{number} «{title}» in {repo}{epic}: you are an executor session of the dispatcher session — do the task whole by the canon of this project, in its mode (`check` prints it; outside auto — commit, push and merge on the owner's «yes»): the task's questions, `{script} task status {number} \"{inProgress}\" --branch <type>/{number}-<slug>`, the branch from a fresh origin/main, code and tests, PR, green CI, `pr premerge`, merge, deploy, the epic refresh, `{script} task close {number}`.",
106
+ sessionIs: "You are a session, not a subagent:",
107
+ sessionName: "- name the session `#{number} {title}` (`set_session_title`) once the task is taken — `est` links the session to the task by it;",
108
+ sessionWait: "- wait for CI and the deploy with a background command — `{waitCi} pr <PR>` (after the merge — `merged <PR>`): its completion wakes the session; no `Monitor`, watcher loop or `ScheduleWakeup` on top;",
109
+ sessionFound: "- work found outside the task — `{script} task new …` and the question «queue it?» by the canon; work of the task's epic — with `--epic` and without an executor of yours: the dispatcher's queue picks it up;",
110
+ sessionQuestion: "- a question without a clear recommendation — into «{questions}» of the task and to the owner in this session; don't start the code that depends on it: the answer comes here, from the owner or the dispatcher;",
111
+ sessionEnd: "- the end — the canon's last line («{done}» · «{doneQuestions} …» · «{remaining} …»), in auto mode after «{done}» — archiving the session by the canon: the dispatcher reads the end of each of your turns as your answer."
104
112
  },
105
113
  est: {
106
114
  answer: "Answer",
@@ -541,6 +549,7 @@ var ru_default = {
541
549
  git: "- git — отдельными вызовами, без `cd`, `&&` и `|`: составную команду с git изоляция worktree не пропускает;",
542
550
  subagents: "- пишущих субагентов запускай по необходимости, без лимита: каждому — свой worktree и «{task} #{number}» в начале промпта (по нему `est` привяжет его работу); их коммиты перенеси в свою ветку, их worktree и ветки после мержа убери сам — что не убралось, назови в ответе;",
543
551
  live: "- пока работает хоть один твой субагент, ответ не отдавай: жди их ответов — фоновый субагент будит тебя своим ответом; ответ при живых субагентах выглядит провалом, и диспетчер отдал бы задачу второму исполнителю рядом с тобой. Ждать нельзя — ответь `ждёт субагентов: <какие и что делают>`: это не «сломалось», второго исполнителя не будет;",
552
+ refresh: '- актуализация эпика — субагент, которого печатает `task actualize`, без своего worktree (в твоём) и не в фоне (`run_in_background: false`): его ответ — в том же ходе, затем `{script} task close {number}`, и только потом ответ диспетчеру: стоит тебе ответить, worktree без изменений убирают, и фоновый субагент остаётся без каталога; у задачи без PR (правка данных, сверка, исследование) — сначала закрой issue как выполненную с итоговым комментарием: `gh issue close {number} --repo {repo} --reason completed --comment "<итог>"`;',
544
553
  found: "- найденная работа вне задачи — `{script} task new …` (работа эпика задачи — с `--epic`: диспетчер её подхватит) — назови её в ответе: ставить ли её в очередь, решает владелец;",
545
554
  question: "- вопрос без ясной рекомендации — в «{questions}» задачи; код, который от него зависит, не начинай и ответь диспетчеру: ответ владельца придёт сюда же.",
546
555
  answer: "Ответ диспетчеру — одной строкой: `влито #<PR>` · `вопрос: <вопрос, варианты, рекомендация>` · `ждёт субагентов: <какие и что делают>` · `сломалось: <что и где остановилось>`.",
@@ -550,7 +559,14 @@ var ru_default = {
550
559
  cloudDoc: "- облако — по `docs/cloud-sessions.md` канона, прочитай его первым: из скриптов скилла github там работают `task show`, `task answer` и `task new` (через REST), остальные отказывают, раздел сценариев — правкой описания через REST, поля проекта из облака не ставятся; ветка — та, что назначило облако, не переименовывай;",
551
560
  cloudClose: "- после мержа — его раздел «Closing a task from the cloud»: комментарий «{actual}» от `est fact {number}` и комментарий «{state}» с тем, что из облака не сделать, — оба в задачу;",
552
561
  cloudDispatcher: "- диспетчер ждёт закрытия задачи и закрывает её локально — факт, статус, влитая ветка, архив этой сессии: сообщать ему не нужно, сессию не архивируй;",
553
- cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу."
562
+ cloudQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии, код, который от него зависит, не начинай; остановился (вопрос, сбой, лимит) — комментарий «{state}» в задачу.",
563
+ sessionLead: '{task} #{number} «{title}» в {repo}{epic}: ты сессия-исполнитель сессии-диспетчера — сделай задачу целиком по канону этого проекта, в его режиме (его печатает `check`; не в auto — коммит, пуш и мерж по «да» владельца): вопросы задачи, `{script} task status {number} "{inProgress}" --branch <тип>/{number}-<slug>`, ветка от свежего origin/main, код и тесты, PR, зелёный CI, `pr premerge`, мерж, деплой, актуализация эпика, `{script} task close {number}`.',
564
+ sessionIs: "Ты сессия, а не субагент:",
565
+ sessionName: "- назови сессию `#{number} {title}` (`set_session_title`), как только взял задачу, — по имени `est` привяжет сессию к задаче;",
566
+ sessionWait: "- CI и деплой жди фоновой командой — `{waitCi} pr <PR>` (после мержа — `merged <PR>`): её завершение будит сессию; без `Monitor`, цикла-наблюдателя и `ScheduleWakeup` поверх;",
567
+ sessionFound: "- найденная работа вне задачи — `{script} task new …` и вопрос «ставить в очередь?» по канону; работа эпика задачи — с `--epic` и без твоего исполнителя: её подхватит очередь диспетчера;",
568
+ sessionQuestion: "- вопрос без ясной рекомендации — в «{questions}» задачи и владельцу в этой сессии; код, который от него зависит, не начинай: ответ придёт сюда же — от владельца или диспетчера;",
569
+ sessionEnd: "- конец — последняя строка канона («{done}» · «{doneQuestions} …» · «{remaining} …»), в режиме auto после «{done}» — архивация сессии по канону: конец каждого твоего хода диспетчер читает как твой ответ."
554
570
  },
555
571
  est: {
556
572
  answer: "Ответ",
@@ -23,8 +23,8 @@ import {
23
23
  envNamesInCode2,
24
24
  dotenvNames2,
25
25
  envVars2
26
- } from "./harness-v9s1v9w3.js";
27
- import"./harness-qzb932c8.js";
26
+ } from "./harness-ay62ws50.js";
27
+ import"./harness-wqrt5gpa.js";
28
28
  export {
29
29
  ENV_SERVICE2 as ENV_SERVICE,
30
30
  codeFiles2 as codeFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miroshnik/elves",
3
- "version": "2026.1018.2",
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": "40c5e0387c9bfa5dfb56d6a7f5b05a99333aabac"
74
+ "gitHead": "42576112e74dfdc383f64b0edb5e08a24ff4afa2"
74
75
  }
@@ -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. The first failed check — FAIL right away; --wait-all — FAIL when all have finished.
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 "$deadline" ] && finish "TIMEOUT after $(dur $((now - start))) ($full)" 2
375
- [ "$nap" -gt $((deadline - now)) ] && nap=$((deadline - now))
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
@@ -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 — the
152
- indented line as is; at most 3 subagents and 5 cloud sessions at once, the
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 removes
159
- finished executors' worktrees (`+`); its last line names the step:
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's questions section and to the same
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
- a cloud one's: `--cloud`) — `reference.md`, "task queue".
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), found work — `task new`,
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. Archiving, app and cloud executors alike
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). The epic has
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
- * github pr labels — decision labels for the task from "Closes #N" by the PR diff, for its epic — the union
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 cloud one — null. */
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
- /** A cloud executor's (#609): the dispatcher waits for the task's closing. */
1788
- cloud: boolean;
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
- return { number: Number(number), worktree: worktree === CLOUD_HANDOUT ? null : worktree || null, cloud: worktree === CLOUD_HANDOUT };
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: boolean } = { cloud: false }): number {
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 lines = [N.queue.lead, ...(restart.length ? [N.queue.resume] : []), N.queue.subagent, N.queue.session, N.queue.wait, N.queue.git, N.queue.subagents, N.queue.live, N.queue.found, N.queue.question, N.queue.answer];
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
- throw new GhError(`#${number} is too early to close: ${what}${checked} — close after the PR is merged or the task is closed`);
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: {