@quiel/cli 0.5.2 → 0.5.3

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.js +1537 -1273
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1317,13 +1317,22 @@ var taskPayloadLeaseSchema = z23.object({
1317
1317
  resumeToken: z23.string().min(1),
1318
1318
  expiresAt: z23.string().datetime()
1319
1319
  });
1320
+ var taskPayloadContinueFromSchema = z23.object({
1321
+ branchName: z23.string().min(1),
1322
+ sha: z23.string().optional(),
1323
+ /** Короткая записка прошлого исполнителя «что успел». */
1324
+ note: z23.string().optional(),
1325
+ savedAt: z23.string()
1326
+ });
1320
1327
  var taskPayloadSchema = z23.object({
1321
1328
  task: taskPayloadTaskSchema,
1322
1329
  project: taskPayloadProjectSchema,
1323
1330
  dependencies: z23.array(taskPayloadDependencySchema),
1324
1331
  openQuestions: z23.array(taskPayloadOpenQuestionSchema),
1325
1332
  trust: taskPayloadTrustSchema,
1326
- lease: taskPayloadLeaseSchema
1333
+ lease: taskPayloadLeaseSchema,
1334
+ /** Есть только у задач, которые уже начинали и прервали. */
1335
+ continueFrom: taskPayloadContinueFromSchema.optional()
1327
1336
  });
1328
1337
  var taskClaimPayloadSchema = z23.object({
1329
1338
  /**
@@ -1440,7 +1449,21 @@ var contextPayloadSchema = z23.object({
1440
1449
  });
1441
1450
  var agentRateLimitedPayloadSchema = z23.object({
1442
1451
  retryAfterSec: z23.number().int().positive().optional(),
1443
- taskId: z23.string().min(1).optional()
1452
+ taskId: z23.string().min(1).optional(),
1453
+ /**
1454
+ * Когда лимит сбросится (LIM-06) — разобрано клиентом из текста самой
1455
+ * ошибки Claude Code («resets 3:45pm»).
1456
+ *
1457
+ * НЕ ПУТАТЬ С `retryAfterSec`. Тот — сколько секунд подождать, как
1458
+ * посчитал сам Claude Code, и его часто нет вовсе. Этот — абсолютный
1459
+ * момент, уже приведённый к UTC клиентом: в сообщении время местное и
1460
+ * без пояса, и разобрать его может только тот, кто стоит на той же
1461
+ * машине.
1462
+ *
1463
+ * Необязательное: формат сообщения может смениться, и тогда клиент
1464
+ * честно ничего не присылает, а не гадает.
1465
+ */
1466
+ resetsAt: z23.string().datetime().optional()
1444
1467
  });
1445
1468
  var agentRateLimitedAckSchema = z23.object({
1446
1469
  ok: z23.boolean(),
@@ -1520,6 +1543,18 @@ var taskUsagePayloadSchema = z23.object({
1520
1543
  entries: z23.array(taskUsageEntrySchema).min(1).max(10)
1521
1544
  });
1522
1545
  var taskUsageAckSchema = z23.object({ ok: z23.boolean() });
1546
+ var taskWorkSavedPayloadSchema = z23.object({
1547
+ taskId: z23.string().min(1),
1548
+ /** Ветка, в которую ушла незаконченная работа. */
1549
+ branchName: z23.string().min(1).max(255),
1550
+ sha: z23.string().min(7).max(64).optional(),
1551
+ /**
1552
+ * Короткая записка «что успел» — её увидит следующий исполнитель.
1553
+ * Потолок жёсткий: это подпись к ветке, а не отчёт о работе.
1554
+ */
1555
+ note: z23.string().max(500).optional()
1556
+ });
1557
+ var taskWorkSavedAckSchema = z23.object({ ok: z23.boolean() });
1523
1558
  var sessionLimitReachedPayloadSchema = z23.object({
1524
1559
  tasksCompletedInSession: z23.number().int().nonnegative()
1525
1560
  });
@@ -3423,7 +3458,22 @@ var CurrentTaskSchema = z30.object({
3423
3458
  * Необязательно: состояния, записанные прежними версиями клиента, поля
3424
3459
  * не имеют — тогда `submit_work` работает как раньше, в режиме DIRECT.
3425
3460
  */
3426
- gitMode: z30.enum([GitMode.DIRECT, GitMode.PR]).optional()
3461
+ gitMode: z30.enum([GitMode.DIRECT, GitMode.PR]).optional(),
3462
+ /**
3463
+ * Имя ветки задачи и префикс сообщения коммита — как их посчитал
3464
+ * СЕРВЕР (`project.branchName`, `project.commitPrefix`, §10.3 ТЗ).
3465
+ *
3466
+ * Добавлены для спасения незаконченной работы (LIM-01): когда агент
3467
+ * упирается в лимит подписки, коммитить и отправлять надо уже без
3468
+ * него — он в этот момент ничего не может. Значит и ветку, и префикс
3469
+ * надо помнить заранее, а не спрашивать в момент прерывания.
3470
+ *
3471
+ * Необязательны: у состояний, записанных прежними версиями клиента,
3472
+ * полей нет — тогда работа спасается только коммитом (без push) или
3473
+ * не спасается вовсе, и об этом честно сообщается.
3474
+ */
3475
+ branchName: z30.string().min(1).optional(),
3476
+ commitPrefix: z30.string().min(1).optional()
3427
3477
  });
3428
3478
  var TrustCacheSchema = z30.object({
3429
3479
  profileKey: z30.string().min(1),
@@ -4175,7 +4225,14 @@ var createSocket = (url, options) => io(url, {
4175
4225
  // src/hooks/audit-sender.ts
4176
4226
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e3;
4177
4227
  var DEFAULT_FLUSH_GRACE_MS = 150;
4178
- var WIRE_EVENTS = /* @__PURE__ */ new Set(["audit:command", "task:usage", "session-limit-reached"]);
4228
+ var WIRE_EVENTS = /* @__PURE__ */ new Set([
4229
+ "audit:command",
4230
+ "task:usage",
4231
+ "session-limit-reached",
4232
+ "agent:rate_limited",
4233
+ // LIM-02: о спасённой ветке сервер узнаёт отсюда же.
4234
+ "task:work_saved"
4235
+ ]);
4179
4236
  function sendHookActions(actions, options = {}) {
4180
4237
  const wireActions = actions.filter((action) => WIRE_EVENTS.has(action.type));
4181
4238
  if (wireActions.length === 0) return;
@@ -4360,1297 +4417,1528 @@ function renderHookOutput(format, hook, output) {
4360
4417
  return { stdout: "{}", stderr: "", exitCode: 0 };
4361
4418
  }
4362
4419
 
4363
- // src/socket/auth-reject.ts
4364
- var AUTH_REJECT_REASONS = /* @__PURE__ */ new Set(["missing", "invalid"]);
4365
- function isAuthRejection(error) {
4366
- const message = error instanceof Error ? error.message : String(error);
4367
- return AUTH_REJECT_REASONS.has(message.trim());
4420
+ // src/git/exec.ts
4421
+ import { execFile } from "child_process";
4422
+ var DEFAULT_TIMEOUT_MS = 12e4;
4423
+ var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
4424
+ function runGit(args, options) {
4425
+ return new Promise((resolvePromise) => {
4426
+ execFile(
4427
+ "git",
4428
+ [...args],
4429
+ {
4430
+ cwd: options.cwd,
4431
+ shell: false,
4432
+ timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
4433
+ maxBuffer: MAX_BUFFER_BYTES,
4434
+ env: {
4435
+ ...process.env,
4436
+ ...options.env,
4437
+ // Без человека за терминалом: если git вдруг решит спросить
4438
+ // логин/пароль, зависший prompt выглядел бы как неожиданное
4439
+ // поведение (требование подзадачи: «любая неожиданность —
4440
+ // понятный отказ, а не попробую починить»), а не как честный
4441
+ // отказ команды с кодом возврата.
4442
+ GIT_TERMINAL_PROMPT: "0"
4443
+ }
4444
+ },
4445
+ (error, stdout, stderr) => {
4446
+ if (!error) {
4447
+ resolvePromise({ ok: true, code: 0, signal: null, stdout, stderr });
4448
+ return;
4449
+ }
4450
+ const code = typeof error.code === "number" ? error.code : null;
4451
+ resolvePromise({
4452
+ ok: false,
4453
+ code,
4454
+ signal: error.signal ?? null,
4455
+ stdout,
4456
+ // Если git ничего не написал в stderr (например, сам процесс не
4457
+ // запустился — ENOENT), берём текст ошибки Node, чтобы вызывающий
4458
+ // код не остался с пустым сообщением.
4459
+ stderr: stderr.length > 0 ? stderr : error.message
4460
+ });
4461
+ }
4462
+ );
4463
+ });
4368
4464
  }
4369
- function describeAuthRejection(agentId) {
4370
- return `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B\u0430 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 ${agentId}: \u043E\u043D \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D, \u043E\u0442\u043E\u0437\u0432\u0430\u043D \u0438\u043B\u0438 \u0438\u0441\u043F\u043E\u0440\u0447\u0435\u043D. \u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043D\u0435 \u043F\u043E\u043C\u043E\u0436\u0435\u0442 \u2014 \u0432\u044B\u043F\u0443\u0441\u0442\u0438 \u043D\u043E\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0430\u0433\u0435\u043D\u0442\u0430 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438 "quiel init" \u0437\u0430\u043D\u043E\u0432\u043E.`;
4465
+ function describeGitFailure(commandLabel, result) {
4466
+ const stderr = result.stderr.trim();
4467
+ const codeText = result.code === null ? "" : ` (\u043A\u043E\u0434 ${result.code})`;
4468
+ return stderr.length > 0 ? `${commandLabel}${codeText}: ${stderr}` : `${commandLabel} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439${codeText}`;
4371
4469
  }
4372
4470
 
4373
- // src/socket/backoff.ts
4374
- function nextReconnectDelayMs(attempt, options = {}) {
4375
- const baseMs = options.baseMs ?? 1e3;
4376
- const maxMs = options.maxMs ?? 3e4;
4377
- const safeAttempt = Math.max(0, Math.trunc(attempt));
4378
- const exponent = Math.min(safeAttempt, 30);
4379
- const raw = baseMs * 2 ** exponent;
4380
- return Math.min(raw, maxMs);
4471
+ // src/git/result.ts
4472
+ function gitOk(data) {
4473
+ return { ok: true, data };
4381
4474
  }
4382
-
4383
- // src/socket/client.ts
4384
- import { EventEmitter } from "events";
4385
-
4386
- // src/socket/emit-with-timeout.ts
4387
- function toError(raw, context) {
4388
- return raw instanceof Error ? raw : new Error(`${context}: ${String(raw)}`);
4475
+ function gitErr(message) {
4476
+ return { ok: false, message };
4389
4477
  }
4390
- async function emitWithAckTimeout(socket, event, payload, timeoutMs) {
4391
- return await new Promise((resolve4, reject) => {
4392
- const timer = setTimeout(() => {
4393
- reject(new Error(`${event}: \u043D\u0435\u0442 ack \u043E\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0437\u0430 ${timeoutMs} \u043C\u0441`));
4394
- }, timeoutMs);
4395
- if (typeof timer.unref === "function") timer.unref();
4396
- socket.emitWithAck(event, payload).then((value) => {
4397
- clearTimeout(timer);
4398
- resolve4(value);
4399
- }).catch((err) => {
4400
- clearTimeout(timer);
4401
- reject(toError(err, event));
4402
- });
4478
+
4479
+ // src/git/repo.ts
4480
+ async function localBranchExists(repoRoot, branch) {
4481
+ const result = await runGit(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
4482
+ cwd: repoRoot
4403
4483
  });
4484
+ return result.ok;
4404
4485
  }
4405
-
4406
- // src/socket/trust-cache.ts
4407
- var UNKNOWN_TRUST_PROFILE_KEY = "unknown";
4408
- function toTrustCache(rules, now = () => /* @__PURE__ */ new Date()) {
4409
- return {
4410
- profileKey: UNKNOWN_TRUST_PROFILE_KEY,
4411
- updatedAt: now().toISOString(),
4412
- rules
4413
- };
4486
+ async function getHeadSha(worktreeDir) {
4487
+ const result = await runGit(["rev-parse", "HEAD"], { cwd: worktreeDir });
4488
+ if (!result.ok) {
4489
+ return gitErr(describeGitFailure("git rev-parse HEAD", result));
4490
+ }
4491
+ return gitOk(result.stdout.trim());
4414
4492
  }
4415
-
4416
- // src/socket/types.ts
4417
- function isAckError(raw) {
4418
- return typeof raw === "object" && raw !== null && "error" in raw && typeof raw.error === "object";
4493
+ async function currentBranch(worktreeDir) {
4494
+ const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreeDir });
4495
+ if (!result.ok) {
4496
+ return gitErr(describeGitFailure("git rev-parse --abbrev-ref HEAD", result));
4497
+ }
4498
+ return gitOk(result.stdout.trim());
4419
4499
  }
4420
4500
 
4421
- // src/socket/client.ts
4422
- var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
4423
- var DEFAULT_ACK_TIMEOUT_MS = 1e4;
4424
- var DEFAULT_BACKOFF_BASE_MS = 1e3;
4425
- var DEFAULT_BACKOFF_MAX_MS = 3e4;
4426
- function toError2(raw, context) {
4427
- if (raw instanceof Error) return raw;
4428
- return new Error(`${context}: ${String(raw)}`);
4501
+ // src/git/status.ts
4502
+ var CONFLICT_CODES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
4503
+ async function getWorktreeStatus(worktreeDir) {
4504
+ const result = await runGit(["status", "--porcelain=v1", "--untracked-files=all"], {
4505
+ cwd: worktreeDir
4506
+ });
4507
+ if (!result.ok) {
4508
+ return gitErr(describeGitFailure("git status", result));
4509
+ }
4510
+ const changedPaths = [];
4511
+ const conflictedPaths = [];
4512
+ for (const line of result.stdout.split("\n")) {
4513
+ if (line.length === 0) continue;
4514
+ const code = line.slice(0, 2);
4515
+ const rawPath = line.slice(3);
4516
+ const arrowIndex = rawPath.indexOf(" -> ");
4517
+ const path = arrowIndex >= 0 ? rawPath.slice(arrowIndex + 4) : rawPath;
4518
+ changedPaths.push(path);
4519
+ if (CONFLICT_CODES.has(code)) conflictedPaths.push(path);
4520
+ }
4521
+ return gitOk({ clean: changedPaths.length === 0, changedPaths, conflictedPaths });
4429
4522
  }
4430
- function resolveOptions(options) {
4431
- return {
4432
- roleKeys: options.roleKeys,
4433
- cwd: options.cwd,
4434
- clientVersion: options.clientVersion,
4435
- agentVersion: options.agentVersion ?? "unknown",
4436
- agentKind: options.agentKind ?? DEFAULT_AGENT_KIND,
4437
- os: options.os ?? process.platform,
4438
- // По умолчанию — файл ИМЕННО этого агента (state/store.ts::getStateFilePath(agentId)),
4439
- // а не общий `state.json`: не трогаем src/state/**, но обязаны использовать
4440
- // его правильно (OQ-05 — состояние на агента, не на машину).
4441
- stateFilePath: options.stateFilePath ?? getStateFilePath(options.agentId),
4442
- heartbeatIntervalMs: options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
4443
- ackTimeoutMs: options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS,
4444
- backoffBaseMs: options.backoffBaseMs ?? DEFAULT_BACKOFF_BASE_MS,
4445
- backoffMaxMs: options.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS,
4446
- now: options.now ?? (() => /* @__PURE__ */ new Date())
4447
- };
4523
+
4524
+ // src/git/commit.ts
4525
+ function buildCommitMessage(params) {
4526
+ const subject = `${params.commitPrefix} ${params.title}`.trim();
4527
+ return `${subject}
4528
+
4529
+ ${buildTaskTrailer(params.taskKey)}
4530
+ `;
4448
4531
  }
4449
- var AgentSocketClient = class extends EventEmitter {
4450
- socket;
4451
- opts;
4452
- agentId;
4453
- stopped = true;
4454
- reconnectAttempt = 0;
4455
- reconnectTimer;
4456
- heartbeatTimer;
4457
- lastHelloAck;
4458
- paused = false;
4459
- pauseReasonText;
4460
- leaseLostTasks = /* @__PURE__ */ new Map();
4461
- constructor(options) {
4462
- super();
4463
- this.opts = resolveOptions(options);
4464
- this.agentId = options.agentId;
4465
- const factory = options.createSocket ?? createSocket;
4466
- this.socket = factory(`${trimTrailingSlash2(options.url)}/agent`, {
4467
- auth: { token: options.token },
4468
- autoConnect: false,
4469
- reconnection: false
4470
- });
4471
- this.registerSocketListeners();
4472
- }
4473
- // -- Публичное API -----------------------------------------------------
4474
- on(event, listener) {
4475
- return super.on(event, listener);
4532
+ async function commitAll(params) {
4533
+ const addResult = await runGit(["add", "-A"], { cwd: params.worktreeDir });
4534
+ if (!addResult.ok) {
4535
+ return gitErr(describeGitFailure("git add -A", addResult));
4476
4536
  }
4477
- once(event, listener) {
4478
- return super.once(event, listener);
4537
+ const status = await getWorktreeStatus(params.worktreeDir);
4538
+ if (!status.ok) return status;
4539
+ if (status.data.clean) {
4540
+ return gitOk({ committed: false, sha: null });
4479
4541
  }
4480
- off(event, listener) {
4481
- return super.off(event, listener);
4542
+ const message = buildCommitMessage(params);
4543
+ const commitResult = await runGit(["commit", "-m", message], { cwd: params.worktreeDir });
4544
+ if (!commitResult.ok) {
4545
+ return gitErr(describeGitFailure("git commit", commitResult));
4482
4546
  }
4483
- emit(event, ...args) {
4484
- return super.emit(event, ...args);
4547
+ const sha = await getHeadSha(params.worktreeDir);
4548
+ return gitOk({ committed: true, sha: sha.ok ? sha.data : null });
4549
+ }
4550
+
4551
+ // src/git/branch-name.ts
4552
+ var FORBIDDEN_CHARS = /[\s~^:?*[\\\x00-\x1f\x7f]/;
4553
+ function isValidGitBranchName(name) {
4554
+ if (name.length === 0 || name.length > 255) return false;
4555
+ if (name.startsWith("-")) return false;
4556
+ if (name.startsWith("/") || name.endsWith("/")) return false;
4557
+ if (name.endsWith(".")) return false;
4558
+ if (name.includes("..")) return false;
4559
+ if (name.includes("//")) return false;
4560
+ if (name.includes("@{")) return false;
4561
+ if (name === "@") return false;
4562
+ if (FORBIDDEN_CHARS.test(name)) return false;
4563
+ const segments = name.split("/");
4564
+ for (const segment of segments) {
4565
+ if (segment.length === 0) return false;
4566
+ if (segment.startsWith(".")) return false;
4567
+ if (segment.endsWith(".lock")) return false;
4485
4568
  }
4486
- /** Поднимает соединение и heartbeat-таймер. Идемпотентно — повторный вызов на уже запущенном клиенте не делает ничего. */
4487
- start() {
4488
- if (!this.stopped) return;
4489
- this.stopped = false;
4490
- this.reconnectAttempt = 0;
4491
- this.startHeartbeatTimer();
4492
- this.socket.connect();
4493
- }
4494
- /** Останавливает heartbeat, отменяет запланированное переподключение, закрывает сокет. После `stop()` клиент больше не переподключается сам. */
4495
- stop() {
4496
- this.stopped = true;
4497
- if (this.reconnectTimer) {
4498
- clearTimeout(this.reconnectTimer);
4499
- this.reconnectTimer = void 0;
4500
- }
4501
- this.stopHeartbeatTimer();
4502
- this.socket.disconnect();
4569
+ return true;
4570
+ }
4571
+ function requireValidBranchName(name, label) {
4572
+ if (!isValidGitBranchName(name)) {
4573
+ return gitErr(
4574
+ `${label} "${name}" \u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0438\u043C\u044F git-\u0432\u0435\u0442\u043A\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
4575
+ );
4503
4576
  }
4504
- get connected() {
4505
- return this.socket.connected;
4577
+ return gitOk(name);
4578
+ }
4579
+
4580
+ // src/git/worktree.ts
4581
+ import { createHash as createHash2 } from "crypto";
4582
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, realpathSync } from "fs";
4583
+ import { dirname as dirname4, isAbsolute, join as join9, relative, resolve as resolve2 } from "path";
4584
+ var WORKTREES_RELATIVE_ROOT = join9(".claude", "worktrees");
4585
+ function getDefaultWorktreesRoot(repoRoot) {
4586
+ return join9(repoRoot, WORKTREES_RELATIVE_ROOT);
4587
+ }
4588
+ function resolveWorktreeDir(worktreesRoot, repoRoot, branchName) {
4589
+ if (isInsideRepo(worktreesRoot, repoRoot)) return join9(worktreesRoot, branchName);
4590
+ const repoHash = createHash2("sha1").update(resolve2(repoRoot)).digest("hex").slice(0, 12);
4591
+ return join9(worktreesRoot, repoHash, branchName);
4592
+ }
4593
+ function isInsideRepo(candidate, repoRoot) {
4594
+ const rel = relative(resolve2(repoRoot), resolve2(candidate));
4595
+ return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
4596
+ }
4597
+ function canonicalPath(path) {
4598
+ try {
4599
+ return realpathSync(path);
4600
+ } catch {
4601
+ return resolve2(path);
4506
4602
  }
4507
- get lastHello() {
4508
- return this.lastHelloAck;
4603
+ }
4604
+ async function listWorktrees(repoRoot) {
4605
+ const result = await runGit(["worktree", "list", "--porcelain"], { cwd: repoRoot });
4606
+ if (!result.ok) {
4607
+ return gitErr(describeGitFailure("git worktree list", result));
4509
4608
  }
4510
- get isPaused() {
4511
- return this.paused;
4609
+ const entries = [];
4610
+ let path;
4611
+ let branch = null;
4612
+ const flush = () => {
4613
+ if (path) entries.push({ path, branch });
4614
+ path = void 0;
4615
+ branch = null;
4616
+ };
4617
+ for (const line of result.stdout.split("\n")) {
4618
+ if (line.length === 0) {
4619
+ flush();
4620
+ continue;
4621
+ }
4622
+ if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
4623
+ else if (line.startsWith("branch refs/heads/")) branch = line.slice("branch refs/heads/".length);
4512
4624
  }
4513
- get pauseReason() {
4514
- return this.pauseReasonText;
4625
+ flush();
4626
+ return gitOk(entries);
4627
+ }
4628
+ async function ensureWorktree(params) {
4629
+ const remote = params.remote ?? "origin";
4630
+ const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
4631
+ if (!branchCheck.ok) return branchCheck;
4632
+ const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
4633
+ if (!defaultBranchCheck.ok) return defaultBranchCheck;
4634
+ const worktreesRoot = params.worktreesRoot ?? getDefaultWorktreesRoot(params.repoRoot);
4635
+ const worktreeDir = resolveWorktreeDir(worktreesRoot, params.repoRoot, params.branchName);
4636
+ const list = await listWorktrees(params.repoRoot);
4637
+ if (!list.ok) return list;
4638
+ const existing = list.data.find((wt) => canonicalPath(wt.path) === canonicalPath(worktreeDir));
4639
+ if (existing) {
4640
+ if (existing.branch === params.branchName) {
4641
+ return gitOk({ worktreeDir, reused: true });
4642
+ }
4643
+ return gitErr(
4644
+ `\u041F\u043E \u043F\u0443\u0442\u0438 ${worktreeDir} \u0443\u0436\u0435 \u0435\u0441\u0442\u044C git worktree \u043D\u0430 \u0432\u0435\u0442\u043A\u0435 "${existing.branch ?? "(\u043E\u0442\u0441\u043E\u0435\u0434\u0438\u043D\u0451\u043D\u043D\u044B\u0439 HEAD)"}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}". \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0440\u0443\u043A\u0430\u043C\u0438: "git -C ${params.repoRoot} worktree remove ${worktreeDir}" (\u0434\u043E\u0431\u0430\u0432\u044C "--force", \u0435\u0441\u043B\u0438 \u0442\u0430\u043C \u043D\u0435\u0442 \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u0430\u0436\u043D\u043E\u0433\u043E), \u0437\u0430\u0442\u0435\u043C \u043F\u043E\u0432\u0442\u043E\u0440\u0438.`
4645
+ );
4515
4646
  }
4516
- /** §11.2 ТЗ: «следующий вызов любого тула по этой задаче должен сообщить модели, что работу надо прекратить» — тулы (P2-12) сверяются с этим геттером. */
4517
- isLeaseLost(taskId) {
4518
- const reason = this.leaseLostTasks.get(taskId);
4519
- return reason === void 0 ? { lost: false } : { lost: true, reason };
4647
+ if (existsSync9(worktreeDir)) {
4648
+ return gitErr(
4649
+ `\u041F\u0443\u0442\u044C ${worktreeDir} \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E git \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0435\u0433\u043E \u0441\u0432\u043E\u0435\u0439 worktree. \u0423\u0431\u0435\u0440\u0438 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u2014 \u0442\u0440\u043E\u0433\u0430\u0442\u044C \u0447\u0443\u0436\u043E\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E.`
4650
+ );
4520
4651
  }
4521
- /**
4522
- * Общий emit+ack поверх ЭТОГО ЖЕ постоянного сокета — тем же приёмом
4523
- * (`emitWithAckTimeout`), которым `sendHello`/`sendHeartbeat` уже
4524
- * пользуются внутри класса, но публично: MCP-тулы (P2-12/13,
4525
- * `../mcp/**`) шлют через него `task:claim`/`task:start`/`task:progress`/
4526
- * `task:get`/`task:report`/`task:release`/`context:get` (§11.1 ТЗ) — то
4527
- * есть ВСЁ, что не `agent:hello`/`agent:heartbeat`/`agent:mode` (у
4528
- * первых двух своя логика согласования состояния в этом классе, третий
4529
- * — короткоживущий отдельный сокет, см. `send-agent-mode.ts`). Нарочно
4530
- * НЕ отдельное соединение: `task:claim` — блокирующий долгий long-poll,
4531
- * и держать его на том же сокете, где уже идёт heartbeat/presence этого
4532
- * агента, а не поднимать вторую параллельную сессию — единственный
4533
- * вариант, не удваивающий presence на сервере.
4534
- */
4535
- callServer(event, payload, timeoutMs = this.opts.ackTimeoutMs) {
4536
- return emitWithAckTimeout(this.socket, event, payload, timeoutMs);
4652
+ await runGit(["worktree", "prune"], { cwd: params.repoRoot });
4653
+ const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
4654
+ cwd: params.repoRoot
4655
+ });
4656
+ if (!fetchResult.ok) {
4657
+ return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
4537
4658
  }
4538
- /**
4539
- * Ждёт первого успешного `agent:hello` не дольше `timeoutMs` (по
4540
- * умолчанию 5 с — как раньше HTTP-пинг в `network.ts`). Таймаут НЕ
4541
- * останавливает клиент: переподключение по backoff продолжается в фоне,
4542
- * это только ограничение по времени для вызывающей стороны (`connect.ts`).
4543
- */
4544
- waitForFirstConnection(timeoutMs = 5e3) {
4545
- return new Promise((resolve4) => {
4546
- let settled = false;
4547
- const onConnected = (ack) => {
4548
- if (settled) return;
4549
- settled = true;
4550
- clearTimeout(timer);
4551
- this.off("connected", onConnected);
4552
- resolve4({ ok: true, ack });
4553
- };
4554
- const onAuthRejected = (message) => {
4555
- if (settled) return;
4556
- settled = true;
4557
- clearTimeout(timer);
4558
- this.off("connected", onConnected);
4559
- this.off("auth-rejected", onAuthRejected);
4560
- resolve4({ ok: false, message });
4561
- };
4562
- const timer = setTimeout(() => {
4563
- if (settled) return;
4564
- settled = true;
4565
- this.off("connected", onConnected);
4566
- this.off("auth-rejected", onAuthRejected);
4567
- resolve4({
4568
- ok: false,
4569
- message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 agent:hello \u0437\u0430 ${timeoutMs} \u043C\u0441 \u2014 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430. \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0432 \u0444\u043E\u043D\u0435 (backoff \xA711.4 \u0422\u0417).`
4570
- });
4571
- }, timeoutMs);
4572
- if (typeof timer.unref === "function") timer.unref();
4573
- this.on("connected", onConnected);
4574
- this.on("auth-rejected", onAuthRejected);
4659
+ let startPoint = `${remote}/${params.defaultBranch}`;
4660
+ if (params.continueFromBranch) {
4661
+ const continued = await runGit(["fetch", remote, params.continueFromBranch], {
4662
+ cwd: params.repoRoot
4575
4663
  });
4664
+ if (continued.ok) startPoint = `${remote}/${params.continueFromBranch}`;
4576
4665
  }
4577
- // -- Подключение --------------------------------------------------------
4578
- registerSocketListeners() {
4579
- this.socket.on("connect", () => this.handleConnect());
4580
- this.socket.on("connect_error", (err) => this.handleConnectError(err));
4581
- this.socket.on("disconnect", (reason) => this.handleDisconnect(String(reason)));
4582
- this.socket.on("lease:lost", (raw) => this.handleLeaseLost(raw));
4583
- this.socket.on("mode:set", (raw) => this.handleModeSet(raw));
4584
- this.socket.on("agent:pause", (raw) => this.handleAgentPause(raw));
4585
- this.socket.on("trust:updated", (raw) => this.handleTrustUpdated(raw));
4586
- this.socket.on("task:updated", (raw) => this.handleTaskUpdated(raw));
4587
- this.socket.on("question:answered", (raw) => this.handleQuestionAnswered(raw));
4588
- this.socket.on("approval:decided", (raw) => this.handleApprovalDecided(raw));
4666
+ mkdirSync6(dirname4(worktreeDir), { recursive: true });
4667
+ const branchExists = await localBranchExists(params.repoRoot, params.branchName);
4668
+ const addArgs = branchExists ? ["worktree", "add", worktreeDir, params.branchName] : ["worktree", "add", "-b", params.branchName, worktreeDir, startPoint];
4669
+ const addResult = await runGit(addArgs, { cwd: params.repoRoot });
4670
+ if (!addResult.ok) {
4671
+ return gitErr(describeGitFailure("git worktree add", addResult));
4589
4672
  }
4590
- handleConnect() {
4591
- if (this.stopped) return;
4592
- this.reconnectAttempt = 0;
4593
- void this.sendHello();
4673
+ return gitOk({ worktreeDir, reused: false });
4674
+ }
4675
+ async function removeWorktree(repoRoot, worktreeDir) {
4676
+ const result = await runGit(["worktree", "remove", worktreeDir], { cwd: repoRoot });
4677
+ if (!result.ok) {
4678
+ return gitErr(describeGitFailure("git worktree remove", result));
4594
4679
  }
4595
- handleConnectError(err) {
4596
- if (isAuthRejection(err)) {
4597
- this.stop();
4598
- this.emit("auth-rejected", describeAuthRejection(this.agentId));
4599
- return;
4600
- }
4601
- this.emit("client-error", toError2(err, "connect_error"));
4602
- this.scheduleReconnect();
4680
+ return gitOk(void 0);
4681
+ }
4682
+
4683
+ // src/git/submit.ts
4684
+ async function rebaseOntoDefault(params) {
4685
+ const remote = params.remote ?? "origin";
4686
+ const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
4687
+ if (!defaultBranchCheck.ok) return defaultBranchCheck;
4688
+ const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
4689
+ cwd: params.worktreeDir
4690
+ });
4691
+ if (!fetchResult.ok) {
4692
+ return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
4603
4693
  }
4604
- handleDisconnect(reason) {
4605
- this.emit("disconnected", reason);
4606
- if (this.stopped) return;
4607
- this.scheduleReconnect();
4694
+ const rebaseResult = await runGit(["rebase", `${remote}/${params.defaultBranch}`], {
4695
+ cwd: params.worktreeDir
4696
+ });
4697
+ if (rebaseResult.ok) {
4698
+ return gitOk({ outcome: "rebased", conflictedFiles: [] });
4608
4699
  }
4609
- scheduleReconnect() {
4610
- if (this.stopped || this.reconnectTimer) return;
4611
- const delayMs = nextReconnectDelayMs(this.reconnectAttempt, {
4612
- baseMs: this.opts.backoffBaseMs,
4613
- maxMs: this.opts.backoffMaxMs
4614
- });
4615
- this.reconnectAttempt += 1;
4616
- this.emit("reconnecting", { attempt: this.reconnectAttempt, delayMs });
4617
- this.reconnectTimer = setTimeout(() => {
4618
- this.reconnectTimer = void 0;
4619
- if (this.stopped) return;
4620
- this.socket.connect();
4621
- }, delayMs);
4700
+ const status = await getWorktreeStatus(params.worktreeDir);
4701
+ if (status.ok && status.data.conflictedPaths.length > 0) {
4702
+ const conflictedFiles = status.data.conflictedPaths;
4703
+ const abortResult = await runGit(["rebase", "--abort"], { cwd: params.worktreeDir });
4704
+ if (!abortResult.ok) {
4705
+ return gitErr(
4706
+ `\u041A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043F\u0440\u0438 rebase (${conflictedFiles.join(", ")}), \u0438 "git rebase --abort" \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u043B: ${describeGitFailure("git rebase --abort", abortResult)}. Worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u043D\u043E \u0440\u0443\u0447\u043D\u043E\u0435 \u0432\u043C\u0435\u0448\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E.`
4707
+ );
4708
+ }
4709
+ return gitOk({ outcome: "conflict", conflictedFiles });
4622
4710
  }
4623
- // -- agent:hello ---------------------------------------------------------
4624
- async sendHello() {
4625
- const state = readState(this.opts.stateFilePath);
4626
- const payload = agentHelloPayloadSchema.parse({
4627
- clientVersion: this.opts.clientVersion,
4628
- cwd: this.opts.cwd,
4629
- os: this.opts.os,
4630
- agentKind: this.opts.agentKind,
4631
- agentVersion: this.opts.agentVersion,
4632
- // `claudeVersion` шлётся ТОЛЬКО для Claude Code — и только чтобы
4633
- // клиент работал против ещё не обновлённого сервера, который знает
4634
- // одно это поле. Для Codex поле с таким именем было бы неправдой.
4635
- ...this.opts.agentKind === AgentKind.CLAUDE_CODE ? { claudeVersion: this.opts.agentVersion } : {},
4636
- roleKeys: [...this.opts.roleKeys],
4637
- ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {}
4638
- });
4639
- let raw;
4640
- try {
4641
- raw = await emitWithAckTimeout(this.socket, "agent:hello", payload, this.opts.ackTimeoutMs);
4642
- } catch (err) {
4643
- this.emit("client-error", toError2(err, "agent:hello"));
4644
- return;
4645
- }
4646
- if (this.stopped) return;
4647
- if (isAckError(raw)) {
4648
- this.emit("client-error", new Error(`agent:hello: ${raw.error.code}: ${raw.error.message}`));
4649
- return;
4650
- }
4651
- const parsed = agentHelloAckSchema.safeParse(raw);
4652
- if (!parsed.success) {
4653
- this.emit(
4654
- "client-error",
4655
- new Error(`agent:hello: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
4656
- );
4657
- return;
4658
- }
4659
- const ack = parsed.data;
4660
- this.lastHelloAck = ack;
4661
- this.paused = false;
4662
- this.pauseReasonText = void 0;
4663
- this.applyHelloAck(ack);
4664
- this.emit("connected", ack);
4665
- }
4666
- applyHelloAck(ack) {
4667
- const state = readState(this.opts.stateFilePath);
4668
- const patch = {
4669
- // OQ-06: сервер — источник правды, локальный режим перекрывается безусловно.
4670
- mode: ack.mode,
4671
- trust: toTrustCache(ack.trust, this.opts.now)
4672
- };
4673
- if (!ack.currentTask || ack.currentTask.id !== state.currentTask?.taskId) {
4674
- patch.currentTask = null;
4675
- }
4676
- updateState(patch, this.opts.stateFilePath);
4711
+ return gitErr(describeGitFailure("git rebase", rebaseResult));
4712
+ }
4713
+ async function assertReadyToPush(params) {
4714
+ const branch = await currentBranch(params.worktreeDir);
4715
+ if (!branch.ok) return branch;
4716
+ if (branch.data !== params.branchName) {
4717
+ return gitErr(
4718
+ `\u041E\u0442\u043A\u0430\u0437: worktree \u0441\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u0432\u0435\u0442\u043A\u0443 "${branch.data}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}" \u2014 \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E "\u0443\u0448\u043B\u043E \u043D\u0435 \u0442\u0443\u0434\u0430", push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
4719
+ );
4677
4720
  }
4678
- // -- agent:heartbeat -------------------------------------------------------
4679
- startHeartbeatTimer() {
4680
- if (this.heartbeatTimer) return;
4681
- this.heartbeatTimer = setInterval(() => {
4682
- void this.sendHeartbeat();
4683
- }, this.opts.heartbeatIntervalMs);
4721
+ const status = await getWorktreeStatus(params.worktreeDir);
4722
+ if (!status.ok) return status;
4723
+ if (!status.data.clean) {
4724
+ return gitErr(
4725
+ `\u041E\u0442\u043A\u0430\u0437: \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E \u0433\u0440\u044F\u0437\u043D\u043E\u0435, \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u043F\u0443\u0442\u0438: ${status.data.changedPaths.join(", ")}. Push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
4726
+ );
4684
4727
  }
4685
- stopHeartbeatTimer() {
4686
- if (!this.heartbeatTimer) return;
4687
- clearInterval(this.heartbeatTimer);
4688
- this.heartbeatTimer = void 0;
4728
+ return gitOk(void 0);
4729
+ }
4730
+ async function pushFastForward(params) {
4731
+ const remote = params.remote ?? "origin";
4732
+ const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
4733
+ if (!branchCheck.ok) return branchCheck;
4734
+ const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
4735
+ if (!defaultBranchCheck.ok) return defaultBranchCheck;
4736
+ const ready = await assertReadyToPush(params);
4737
+ if (!ready.ok) return ready;
4738
+ const pushResult = await runGit(["push", remote, `HEAD:refs/heads/${params.defaultBranch}`], {
4739
+ cwd: params.worktreeDir
4740
+ });
4741
+ if (!pushResult.ok) {
4742
+ return gitErr(
4743
+ describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.defaultBranch}`, pushResult)
4744
+ );
4689
4745
  }
4690
- async sendHeartbeat() {
4691
- if (!this.socket.connected) return;
4692
- const state = readState(this.opts.stateFilePath);
4693
- const payload = agentHeartbeatPayloadSchema.parse({
4694
- ...state.currentTask?.taskId ? { currentTaskId: state.currentTask.taskId } : {},
4695
- ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {},
4696
- status: state.currentTask ? AgentStatus.BUSY : AgentStatus.IDLE
4746
+ return gitOk({ pushedTo: `${remote}/${params.defaultBranch}` });
4747
+ }
4748
+ async function submitDirectTask(params) {
4749
+ const remote = params.remote ?? "origin";
4750
+ let worktreeDir;
4751
+ if (params.worktreeDir) {
4752
+ worktreeDir = params.worktreeDir;
4753
+ } else {
4754
+ const worktree = await ensureWorktree({
4755
+ repoRoot: params.repoRoot,
4756
+ branchName: params.branchName,
4757
+ defaultBranch: params.defaultBranch,
4758
+ remote,
4759
+ worktreesRoot: params.worktreesRoot
4697
4760
  });
4698
- let raw;
4699
- try {
4700
- raw = await emitWithAckTimeout(
4701
- this.socket,
4702
- "agent:heartbeat",
4703
- payload,
4704
- this.opts.ackTimeoutMs
4705
- );
4706
- } catch (err) {
4707
- this.emit("client-error", toError2(err, "agent:heartbeat"));
4708
- return;
4709
- }
4710
- if (isAckError(raw)) {
4711
- this.emit(
4712
- "client-error",
4713
- new Error(`agent:heartbeat: ${raw.error.code}: ${raw.error.message}`)
4714
- );
4715
- return;
4716
- }
4717
- const parsed = agentHeartbeatAckSchema.safeParse(raw);
4718
- if (!parsed.success) {
4719
- this.emit(
4720
- "client-error",
4721
- new Error(`agent:heartbeat: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
4722
- );
4723
- return;
4724
- }
4725
- this.emit("heartbeat", parsed.data);
4726
- }
4727
- // -- Пуш-события сервера (§11.2 ТЗ) --------------------------------------
4728
- handleLeaseLost(raw) {
4729
- const parsed = leaseLostPayloadSchema.safeParse(raw);
4730
- if (!parsed.success) {
4731
- this.emit(
4732
- "client-error",
4733
- new Error(`lease:lost: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4734
- );
4735
- return;
4736
- }
4737
- const { taskId, reason } = parsed.data;
4738
- this.leaseLostTasks.set(taskId, reason);
4739
- const state = readState(this.opts.stateFilePath);
4740
- if (state.currentTask?.taskId === taskId) {
4741
- updateState({ currentTask: null }, this.opts.stateFilePath);
4742
- }
4743
- this.emit("lease:lost", parsed.data);
4761
+ if (!worktree.ok) return { status: "refused", message: worktree.message };
4762
+ worktreeDir = worktree.data.worktreeDir;
4744
4763
  }
4745
- handleModeSet(raw) {
4746
- const parsed = modeSetPayloadSchema.safeParse(raw);
4747
- if (!parsed.success) {
4748
- this.emit("client-error", new Error(`mode:set: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`));
4749
- return;
4750
- }
4751
- updateState({ mode: parsed.data.mode }, this.opts.stateFilePath);
4752
- this.emit("mode:set", parsed.data);
4764
+ const commit = await commitAll({
4765
+ worktreeDir,
4766
+ commitPrefix: params.commitPrefix,
4767
+ title: params.taskTitle,
4768
+ taskKey: params.taskKey
4769
+ });
4770
+ if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
4771
+ const rebase = await rebaseOntoDefault({
4772
+ worktreeDir,
4773
+ defaultBranch: params.defaultBranch,
4774
+ remote
4775
+ });
4776
+ if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
4777
+ if (rebase.data.outcome === "conflict") {
4778
+ const files = rebase.data.conflictedFiles;
4779
+ return {
4780
+ status: "conflict",
4781
+ worktreeDir,
4782
+ conflictedFiles: files,
4783
+ message: `Rebase \u043D\u0430 ${remote}/${params.defaultBranch} \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0432 \u0444\u0430\u0439\u043B\u0430\u0445: ${files.join(", ")}. Rebase \u043E\u0442\u043C\u0435\u043D\u0451\u043D ("git rebase --abort"), \u0432\u0435\u0442\u043A\u0430 "${params.branchName}" \u0438 \u0435\u0451 worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`
4784
+ };
4753
4785
  }
4754
- handleAgentPause(raw) {
4755
- const parsed = agentPausePayloadSchema.safeParse(raw);
4756
- if (!parsed.success) {
4757
- this.emit(
4758
- "client-error",
4759
- new Error(`agent:pause: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4760
- );
4761
- return;
4762
- }
4763
- this.paused = true;
4764
- this.pauseReasonText = parsed.data.reason;
4765
- this.emit("pause", parsed.data);
4786
+ if (params.requireTestsPassed && !params.testsPassed) {
4787
+ return {
4788
+ status: "refused",
4789
+ worktreeDir,
4790
+ message: "\u0414\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 requireTests=true, \u0430 testsPassed \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D (\u0438\u043B\u0438 false) \u2014 push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F."
4791
+ };
4766
4792
  }
4767
- handleTrustUpdated(raw) {
4768
- const parsed = trustUpdatedPayloadSchema.safeParse(raw);
4769
- if (!parsed.success) {
4770
- this.emit(
4771
- "client-error",
4772
- new Error(`trust:updated: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4773
- );
4774
- return;
4793
+ const push = await pushFastForward({
4794
+ worktreeDir,
4795
+ branchName: params.branchName,
4796
+ defaultBranch: params.defaultBranch,
4797
+ remote
4798
+ });
4799
+ if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
4800
+ const sha = await getHeadSha(worktreeDir);
4801
+ let worktreeRemoved = false;
4802
+ if (params.removeWorktreeOnSuccess !== false) {
4803
+ const removed = await removeWorktree(params.repoRoot, worktreeDir);
4804
+ if (removed.ok) {
4805
+ await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
4806
+ worktreeRemoved = true;
4775
4807
  }
4776
- updateState({ trust: toTrustCache(parsed.data, this.opts.now) }, this.opts.stateFilePath);
4777
- this.emit("trust:updated", parsed.data);
4778
- }
4779
- handleTaskUpdated(raw) {
4780
- const parsed = taskUpdatedAgentPayloadSchema.safeParse(raw);
4781
- if (parsed.success) this.emit("task:updated", parsed.data);
4782
4808
  }
4783
- handleQuestionAnswered(raw) {
4784
- const parsed = questionAnsweredPayloadSchema.safeParse(raw);
4785
- if (parsed.success) this.emit("question:answered", parsed.data);
4809
+ return {
4810
+ status: "pushed",
4811
+ worktreeDir,
4812
+ commitSha: sha.ok ? sha.data : null,
4813
+ pushedTo: push.data.pushedTo,
4814
+ worktreeRemoved
4815
+ };
4816
+ }
4817
+
4818
+ // src/git/submit-pr.ts
4819
+ async function pushTaskBranch(params) {
4820
+ const remote = params.remote ?? "origin";
4821
+ const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
4822
+ if (!branchCheck.ok) return branchCheck;
4823
+ const ready = await assertReadyToPush(params);
4824
+ if (!ready.ok) return ready;
4825
+ const pushResult = await runGit(
4826
+ ["push", "--set-upstream", remote, `HEAD:refs/heads/${params.branchName}`],
4827
+ { cwd: params.worktreeDir }
4828
+ );
4829
+ if (!pushResult.ok) {
4830
+ return gitErr(
4831
+ describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.branchName}`, pushResult)
4832
+ );
4786
4833
  }
4787
- /**
4788
- * §14.4 ТЗ, п.4-5 — асинхронный путь (в дополнение к синхронному в
4789
- * `mcp/request-approval.ts::settleApproval`, который срабатывает, когда
4790
- * решение приходит в рамках ОДНОГО вызова `request_approval`/
4791
- * `wait_approval`). Этот пуш ловит случай, когда решение пришло уже
4792
- * ПОСЛЕ того, как собственный long-poll тула истёк (вернул `PENDING`),
4793
- * а модель ещё не успела/не стала звать `wait_approval` снова — на
4794
- * постоянном соединении (`quiel mcp`, единственный держатель ЭТОГО
4795
- * класса) `approval:decided` долетит и без явного повторного вызова.
4796
- *
4797
- * `approvalDecidedPayloadSchema` несёт только `approvalId`+`status`+
4798
- * `oneTimeToken?` — НЕ `command`/`cwd` (провод их не отдаёт, см.
4799
- * `state/schema.ts::PendingApprovalSchema`), поэтому command+cwd берутся
4800
- * из `state.pendingApprovals[approvalId]`, куда их кладёт
4801
- * `request_approval` сразу после создания запроса. Если записи там нет
4802
- * (approval создан не этим клиентом/до перезапуска процесса, либо уже
4803
- * обработан) — писать пока нечего, `pendingApprovals` и так пуст.
4804
- */
4805
- handleApprovalDecided(raw) {
4806
- const parsed = approvalDecidedPayloadSchema.safeParse(raw);
4807
- if (!parsed.success) {
4808
- this.emit(
4809
- "client-error",
4810
- new Error(`approval:decided: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4811
- );
4812
- return;
4813
- }
4814
- this.applyApprovalDecided(parsed.data);
4815
- this.emit("approval:decided", parsed.data);
4834
+ return gitOk({ pushedTo: `${remote}/${params.branchName}` });
4835
+ }
4836
+ async function submitPullRequestTask(params) {
4837
+ const remote = params.remote ?? "origin";
4838
+ let worktreeDir;
4839
+ if (params.worktreeDir) {
4840
+ worktreeDir = params.worktreeDir;
4841
+ } else {
4842
+ const worktree = await ensureWorktree({
4843
+ repoRoot: params.repoRoot,
4844
+ branchName: params.branchName,
4845
+ defaultBranch: params.defaultBranch,
4846
+ remote,
4847
+ worktreesRoot: params.worktreesRoot
4848
+ });
4849
+ if (!worktree.ok) return { status: "refused", message: worktree.message };
4850
+ worktreeDir = worktree.data.worktreeDir;
4816
4851
  }
4817
- applyApprovalDecided(payload) {
4818
- const state = readState(this.opts.stateFilePath);
4819
- const pending = state.pendingApprovals[payload.approvalId];
4820
- if (!pending) return;
4821
- if (payload.status === ApprovalStatus.APPROVED && payload.oneTimeToken) {
4822
- const approvalTtlMin = state.trust?.rules.approvalTtlMin ?? 60;
4823
- const expiresAt = new Date(this.opts.now().getTime() + approvalTtlMin * 6e4).toISOString();
4824
- updateState(
4825
- {
4826
- ...storeApprovedCommand(
4827
- state,
4828
- { command: pending.command, cwd: pending.cwd, token: payload.oneTimeToken, expiresAt },
4829
- this.opts.now
4830
- ),
4831
- ...clearPendingApproval(state, payload.approvalId)
4832
- },
4833
- this.opts.stateFilePath
4834
- );
4835
- return;
4852
+ const commit = await commitAll({
4853
+ worktreeDir,
4854
+ commitPrefix: params.commitPrefix,
4855
+ title: params.taskTitle,
4856
+ taskKey: params.taskKey
4857
+ });
4858
+ if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
4859
+ const rebase = await rebaseOntoDefault({
4860
+ worktreeDir,
4861
+ defaultBranch: params.defaultBranch,
4862
+ remote
4863
+ });
4864
+ if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
4865
+ const conflicted = rebase.data.outcome === "conflict";
4866
+ const push = await pushTaskBranch({ worktreeDir, branchName: params.branchName, remote });
4867
+ if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
4868
+ const sha = await getHeadSha(worktreeDir);
4869
+ let worktreeRemoved = false;
4870
+ if (params.removeWorktreeOnSuccess !== false) {
4871
+ const removed = await removeWorktree(params.repoRoot, worktreeDir);
4872
+ if (removed.ok) {
4873
+ await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
4874
+ worktreeRemoved = true;
4836
4875
  }
4837
- updateState(clearPendingApproval(state, payload.approvalId), this.opts.stateFilePath);
4838
4876
  }
4839
- };
4840
- function trimTrailingSlash2(url) {
4841
- return url.endsWith("/") ? url.slice(0, -1) : url;
4877
+ return {
4878
+ status: "pushed",
4879
+ worktreeDir,
4880
+ branchName: params.branchName,
4881
+ commitSha: sha.ok ? sha.data : null,
4882
+ pushedTo: push.data.pushedTo,
4883
+ conflicted,
4884
+ conflictedFiles: rebase.data.conflictedFiles,
4885
+ worktreeRemoved
4886
+ };
4842
4887
  }
4843
4888
 
4844
- // src/socket/send-agent-mode.ts
4845
- var DEFAULT_TIMEOUT_MS = 8e3;
4846
- function trimTrailingSlash3(url) {
4847
- return url.endsWith("/") ? url.slice(0, -1) : url;
4848
- }
4849
- async function sendAgentMode(options) {
4850
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4851
- const factory = options.createSocket ?? createSocket;
4852
- const socket = factory(`${trimTrailingSlash3(options.url)}/agent`, {
4853
- auth: { token: options.token },
4854
- autoConnect: false,
4855
- reconnection: false
4889
+ // src/git/rescue.ts
4890
+ var RescueReason = {
4891
+ /** Модель упёрлась в лимит подписки (`StopFailure`, matcher `rate_limit`). */
4892
+ RATE_LIMIT: "rate_limit",
4893
+ /** Сервер забрал задачу: аренда истекла или её отдали другому. */
4894
+ LEASE_LOST: "lease_lost"
4895
+ };
4896
+ var REASON_TEXT = {
4897
+ [RescueReason.RATE_LIMIT]: "\u043F\u0440\u0435\u0440\u0432\u0430\u043D\u043E: \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u043B\u0438\u043C\u0438\u0442 \u043F\u043E\u0434\u043F\u0438\u0441\u043A\u0438",
4898
+ [RescueReason.LEASE_LOST]: "\u043F\u0440\u0435\u0440\u0432\u0430\u043D\u043E: \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0442\u043E\u0437\u0432\u0430\u043D\u0430 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u043E\u0439"
4899
+ };
4900
+ async function rescueUnfinishedWork(params) {
4901
+ const status = await getWorktreeStatus(params.worktreeDir);
4902
+ if (!status.ok) return status;
4903
+ if (status.data.clean) {
4904
+ return gitOk({ committed: false, sha: null, pushedTo: null });
4905
+ }
4906
+ const what = params.title?.trim();
4907
+ const commit = await commitAll({
4908
+ worktreeDir: params.worktreeDir,
4909
+ commitPrefix: params.commitPrefix,
4910
+ title: what ? `${what} (${REASON_TEXT[params.reason]})` : REASON_TEXT[params.reason],
4911
+ taskKey: params.taskKey
4856
4912
  });
4857
- try {
4858
- await connectOnce(socket, timeoutMs);
4859
- const payload = agentModePayloadSchema.parse({ mode: options.mode });
4860
- const raw = await emitWithAckTimeout(socket, "agent:mode", payload, timeoutMs);
4861
- if (isAckError(raw)) {
4862
- return { ok: false, message: `${raw.error.code}: ${raw.error.message}` };
4863
- }
4864
- const parsed = agentModeAckSchema.safeParse(raw);
4865
- if (!parsed.success) {
4866
- return { ok: false, message: `\u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}` };
4867
- }
4868
- if (!parsed.data.ok) {
4869
- return { ok: false, message: "\u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B agent:mode (ok:false)" };
4870
- }
4871
- return { ok: true, mode: parsed.data.mode ?? options.mode };
4872
- } catch (err) {
4873
- return { ok: false, message: err instanceof Error ? err.message : String(err) };
4874
- } finally {
4875
- socket.disconnect();
4913
+ if (!commit.ok) return commit;
4914
+ if (!commit.data.committed) {
4915
+ return gitOk({ committed: false, sha: null, pushedTo: null });
4916
+ }
4917
+ const push = await pushTaskBranch({
4918
+ worktreeDir: params.worktreeDir,
4919
+ branchName: params.branchName
4920
+ });
4921
+ if (!push.ok) {
4922
+ return gitErr(
4923
+ `\u0420\u0430\u0431\u043E\u0442\u0430 \u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u0430 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E (${commit.data.sha ?? "\u0431\u0435\u0437 sha"}), \u043D\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0432\u0435\u0442\u043A\u0443 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${push.message}`
4924
+ );
4876
4925
  }
4926
+ return gitOk({
4927
+ committed: true,
4928
+ sha: commit.data.sha,
4929
+ pushedTo: push.data.pushedTo
4930
+ });
4877
4931
  }
4878
4932
 
4879
- // src/mcp/helpers.ts
4880
- function textResult(text) {
4881
- return { content: [{ type: "text", text }] };
4882
- }
4883
- function errorResult(text) {
4884
- return { content: [{ type: "text", text }], isError: true };
4885
- }
4886
- function leaseLostResult(taskLabel2, reason) {
4887
- return errorResult(
4888
- `\u0410\u0440\u0435\u043D\u0434\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 ${taskLabel2} \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430: ${reason}. \u041F\u0440\u0435\u043A\u0440\u0430\u0442\u0438 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430\u0434 \u043D\u0435\u0439 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E \u2014 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0441\u0434\u0430\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430, \u0432\u044B\u0437\u043E\u0432\u0438 next_task \u0437\u0430 \u043D\u043E\u0432\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435\u0439.`
4933
+ // src/hooks/stop-failure.ts
4934
+ import { z as z37 } from "zod";
4935
+
4936
+ // src/hooks/reset-time.ts
4937
+ var DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
4938
+ function parseClock(hour, minute, meridiem) {
4939
+ const lower = meridiem.toLowerCase();
4940
+ let h = hour % 12;
4941
+ if (lower === "pm") h += 12;
4942
+ return { h, m: minute };
4943
+ }
4944
+ function parseResetAt(message, now) {
4945
+ if (!message) return void 0;
4946
+ const withDay = message.match(
4947
+ /resets\s+(sun|mon|tue|wed|thu|fri|sat)[a-z]*\s+(\d{1,2}):(\d{2})\s*(am|pm)/i
4889
4948
  );
4949
+ if (withDay) {
4950
+ const [, dayRaw = "", hourRaw = "", minuteRaw = "", meridiem = ""] = withDay;
4951
+ const targetDay = DAYS.indexOf(dayRaw.slice(0, 3).toLowerCase());
4952
+ if (targetDay === -1) return void 0;
4953
+ const { h, m } = parseClock(Number(hourRaw), Number(minuteRaw), meridiem);
4954
+ const result = new Date(now);
4955
+ result.setHours(h, m, 0, 0);
4956
+ let shift = (targetDay - result.getDay() + 7) % 7;
4957
+ if (shift === 0 && result.getTime() <= now.getTime()) shift = 7;
4958
+ result.setDate(result.getDate() + shift);
4959
+ return result;
4960
+ }
4961
+ const withTime = message.match(/resets\s+(\d{1,2}):(\d{2})\s*(am|pm)/i);
4962
+ if (withTime) {
4963
+ const [, hourRaw = "", minuteRaw = "", meridiem = ""] = withTime;
4964
+ const { h, m } = parseClock(Number(hourRaw), Number(minuteRaw), meridiem);
4965
+ const result = new Date(now);
4966
+ result.setHours(h, m, 0, 0);
4967
+ if (result.getTime() <= now.getTime()) result.setDate(result.getDate() + 1);
4968
+ return result;
4969
+ }
4970
+ return void 0;
4890
4971
  }
4891
- function describeZodIssues(error) {
4892
- return error.issues.map(
4893
- (issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message
4894
- ).join("; ");
4972
+
4973
+ // src/hooks/stop-failure.ts
4974
+ var StopFailureErrorTypeSchema = z37.enum([
4975
+ "rate_limit",
4976
+ "overloaded",
4977
+ "authentication_failed",
4978
+ "oauth_org_not_allowed",
4979
+ "account_on_hold",
4980
+ "billing_error",
4981
+ "invalid_request",
4982
+ "model_not_found",
4983
+ "server_error",
4984
+ "max_output_tokens",
4985
+ // Добавлен 21 сентября 2026 по живой документации: его в нашем
4986
+ // перечислении не было, и `StopFailureInputSchema` отклонила бы вход с
4987
+ // ним целиком — хук отработал бы вхолостую, а сервер не узнал бы, что
4988
+ // у агента что-то случилось.
4989
+ "cloud_credential_error",
4990
+ "unknown"
4991
+ ]);
4992
+ var StopFailureInputSchema = z37.object({
4993
+ hook_event_name: z37.literal("StopFailure"),
4994
+ session_id: z37.string(),
4995
+ cwd: z37.string(),
4996
+ error: StopFailureErrorTypeSchema,
4997
+ error_details: z37.unknown().optional(),
4998
+ last_assistant_message: z37.string().optional()
4999
+ }).passthrough();
5000
+ function extractRetryAfterSec(errorDetails) {
5001
+ if (typeof errorDetails === "number" && Number.isFinite(errorDetails) && errorDetails >= 0) {
5002
+ return errorDetails;
5003
+ }
5004
+ if (errorDetails && typeof errorDetails === "object") {
5005
+ const record = errorDetails;
5006
+ for (const key of ["retryAfterSec", "retryAfter", "retry_after", "retry_after_seconds"]) {
5007
+ const value = record[key];
5008
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
5009
+ }
5010
+ }
5011
+ if (typeof errorDetails === "string") {
5012
+ const match = errorDetails.match(/retry[-_ ]?after[^0-9]{0,10}(\d+)/i);
5013
+ if (match?.[1] !== void 0) return Number(match[1]);
5014
+ }
5015
+ return void 0;
4895
5016
  }
4896
- function parseArgsOrError(schema, raw) {
4897
- const parsed = schema.safeParse(raw);
4898
- if (!parsed.success) {
4899
- return { ok: false, message: `\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B: ${describeZodIssues(parsed.error)}` };
5017
+ var RESCUE_WORK_ACTION = "rescue-work";
5018
+ function stopFailureHook(input, state) {
5019
+ const retryAfterSec = extractRetryAfterSec(input.error_details);
5020
+ const resetAt = parseResetAt(input.last_assistant_message, /* @__PURE__ */ new Date());
5021
+ const actions = [
5022
+ {
5023
+ type: "agent:rate_limited",
5024
+ payload: {
5025
+ error: input.error,
5026
+ ...retryAfterSec === void 0 ? {} : { retryAfterSec },
5027
+ ...resetAt ? { resetsAt: resetAt.toISOString() } : {}
5028
+ }
5029
+ }
5030
+ ];
5031
+ const task = state.currentTask;
5032
+ if (task?.worktreeDir && task.branchName && task.commitPrefix) {
5033
+ actions.push({
5034
+ type: RESCUE_WORK_ACTION,
5035
+ payload: {
5036
+ reason: "rate_limit",
5037
+ taskId: task.taskId,
5038
+ worktreeDir: task.worktreeDir,
5039
+ branchName: task.branchName,
5040
+ commitPrefix: task.commitPrefix,
5041
+ taskKey: task.taskKey ?? task.taskId
5042
+ }
5043
+ });
4900
5044
  }
4901
- return { ok: true, data: parsed.data };
5045
+ return { output: {}, serverActions: actions };
4902
5046
  }
4903
- async function callServerOrError(client, event, payload, schema, timeoutMs) {
4904
- let raw;
4905
- try {
4906
- raw = await client.callServer(event, payload, timeoutMs);
4907
- } catch (err) {
4908
- return {
4909
- ok: false,
4910
- message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B \u043D\u0430 ${event} (${err instanceof Error ? err.message : String(err)}). \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F \u0432 \u0444\u043E\u043D\u0435 \u2014 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439 \u0435\u0449\u0451 \u0440\u0430\u0437 \u0447\u0443\u0442\u044C \u043F\u043E\u0437\u0436\u0435.`
4911
- };
5047
+
5048
+ // src/hooks/rescue-runner.ts
5049
+ var NOTHING = {
5050
+ attempted: false,
5051
+ committed: false,
5052
+ pushedTo: null,
5053
+ problem: null,
5054
+ report: null
5055
+ };
5056
+ function readParams(action) {
5057
+ const p = action.payload ?? {};
5058
+ const str = (key) => {
5059
+ const value = p[key];
5060
+ return typeof value === "string" && value.length > 0 ? value : null;
5061
+ };
5062
+ const worktreeDir = str("worktreeDir");
5063
+ const branchName = str("branchName");
5064
+ const commitPrefix = str("commitPrefix");
5065
+ const taskKey = str("taskKey");
5066
+ const taskId = str("taskId");
5067
+ if (!worktreeDir || !branchName || !commitPrefix || !taskKey || !taskId) return null;
5068
+ const reason = p.reason === RescueReason.LEASE_LOST ? RescueReason.LEASE_LOST : RescueReason.RATE_LIMIT;
5069
+ return { worktreeDir, branchName, commitPrefix, taskKey, taskId, reason };
5070
+ }
5071
+ async function runRescueAction(actions) {
5072
+ const action = actions.find((candidate) => candidate.type === RESCUE_WORK_ACTION);
5073
+ if (!action) return NOTHING;
5074
+ const params = readParams(action);
5075
+ if (!params) {
5076
+ return { ...NOTHING, attempted: true, problem: "\u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u043E \u0437\u0430\u0434\u0430\u0447\u0435" };
4912
5077
  }
4913
- if (isAckError(raw)) {
5078
+ try {
5079
+ const result = await rescueUnfinishedWork(params);
5080
+ if (!result.ok) {
5081
+ return { ...NOTHING, attempted: true, problem: result.message };
5082
+ }
5083
+ const report = result.data.committed ? {
5084
+ type: "task:work_saved",
5085
+ payload: {
5086
+ taskId: params.taskId,
5087
+ branchName: params.branchName,
5088
+ ...result.data.sha ? { sha: result.data.sha } : {}
5089
+ }
5090
+ } : null;
4914
5091
  return {
4915
- ok: false,
4916
- message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u0430\u0437\u0430\u043B\u0430 \u043D\u0430 ${event}: ${raw.error.code}: ${raw.error.message}`
5092
+ attempted: true,
5093
+ committed: result.data.committed,
5094
+ pushedTo: result.data.pushedTo,
5095
+ problem: null,
5096
+ report
4917
5097
  };
4918
- }
4919
- const parsed = schema.safeParse(raw);
4920
- if (!parsed.success) {
5098
+ } catch (error) {
4921
5099
  return {
4922
- ok: false,
4923
- message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043F\u0440\u0438\u0441\u043B\u0430\u043B\u0430 \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 \u043D\u0430 ${event}: ${parsed.error.message}`
5100
+ ...NOTHING,
5101
+ attempted: true,
5102
+ problem: error instanceof Error ? error.message : String(error)
4924
5103
  };
4925
5104
  }
4926
- return { ok: true, data: parsed.data };
5105
+ }
5106
+ function describeRescue(result) {
5107
+ if (!result.attempted) return null;
5108
+ if (result.problem) return `Quiel: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043D\u0435\u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D\u043D\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u2014 ${result.problem}`;
5109
+ if (!result.committed) return null;
5110
+ return `Quiel: \u043D\u0435\u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430${result.pushedTo ? ` \u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430 \u0432 ${result.pushedTo}` : ""}`;
4927
5111
  }
4928
5112
 
4929
- // src/mcp/ask.ts
4930
- var askHumanInputShape = {
4931
- textMd: questionAskPayloadSchema.shape.textMd.describe(
4932
- "\u0422\u0435\u043A\u0441\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, Markdown. \u041F\u0438\u0448\u0438 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0431\u0435\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430 \u0441 \u043C\u043E\u0434\u0435\u043B\u044C\u044E."
4933
- ),
4934
- targetRole: questionAskPayloadSchema.shape.targetRole.describe(
4935
- '\u0420\u043E\u043B\u044C \u0430\u0434\u0440\u0435\u0441\u0430\u0442\u0430 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, "owner") \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u0443\u0432\u0438\u0434\u044F\u0442 \u0432\u0441\u0435 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441 \u044D\u0442\u043E\u0439 \u0440\u043E\u043B\u044C\u044E, \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u043F\u0435\u0440\u0432\u044B\u0439. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E.'
4936
- ),
4937
- targetMemberId: questionAskPayloadSchema.shape.targetMemberId.describe(
4938
- "ID \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0430\u0434\u0440\u0435\u0441\u0430\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E."
4939
- ),
4940
- wantsFiles: questionAskPayloadSchema.shape.wantsFiles.describe(
4941
- "\u041F\u043E\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u043A \u043E\u0442\u0432\u0435\u0442\u0443 (\u0441\u043A\u0440\u0438\u043D\u0448\u043E\u0442, \u043B\u043E\u0433, \u0432\u044B\u0433\u0440\u0443\u0437\u043A\u0443). \u0424\u043E\u0440\u043C\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443 \u0441 \u0432\u044B\u0431\u043E\u0440\u043E\u043C \u0444\u0430\u0439\u043B\u0430. \u042D\u0442\u043E \u041F\u0420\u041E\u0421\u042C\u0411\u0410, \u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435: \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0432\u043F\u0440\u0430\u0432\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u0431\u0435\u0437 \u0444\u0430\u0439\u043B\u043E\u0432, \u0438 \u0442\u043E\u0433\u0434\u0430 `attachments` \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u0431\u0443\u0434\u0435\u0442 \u043F\u0443\u0441\u0442\u044B\u043C."
4942
- ),
4943
- options: questionAskPayloadSchema.shape.options.describe(
4944
- "\u0412\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u043E\u0442\u0432\u0435\u0442\u0430 (\u043F\u043E\u043A\u0430\u0436\u0443\u0442\u0441\u044F \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438 \u0432 UI/Telegram). \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0439 \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C."
4945
- ),
4946
- blocking: questionAskPayloadSchema.shape.blocking.describe(
4947
- 'true \u2014 \u0442\u0432\u043E\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 \u0441\u0442\u0430\u0442\u0443\u0441 WAITING_HUMAN, \u0438 \u044D\u0442\u043E\u0442 \u0432\u044B\u0437\u043E\u0432 \u0441\u0430\u043C \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442 \u0434\u043E 600 \u0441; \u0435\u0441\u043B\u0438 \u0437\u0430 \u044D\u0442\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435\u0442, \u0432\u044B\u0437\u043E\u0432 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0441 \u043F\u043E\u043C\u0435\u0442\u043A\u043E\u0439 "pending" \u2014 \u0442\u043E\u0433\u0434\u0430 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430, \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430. false \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u043F\u0440\u043E\u0441\u0442\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F, \u0431\u0435\u0437 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0438 \u0431\u0435\u0437 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 (\u0434\u043B\u044F \u043D\u0435 \u0441\u0440\u043E\u0447\u043D\u044B\u0445 \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0439).'
4948
- )
4949
- };
4950
- var BLOCKING_WAIT_SEC = 600;
4951
- var ASK_ACK_BUFFER_MS = 15e3;
4952
- async function askHumanHandler(args, runtime) {
4953
- const state = readState(runtime.stateFilePath);
4954
- const taskId = state.currentTask?.taskId;
4955
- const waitSec = args.blocking ? BLOCKING_WAIT_SEC : 0;
4956
- const parsedArgs = parseArgsOrError(questionAskPayloadSchema, {
4957
- ...taskId ? { taskId } : {},
4958
- textMd: args.textMd,
4959
- ...args.targetRole ? { targetRole: args.targetRole } : {},
4960
- ...args.targetMemberId ? { targetMemberId: args.targetMemberId } : {},
4961
- ...args.options && args.options.length > 0 ? { options: args.options } : {},
4962
- ...args.wantsFiles ? { wantsFiles: true } : {},
4963
- blocking: args.blocking,
4964
- waitSec
4965
- });
4966
- if (!parsedArgs.ok) return errorResult(parsedArgs.message);
4967
- if (taskId) {
4968
- const lease = runtime.client.isLeaseLost(taskId);
4969
- if (lease.lost) return leaseLostResult(state.currentTask?.taskKey ?? taskId, lease.reason);
4970
- }
4971
- const result = await callServerOrError(
4972
- runtime.client,
4973
- "question:ask",
4974
- parsedArgs.data,
4975
- questionAskAckSchema,
4976
- waitSec * 1e3 + ASK_ACK_BUFFER_MS
4977
- );
4978
- if (!result.ok) return errorResult(result.message);
4979
- const settled = describeSettledQuestion(result.data.question);
4980
- if (settled) return textResult(settled);
4981
- if (args.blocking) {
4982
- return textResult(
4983
- `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0437\u0430\u0434\u0430\u043D, \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043D\u0430 \u0432 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 (WAITING_HUMAN). \u041E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E ${BLOCKING_WAIT_SEC} \u0441 (pending) \u2014 \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}", \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0436\u0434\u0430\u0442\u044C. \u041D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443 \u043F\u043E \u0437\u0430\u0434\u0430\u0447\u0435, \u043F\u043E\u043A\u0430 \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u0448\u044C \u043E\u0442\u0432\u0435\u0442.`
4984
- );
4985
- }
4986
- return textResult(
4987
- `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D (\u043D\u0435 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443). \u041E\u0442\u0432\u0435\u0442\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u2014 \u0435\u0441\u043B\u0438 \u043E\u043D \u043D\u0443\u0436\u0435\u043D, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C, \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}".`
4988
- );
5113
+ // src/socket/auth-reject.ts
5114
+ var AUTH_REJECT_REASONS = /* @__PURE__ */ new Set(["missing", "invalid"]);
5115
+ function isAuthRejection(error) {
5116
+ const message = error instanceof Error ? error.message : String(error);
5117
+ return AUTH_REJECT_REASONS.has(message.trim());
4989
5118
  }
4990
- var waitAnswerInputShape = {
4991
- questionId: questionWaitPayloadSchema.shape.questionId.describe(
4992
- "ID \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u2014 \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430 ask_human (question.id) \u0438\u043B\u0438 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0433\u043E \u0432\u044B\u0437\u043E\u0432\u0430 wait_answer."
4993
- ),
4994
- waitSec: questionWaitPayloadSchema.shape.waitSec.optional().describe("\u0421\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0435\u043A\u0443\u043D\u0434 \u0436\u0434\u0430\u0442\u044C \u043E\u0442\u0432\u0435\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u044B\u0437\u043E\u0432\u0435, 0..600. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E 300.")
4995
- };
4996
- var DEFAULT_WAIT_ANSWER_SEC = 300;
4997
- var WAIT_ANSWER_ACK_BUFFER_MS = 15e3;
4998
- async function waitAnswerHandler(args, runtime) {
4999
- const waitSec = args.waitSec ?? DEFAULT_WAIT_ANSWER_SEC;
5000
- const parsedArgs = parseArgsOrError(questionWaitPayloadSchema, {
5001
- questionId: args.questionId,
5002
- waitSec
5003
- });
5004
- if (!parsedArgs.ok) return errorResult(parsedArgs.message);
5005
- const result = await callServerOrError(
5006
- runtime.client,
5007
- "question:wait",
5008
- parsedArgs.data,
5009
- questionWaitAckSchema,
5010
- waitSec * 1e3 + WAIT_ANSWER_ACK_BUFFER_MS
5011
- );
5012
- if (!result.ok) return errorResult(result.message);
5013
- const settled = describeSettledQuestion(result.data.question);
5014
- if (settled) return textResult(settled);
5015
- return textResult(
5016
- `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0432\u0441\u0451 \u0435\u0449\u0451 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 (\u0436\u0434\u0430\u043B ${waitSec} \u0441). \u0412\u044B\u0437\u043E\u0432\u0438 wait_answer \u0435\u0449\u0451 \u0440\u0430\u0437 \u0441 \u0442\u0435\u043C \u0436\u0435 questionId, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0443\u043C\u0435\u0441\u0442\u043D\u043E.`
5017
- );
5119
+ function describeAuthRejection(agentId) {
5120
+ return `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B\u0430 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 ${agentId}: \u043E\u043D \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D, \u043E\u0442\u043E\u0437\u0432\u0430\u043D \u0438\u043B\u0438 \u0438\u0441\u043F\u043E\u0440\u0447\u0435\u043D. \u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043D\u0435 \u043F\u043E\u043C\u043E\u0436\u0435\u0442 \u2014 \u0432\u044B\u043F\u0443\u0441\u0442\u0438 \u043D\u043E\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0430\u0433\u0435\u043D\u0442\u0430 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438 "quiel init" \u0437\u0430\u043D\u043E\u0432\u043E.`;
5018
5121
  }
5019
- function describeAnswerAttachments(question) {
5020
- if (question.attachments.length === 0) return "";
5021
- const lines = question.attachments.map(
5022
- (file) => file.downloadUrl ? `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u043A\u0430\u0447\u0430\u0439: ${file.downloadUrl}` : `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430, \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442`
5023
- );
5024
- return `
5025
5122
 
5026
- \u041A \u043E\u0442\u0432\u0435\u0442\u0443 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u044B \u0444\u0430\u0439\u043B\u044B (${question.attachments.length}). \u042D\u0442\u043E \u0447\u0430\u0441\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u0430: \u0441\u043A\u0430\u0447\u0430\u0439 \u0438 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u041A\u0410\u0416\u0414\u042B\u0419, \u0441\u0441\u044B\u043B\u043A\u0438 \u0436\u0438\u0432\u0443\u0442 ${question.attachments[0]?.expiresInSeconds ?? 300} \u0441 \u2014 \u043D\u0435 \u043E\u0442\u043A\u043B\u0430\u0434\u044B\u0432\u0430\u0439.
5027
- ` + lines.join("\n");
5028
- }
5029
- function describeSettledQuestion(question) {
5030
- if (question.status === QuestionStatus.ANSWERED) {
5031
- return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043F\u043E\u043B\u0443\u0447\u0438\u043B \u043E\u0442\u0432\u0435\u0442:
5032
- ${question.answerMd ?? ""}${describeAnswerAttachments(question)}`;
5033
- }
5034
- if (question.status === QuestionStatus.DISMISSED) {
5035
- return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043E\u0442\u043A\u043B\u043E\u043D\u0451\u043D (DISMISSED) \u2014 \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442. \u0420\u0435\u0448\u0438, \u043A\u0430\u043A \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043B\u044C\u0448\u0435, \u0441\u0430\u043C, \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0437\u0430\u043D\u043E\u0432\u043E.`;
5036
- }
5037
- return void 0;
5123
+ // src/socket/backoff.ts
5124
+ function nextReconnectDelayMs(attempt, options = {}) {
5125
+ const baseMs = options.baseMs ?? 1e3;
5126
+ const maxMs = options.maxMs ?? 3e4;
5127
+ const safeAttempt = Math.max(0, Math.trunc(attempt));
5128
+ const exponent = Math.min(safeAttempt, 30);
5129
+ const raw = baseMs * 2 ** exponent;
5130
+ return Math.min(raw, maxMs);
5038
5131
  }
5039
5132
 
5040
- // package.json
5041
- var package_default = {
5042
- name: "@quiel/cli",
5043
- version: "0.5.2",
5044
- description: "CLI \u0438 MCP-\u0441\u0435\u0440\u0432\u0435\u0440 Quiel: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0430\u0433\u0435\u043D\u0442\u0430 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 (Claude Code, Codex, Cursor, OpenCode) \u043A \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0435",
5045
- homepage: "https://quiel.app",
5046
- repository: {
5047
- type: "git",
5048
- url: "git+https://github.com/Quiel-App/quiel-cli.git"
5049
- },
5050
- bugs: {
5051
- url: "https://github.com/Quiel-App/quiel-cli/issues"
5052
- },
5053
- keywords: [
5054
- "quiel",
5055
- "claude-code",
5056
- "codex",
5057
- "cursor",
5058
- "opencode",
5059
- "mcp",
5060
- "mcp-server",
5061
- "ai-agents",
5062
- "task-tracker"
5063
- ],
5064
- license: "Apache-2.0",
5065
- type: "module",
5066
- bin: {
5067
- quiel: "dist/cli.js"
5068
- },
5069
- files: [
5070
- "dist",
5071
- "README.md",
5072
- "LICENSE"
5073
- ],
5074
- engines: {
5075
- node: ">=22"
5076
- },
5077
- publishConfig: {
5078
- access: "public"
5079
- },
5080
- scripts: {
5081
- build: "tsup",
5082
- dev: "tsup --watch",
5083
- lint: "biome check .",
5084
- typecheck: "tsc --noEmit",
5085
- test: "vitest run --passWithNoTests"
5086
- },
5087
- dependencies: {
5088
- "@modelcontextprotocol/sdk": "1.30.0",
5089
- commander: "15.0.0",
5090
- "socket.io-client": "4.8.3",
5091
- zod: "4.5.4"
5092
- },
5093
- devDependencies: {
5094
- "@quiel/config": "workspace:*",
5095
- "@quiel/shared": "workspace:*",
5096
- "@types/node": "20.19.43",
5097
- tsup: "8.5.1",
5098
- typescript: "7.0.2",
5099
- vitest: "5.0.0"
5100
- }
5101
- };
5102
-
5103
- // src/version.ts
5104
- var VERSION = package_default.version;
5133
+ // src/socket/client.ts
5134
+ import { EventEmitter } from "events";
5105
5135
 
5106
- // src/mcp/constants.ts
5107
- var MCP_SERVER_NAME = "quiel";
5108
- var MCP_SERVER_VERSION = VERSION;
5136
+ // src/socket/emit-with-timeout.ts
5137
+ function toError(raw, context) {
5138
+ return raw instanceof Error ? raw : new Error(`${context}: ${String(raw)}`);
5139
+ }
5140
+ async function emitWithAckTimeout(socket, event, payload, timeoutMs) {
5141
+ return await new Promise((resolve4, reject) => {
5142
+ const timer = setTimeout(() => {
5143
+ reject(new Error(`${event}: \u043D\u0435\u0442 ack \u043E\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0437\u0430 ${timeoutMs} \u043C\u0441`));
5144
+ }, timeoutMs);
5145
+ if (typeof timer.unref === "function") timer.unref();
5146
+ socket.emitWithAck(event, payload).then((value) => {
5147
+ clearTimeout(timer);
5148
+ resolve4(value);
5149
+ }).catch((err) => {
5150
+ clearTimeout(timer);
5151
+ reject(toError(err, event));
5152
+ });
5153
+ });
5154
+ }
5109
5155
 
5110
- // src/mcp/get-context.ts
5111
- var ALL_CONTEXT_INCLUDES = [
5112
- "spec",
5113
- "decisions",
5114
- // ВСЕ документы проекта — по умолчанию, а не по особому запросу.
5115
- // `spec`/`decisions` покрывают два типа из пяти, и человек, положивший
5116
- // контекст под типом `DATA` или `PLAN`, вправе ожидать, что агент его
5117
- // увидит. 14 сентября 2026 ровно этого и не случилось.
5118
- "documents",
5119
- "related",
5120
- "repo"
5121
- ];
5122
- var getContextInputShape = {
5123
- taskId: contextGetPayloadSchema.shape.taskId.describe(
5124
- "ID \u0437\u0430\u0434\u0430\u0447\u0438, \u0434\u043B\u044F \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u043D\u0443\u0436\u0435\u043D \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442. \u0415\u0441\u043B\u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0430\u0433\u0435\u043D\u0442\u0430 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C)."
5125
- ),
5126
- include: contextGetPayloadSchema.shape.include.optional().describe(
5127
- "\u0427\u0442\u043E \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C: spec | decisions | related | repo. \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u0438\u043B\u0438 \u043F\u0443\u0441\u0442 \u2014 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0432\u0441\u0451 \u0441\u0440\u0430\u0437\u0443."
5128
- )
5129
- };
5130
- async function getContextHandler(args, runtime) {
5131
- const state = readState(runtime.stateFilePath);
5132
- const taskId = args.taskId ?? state.currentTask?.taskId;
5133
- if (taskId) {
5134
- const lease = runtime.client.isLeaseLost(taskId);
5135
- if (lease.lost) return leaseLostResult(taskId, lease.reason);
5136
- }
5137
- const include = args.include && args.include.length > 0 ? args.include : [...ALL_CONTEXT_INCLUDES];
5138
- const payload = contextGetPayloadSchema.parse({ ...taskId ? { taskId } : {}, include });
5139
- const result = await callServerOrError(
5140
- runtime.client,
5141
- "context:get",
5142
- payload,
5143
- contextPayloadSchema
5144
- );
5145
- if (!result.ok) return errorResult(result.message);
5146
- return textResult(JSON.stringify(result.data, null, 2));
5156
+ // src/socket/trust-cache.ts
5157
+ var UNKNOWN_TRUST_PROFILE_KEY = "unknown";
5158
+ function toTrustCache(rules, now = () => /* @__PURE__ */ new Date()) {
5159
+ return {
5160
+ profileKey: UNKNOWN_TRUST_PROFILE_KEY,
5161
+ updatedAt: now().toISOString(),
5162
+ rules
5163
+ };
5147
5164
  }
5148
5165
 
5149
- // src/mcp/get-task.ts
5150
- var getTaskInputShape = {
5151
- taskId: taskGetPayloadSchema.shape.taskId.describe("ID \u0437\u0430\u0434\u0430\u0447\u0438 (Task.id) \u2014 \u043D\u0435 \u043A\u043B\u044E\u0447 \u0432\u0438\u0434\u0430 REL-42")
5152
- };
5153
- async function getTaskHandler(args, runtime) {
5154
- const lease = runtime.client.isLeaseLost(args.taskId);
5155
- if (lease.lost) return leaseLostResult(args.taskId, lease.reason);
5156
- const payload = taskGetPayloadSchema.parse({ taskId: args.taskId });
5157
- const result = await callServerOrError(runtime.client, "task:get", payload, taskGetAckSchema);
5158
- if (!result.ok) return errorResult(result.message);
5159
- return textResult(JSON.stringify(result.data, null, 2));
5166
+ // src/socket/types.ts
5167
+ function isAckError(raw) {
5168
+ return typeof raw === "object" && raw !== null && "error" in raw && typeof raw.error === "object";
5160
5169
  }
5161
5170
 
5162
- // src/mcp/log.ts
5163
- var logInputShape = {
5164
- message: taskProgressPayloadSchema.shape.note.describe("\u0421\u0442\u0440\u043E\u043A\u0430 \u0432 \u043B\u0435\u043D\u0442\u0443 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438"),
5165
- level: taskProgressPayloadSchema.shape.level.optional().describe("debug | info | warn | error, \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E info")
5166
- };
5167
- async function logHandler(args, runtime) {
5168
- const state = readState(runtime.stateFilePath);
5169
- if (!state.currentTask) {
5170
- return errorResult(
5171
- "\u041D\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0437\u0430\u043F\u0438\u0441\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430 \u0432 \u0435\u0451 \u043B\u0435\u043D\u0442\u0443. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0437\u043E\u0432\u0438 next_task."
5172
- );
5173
- }
5174
- const lease = runtime.client.isLeaseLost(state.currentTask.taskId);
5175
- if (lease.lost) {
5176
- return leaseLostResult(state.currentTask.taskKey ?? state.currentTask.taskId, lease.reason);
5177
- }
5178
- const payload = taskProgressPayloadSchema.parse({
5179
- taskId: state.currentTask.taskId,
5180
- note: args.message,
5181
- ...args.level ? { level: args.level } : {}
5182
- });
5183
- const result = await callServerOrError(
5184
- runtime.client,
5185
- "task:progress",
5186
- payload,
5187
- taskProgressAckSchema
5188
- );
5189
- if (!result.ok) return errorResult(result.message);
5190
- if (!result.data.ok) return errorResult("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u0437\u0430\u043F\u0438\u0441\u044C \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 (ok:false).");
5191
- return textResult("\u0417\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438.");
5192
- }
5193
-
5194
- // src/git/result.ts
5195
- function gitOk(data) {
5196
- return { ok: true, data };
5171
+ // src/socket/client.ts
5172
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
5173
+ var DEFAULT_ACK_TIMEOUT_MS = 1e4;
5174
+ var DEFAULT_BACKOFF_BASE_MS = 1e3;
5175
+ var DEFAULT_BACKOFF_MAX_MS = 3e4;
5176
+ function toError2(raw, context) {
5177
+ if (raw instanceof Error) return raw;
5178
+ return new Error(`${context}: ${String(raw)}`);
5197
5179
  }
5198
- function gitErr(message) {
5199
- return { ok: false, message };
5180
+ function resolveOptions(options) {
5181
+ return {
5182
+ roleKeys: options.roleKeys,
5183
+ cwd: options.cwd,
5184
+ clientVersion: options.clientVersion,
5185
+ agentVersion: options.agentVersion ?? "unknown",
5186
+ agentKind: options.agentKind ?? DEFAULT_AGENT_KIND,
5187
+ os: options.os ?? process.platform,
5188
+ // По умолчанию — файл ИМЕННО этого агента (state/store.ts::getStateFilePath(agentId)),
5189
+ // а не общий `state.json`: не трогаем src/state/**, но обязаны использовать
5190
+ // его правильно (OQ-05 — состояние на агента, не на машину).
5191
+ stateFilePath: options.stateFilePath ?? getStateFilePath(options.agentId),
5192
+ heartbeatIntervalMs: options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
5193
+ ackTimeoutMs: options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS,
5194
+ backoffBaseMs: options.backoffBaseMs ?? DEFAULT_BACKOFF_BASE_MS,
5195
+ backoffMaxMs: options.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS,
5196
+ now: options.now ?? (() => /* @__PURE__ */ new Date())
5197
+ };
5200
5198
  }
5201
-
5202
- // src/git/branch-name.ts
5203
- var FORBIDDEN_CHARS = /[\s~^:?*[\\\x00-\x1f\x7f]/;
5204
- function isValidGitBranchName(name) {
5205
- if (name.length === 0 || name.length > 255) return false;
5206
- if (name.startsWith("-")) return false;
5207
- if (name.startsWith("/") || name.endsWith("/")) return false;
5208
- if (name.endsWith(".")) return false;
5209
- if (name.includes("..")) return false;
5210
- if (name.includes("//")) return false;
5211
- if (name.includes("@{")) return false;
5212
- if (name === "@") return false;
5213
- if (FORBIDDEN_CHARS.test(name)) return false;
5214
- const segments = name.split("/");
5215
- for (const segment of segments) {
5216
- if (segment.length === 0) return false;
5217
- if (segment.startsWith(".")) return false;
5218
- if (segment.endsWith(".lock")) return false;
5199
+ var AgentSocketClient = class extends EventEmitter {
5200
+ socket;
5201
+ opts;
5202
+ agentId;
5203
+ stopped = true;
5204
+ reconnectAttempt = 0;
5205
+ reconnectTimer;
5206
+ heartbeatTimer;
5207
+ lastHelloAck;
5208
+ paused = false;
5209
+ pauseReasonText;
5210
+ leaseLostTasks = /* @__PURE__ */ new Map();
5211
+ constructor(options) {
5212
+ super();
5213
+ this.opts = resolveOptions(options);
5214
+ this.agentId = options.agentId;
5215
+ const factory = options.createSocket ?? createSocket;
5216
+ this.socket = factory(`${trimTrailingSlash2(options.url)}/agent`, {
5217
+ auth: { token: options.token },
5218
+ autoConnect: false,
5219
+ reconnection: false
5220
+ });
5221
+ this.registerSocketListeners();
5219
5222
  }
5220
- return true;
5221
- }
5222
- function requireValidBranchName(name, label) {
5223
- if (!isValidGitBranchName(name)) {
5224
- return gitErr(
5225
- `${label} "${name}" \u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0438\u043C\u044F git-\u0432\u0435\u0442\u043A\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
5226
- );
5223
+ // -- Публичное API -----------------------------------------------------
5224
+ on(event, listener) {
5225
+ return super.on(event, listener);
5227
5226
  }
5228
- return gitOk(name);
5229
- }
5230
-
5231
- // src/git/exec.ts
5232
- import { execFile } from "child_process";
5233
- var DEFAULT_TIMEOUT_MS2 = 12e4;
5234
- var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
5235
- function runGit(args, options) {
5236
- return new Promise((resolvePromise) => {
5237
- execFile(
5238
- "git",
5239
- [...args],
5240
- {
5241
- cwd: options.cwd,
5242
- shell: false,
5243
- timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5244
- maxBuffer: MAX_BUFFER_BYTES,
5245
- env: {
5246
- ...process.env,
5247
- ...options.env,
5248
- // Без человека за терминалом: если git вдруг решит спросить
5249
- // логин/пароль, зависший prompt выглядел бы как неожиданное
5250
- // поведение (требование подзадачи: «любая неожиданность —
5251
- // понятный отказ, а не попробую починить»), а не как честный
5252
- // отказ команды с кодом возврата.
5253
- GIT_TERMINAL_PROMPT: "0"
5254
- }
5255
- },
5256
- (error, stdout, stderr) => {
5257
- if (!error) {
5258
- resolvePromise({ ok: true, code: 0, signal: null, stdout, stderr });
5259
- return;
5260
- }
5261
- const code = typeof error.code === "number" ? error.code : null;
5262
- resolvePromise({
5227
+ once(event, listener) {
5228
+ return super.once(event, listener);
5229
+ }
5230
+ off(event, listener) {
5231
+ return super.off(event, listener);
5232
+ }
5233
+ emit(event, ...args) {
5234
+ return super.emit(event, ...args);
5235
+ }
5236
+ /** Поднимает соединение и heartbeat-таймер. Идемпотентно — повторный вызов на уже запущенном клиенте не делает ничего. */
5237
+ start() {
5238
+ if (!this.stopped) return;
5239
+ this.stopped = false;
5240
+ this.reconnectAttempt = 0;
5241
+ this.startHeartbeatTimer();
5242
+ this.socket.connect();
5243
+ }
5244
+ /** Останавливает heartbeat, отменяет запланированное переподключение, закрывает сокет. После `stop()` клиент больше не переподключается сам. */
5245
+ stop() {
5246
+ this.stopped = true;
5247
+ if (this.reconnectTimer) {
5248
+ clearTimeout(this.reconnectTimer);
5249
+ this.reconnectTimer = void 0;
5250
+ }
5251
+ this.stopHeartbeatTimer();
5252
+ this.socket.disconnect();
5253
+ }
5254
+ get connected() {
5255
+ return this.socket.connected;
5256
+ }
5257
+ get lastHello() {
5258
+ return this.lastHelloAck;
5259
+ }
5260
+ get isPaused() {
5261
+ return this.paused;
5262
+ }
5263
+ get pauseReason() {
5264
+ return this.pauseReasonText;
5265
+ }
5266
+ /** §11.2 ТЗ: «следующий вызов любого тула по этой задаче должен сообщить модели, что работу надо прекратить» — тулы (P2-12) сверяются с этим геттером. */
5267
+ isLeaseLost(taskId) {
5268
+ const reason = this.leaseLostTasks.get(taskId);
5269
+ return reason === void 0 ? { lost: false } : { lost: true, reason };
5270
+ }
5271
+ /**
5272
+ * Общий emit+ack поверх ЭТОГО ЖЕ постоянного сокета — тем же приёмом
5273
+ * (`emitWithAckTimeout`), которым `sendHello`/`sendHeartbeat` уже
5274
+ * пользуются внутри класса, но публично: MCP-тулы (P2-12/13,
5275
+ * `../mcp/**`) шлют через него `task:claim`/`task:start`/`task:progress`/
5276
+ * `task:get`/`task:report`/`task:release`/`context:get` (§11.1 ТЗ) — то
5277
+ * есть ВСЁ, что не `agent:hello`/`agent:heartbeat`/`agent:mode` (у
5278
+ * первых двух своя логика согласования состояния в этом классе, третий
5279
+ * — короткоживущий отдельный сокет, см. `send-agent-mode.ts`). Нарочно
5280
+ * НЕ отдельное соединение: `task:claim` — блокирующий долгий long-poll,
5281
+ * и держать его на том же сокете, где уже идёт heartbeat/presence этого
5282
+ * агента, а не поднимать вторую параллельную сессию — единственный
5283
+ * вариант, не удваивающий presence на сервере.
5284
+ */
5285
+ callServer(event, payload, timeoutMs = this.opts.ackTimeoutMs) {
5286
+ return emitWithAckTimeout(this.socket, event, payload, timeoutMs);
5287
+ }
5288
+ /**
5289
+ * Ждёт первого успешного `agent:hello` не дольше `timeoutMs` (по
5290
+ * умолчанию 5 с — как раньше HTTP-пинг в `network.ts`). Таймаут НЕ
5291
+ * останавливает клиент: переподключение по backoff продолжается в фоне,
5292
+ * это только ограничение по времени для вызывающей стороны (`connect.ts`).
5293
+ */
5294
+ waitForFirstConnection(timeoutMs = 5e3) {
5295
+ return new Promise((resolve4) => {
5296
+ let settled = false;
5297
+ const onConnected = (ack) => {
5298
+ if (settled) return;
5299
+ settled = true;
5300
+ clearTimeout(timer);
5301
+ this.off("connected", onConnected);
5302
+ resolve4({ ok: true, ack });
5303
+ };
5304
+ const onAuthRejected = (message) => {
5305
+ if (settled) return;
5306
+ settled = true;
5307
+ clearTimeout(timer);
5308
+ this.off("connected", onConnected);
5309
+ this.off("auth-rejected", onAuthRejected);
5310
+ resolve4({ ok: false, message });
5311
+ };
5312
+ const timer = setTimeout(() => {
5313
+ if (settled) return;
5314
+ settled = true;
5315
+ this.off("connected", onConnected);
5316
+ this.off("auth-rejected", onAuthRejected);
5317
+ resolve4({
5263
5318
  ok: false,
5264
- code,
5265
- signal: error.signal ?? null,
5266
- stdout,
5267
- // Если git ничего не написал в stderr (например, сам процесс не
5268
- // запустился — ENOENT), берём текст ошибки Node, чтобы вызывающий
5269
- // код не остался с пустым сообщением.
5270
- stderr: stderr.length > 0 ? stderr : error.message
5319
+ message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 agent:hello \u0437\u0430 ${timeoutMs} \u043C\u0441 \u2014 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430. \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0432 \u0444\u043E\u043D\u0435 (backoff \xA711.4 \u0422\u0417).`
5271
5320
  });
5272
- }
5273
- );
5274
- });
5275
- }
5276
- function describeGitFailure(commandLabel, result) {
5277
- const stderr = result.stderr.trim();
5278
- const codeText = result.code === null ? "" : ` (\u043A\u043E\u0434 ${result.code})`;
5279
- return stderr.length > 0 ? `${commandLabel}${codeText}: ${stderr}` : `${commandLabel} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439${codeText}`;
5280
- }
5281
-
5282
- // src/git/repo.ts
5283
- async function localBranchExists(repoRoot, branch) {
5284
- const result = await runGit(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
5285
- cwd: repoRoot
5286
- });
5287
- return result.ok;
5288
- }
5289
- async function getHeadSha(worktreeDir) {
5290
- const result = await runGit(["rev-parse", "HEAD"], { cwd: worktreeDir });
5291
- if (!result.ok) {
5292
- return gitErr(describeGitFailure("git rev-parse HEAD", result));
5321
+ }, timeoutMs);
5322
+ if (typeof timer.unref === "function") timer.unref();
5323
+ this.on("connected", onConnected);
5324
+ this.on("auth-rejected", onAuthRejected);
5325
+ });
5326
+ }
5327
+ // -- Подключение --------------------------------------------------------
5328
+ registerSocketListeners() {
5329
+ this.socket.on("connect", () => this.handleConnect());
5330
+ this.socket.on("connect_error", (err) => this.handleConnectError(err));
5331
+ this.socket.on("disconnect", (reason) => this.handleDisconnect(String(reason)));
5332
+ this.socket.on("lease:lost", (raw) => this.handleLeaseLost(raw));
5333
+ this.socket.on("mode:set", (raw) => this.handleModeSet(raw));
5334
+ this.socket.on("agent:pause", (raw) => this.handleAgentPause(raw));
5335
+ this.socket.on("trust:updated", (raw) => this.handleTrustUpdated(raw));
5336
+ this.socket.on("task:updated", (raw) => this.handleTaskUpdated(raw));
5337
+ this.socket.on("question:answered", (raw) => this.handleQuestionAnswered(raw));
5338
+ this.socket.on("approval:decided", (raw) => this.handleApprovalDecided(raw));
5339
+ }
5340
+ handleConnect() {
5341
+ if (this.stopped) return;
5342
+ this.reconnectAttempt = 0;
5343
+ void this.sendHello();
5344
+ }
5345
+ handleConnectError(err) {
5346
+ if (isAuthRejection(err)) {
5347
+ this.stop();
5348
+ this.emit("auth-rejected", describeAuthRejection(this.agentId));
5349
+ return;
5350
+ }
5351
+ this.emit("client-error", toError2(err, "connect_error"));
5352
+ this.scheduleReconnect();
5353
+ }
5354
+ handleDisconnect(reason) {
5355
+ this.emit("disconnected", reason);
5356
+ if (this.stopped) return;
5357
+ this.scheduleReconnect();
5358
+ }
5359
+ scheduleReconnect() {
5360
+ if (this.stopped || this.reconnectTimer) return;
5361
+ const delayMs = nextReconnectDelayMs(this.reconnectAttempt, {
5362
+ baseMs: this.opts.backoffBaseMs,
5363
+ maxMs: this.opts.backoffMaxMs
5364
+ });
5365
+ this.reconnectAttempt += 1;
5366
+ this.emit("reconnecting", { attempt: this.reconnectAttempt, delayMs });
5367
+ this.reconnectTimer = setTimeout(() => {
5368
+ this.reconnectTimer = void 0;
5369
+ if (this.stopped) return;
5370
+ this.socket.connect();
5371
+ }, delayMs);
5372
+ }
5373
+ // -- agent:hello ---------------------------------------------------------
5374
+ async sendHello() {
5375
+ const state = readState(this.opts.stateFilePath);
5376
+ const payload = agentHelloPayloadSchema.parse({
5377
+ clientVersion: this.opts.clientVersion,
5378
+ cwd: this.opts.cwd,
5379
+ os: this.opts.os,
5380
+ agentKind: this.opts.agentKind,
5381
+ agentVersion: this.opts.agentVersion,
5382
+ // `claudeVersion` шлётся ТОЛЬКО для Claude Code — и только чтобы
5383
+ // клиент работал против ещё не обновлённого сервера, который знает
5384
+ // одно это поле. Для Codex поле с таким именем было бы неправдой.
5385
+ ...this.opts.agentKind === AgentKind.CLAUDE_CODE ? { claudeVersion: this.opts.agentVersion } : {},
5386
+ roleKeys: [...this.opts.roleKeys],
5387
+ ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {}
5388
+ });
5389
+ let raw;
5390
+ try {
5391
+ raw = await emitWithAckTimeout(this.socket, "agent:hello", payload, this.opts.ackTimeoutMs);
5392
+ } catch (err) {
5393
+ this.emit("client-error", toError2(err, "agent:hello"));
5394
+ return;
5395
+ }
5396
+ if (this.stopped) return;
5397
+ if (isAckError(raw)) {
5398
+ this.emit("client-error", new Error(`agent:hello: ${raw.error.code}: ${raw.error.message}`));
5399
+ return;
5400
+ }
5401
+ const parsed = agentHelloAckSchema.safeParse(raw);
5402
+ if (!parsed.success) {
5403
+ this.emit(
5404
+ "client-error",
5405
+ new Error(`agent:hello: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
5406
+ );
5407
+ return;
5408
+ }
5409
+ const ack = parsed.data;
5410
+ this.lastHelloAck = ack;
5411
+ this.paused = false;
5412
+ this.pauseReasonText = void 0;
5413
+ this.applyHelloAck(ack);
5414
+ this.emit("connected", ack);
5415
+ }
5416
+ applyHelloAck(ack) {
5417
+ const state = readState(this.opts.stateFilePath);
5418
+ const patch = {
5419
+ // OQ-06: сервер — источник правды, локальный режим перекрывается безусловно.
5420
+ mode: ack.mode,
5421
+ trust: toTrustCache(ack.trust, this.opts.now)
5422
+ };
5423
+ if (!ack.currentTask || ack.currentTask.id !== state.currentTask?.taskId) {
5424
+ patch.currentTask = null;
5425
+ }
5426
+ updateState(patch, this.opts.stateFilePath);
5427
+ }
5428
+ // -- agent:heartbeat -------------------------------------------------------
5429
+ startHeartbeatTimer() {
5430
+ if (this.heartbeatTimer) return;
5431
+ this.heartbeatTimer = setInterval(() => {
5432
+ void this.sendHeartbeat();
5433
+ }, this.opts.heartbeatIntervalMs);
5434
+ }
5435
+ stopHeartbeatTimer() {
5436
+ if (!this.heartbeatTimer) return;
5437
+ clearInterval(this.heartbeatTimer);
5438
+ this.heartbeatTimer = void 0;
5439
+ }
5440
+ async sendHeartbeat() {
5441
+ if (!this.socket.connected) return;
5442
+ const state = readState(this.opts.stateFilePath);
5443
+ const payload = agentHeartbeatPayloadSchema.parse({
5444
+ ...state.currentTask?.taskId ? { currentTaskId: state.currentTask.taskId } : {},
5445
+ ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {},
5446
+ status: state.currentTask ? AgentStatus.BUSY : AgentStatus.IDLE
5447
+ });
5448
+ let raw;
5449
+ try {
5450
+ raw = await emitWithAckTimeout(
5451
+ this.socket,
5452
+ "agent:heartbeat",
5453
+ payload,
5454
+ this.opts.ackTimeoutMs
5455
+ );
5456
+ } catch (err) {
5457
+ this.emit("client-error", toError2(err, "agent:heartbeat"));
5458
+ return;
5459
+ }
5460
+ if (isAckError(raw)) {
5461
+ this.emit(
5462
+ "client-error",
5463
+ new Error(`agent:heartbeat: ${raw.error.code}: ${raw.error.message}`)
5464
+ );
5465
+ return;
5466
+ }
5467
+ const parsed = agentHeartbeatAckSchema.safeParse(raw);
5468
+ if (!parsed.success) {
5469
+ this.emit(
5470
+ "client-error",
5471
+ new Error(`agent:heartbeat: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
5472
+ );
5473
+ return;
5474
+ }
5475
+ this.emit("heartbeat", parsed.data);
5476
+ }
5477
+ // -- Пуш-события сервера (§11.2 ТЗ) --------------------------------------
5478
+ handleLeaseLost(raw) {
5479
+ const parsed = leaseLostPayloadSchema.safeParse(raw);
5480
+ if (!parsed.success) {
5481
+ this.emit(
5482
+ "client-error",
5483
+ new Error(`lease:lost: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5484
+ );
5485
+ return;
5486
+ }
5487
+ const { taskId, reason } = parsed.data;
5488
+ this.leaseLostTasks.set(taskId, reason);
5489
+ const state = readState(this.opts.stateFilePath);
5490
+ if (state.currentTask?.taskId === taskId) {
5491
+ updateState({ currentTask: null }, this.opts.stateFilePath);
5492
+ }
5493
+ this.emit("lease:lost", parsed.data);
5494
+ }
5495
+ handleModeSet(raw) {
5496
+ const parsed = modeSetPayloadSchema.safeParse(raw);
5497
+ if (!parsed.success) {
5498
+ this.emit("client-error", new Error(`mode:set: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`));
5499
+ return;
5500
+ }
5501
+ updateState({ mode: parsed.data.mode }, this.opts.stateFilePath);
5502
+ this.emit("mode:set", parsed.data);
5503
+ }
5504
+ handleAgentPause(raw) {
5505
+ const parsed = agentPausePayloadSchema.safeParse(raw);
5506
+ if (!parsed.success) {
5507
+ this.emit(
5508
+ "client-error",
5509
+ new Error(`agent:pause: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5510
+ );
5511
+ return;
5512
+ }
5513
+ this.paused = true;
5514
+ this.pauseReasonText = parsed.data.reason;
5515
+ this.emit("pause", parsed.data);
5516
+ }
5517
+ handleTrustUpdated(raw) {
5518
+ const parsed = trustUpdatedPayloadSchema.safeParse(raw);
5519
+ if (!parsed.success) {
5520
+ this.emit(
5521
+ "client-error",
5522
+ new Error(`trust:updated: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5523
+ );
5524
+ return;
5525
+ }
5526
+ updateState({ trust: toTrustCache(parsed.data, this.opts.now) }, this.opts.stateFilePath);
5527
+ this.emit("trust:updated", parsed.data);
5528
+ }
5529
+ handleTaskUpdated(raw) {
5530
+ const parsed = taskUpdatedAgentPayloadSchema.safeParse(raw);
5531
+ if (parsed.success) this.emit("task:updated", parsed.data);
5293
5532
  }
5294
- return gitOk(result.stdout.trim());
5295
- }
5296
- async function currentBranch(worktreeDir) {
5297
- const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreeDir });
5298
- if (!result.ok) {
5299
- return gitErr(describeGitFailure("git rev-parse --abbrev-ref HEAD", result));
5533
+ handleQuestionAnswered(raw) {
5534
+ const parsed = questionAnsweredPayloadSchema.safeParse(raw);
5535
+ if (parsed.success) this.emit("question:answered", parsed.data);
5300
5536
  }
5301
- return gitOk(result.stdout.trim());
5302
- }
5303
-
5304
- // src/git/status.ts
5305
- var CONFLICT_CODES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
5306
- async function getWorktreeStatus(worktreeDir) {
5307
- const result = await runGit(["status", "--porcelain=v1", "--untracked-files=all"], {
5308
- cwd: worktreeDir
5309
- });
5310
- if (!result.ok) {
5311
- return gitErr(describeGitFailure("git status", result));
5537
+ /**
5538
+ * §14.4 ТЗ, п.4-5 — асинхронный путь (в дополнение к синхронному в
5539
+ * `mcp/request-approval.ts::settleApproval`, который срабатывает, когда
5540
+ * решение приходит в рамках ОДНОГО вызова `request_approval`/
5541
+ * `wait_approval`). Этот пуш ловит случай, когда решение пришло уже
5542
+ * ПОСЛЕ того, как собственный long-poll тула истёк (вернул `PENDING`),
5543
+ * а модель ещё не успела/не стала звать `wait_approval` снова — на
5544
+ * постоянном соединении (`quiel mcp`, единственный держатель ЭТОГО
5545
+ * класса) `approval:decided` долетит и без явного повторного вызова.
5546
+ *
5547
+ * `approvalDecidedPayloadSchema` несёт только `approvalId`+`status`+
5548
+ * `oneTimeToken?` — НЕ `command`/`cwd` (провод их не отдаёт, см.
5549
+ * `state/schema.ts::PendingApprovalSchema`), поэтому command+cwd берутся
5550
+ * из `state.pendingApprovals[approvalId]`, куда их кладёт
5551
+ * `request_approval` сразу после создания запроса. Если записи там нет
5552
+ * (approval создан не этим клиентом/до перезапуска процесса, либо уже
5553
+ * обработан) — писать пока нечего, `pendingApprovals` и так пуст.
5554
+ */
5555
+ handleApprovalDecided(raw) {
5556
+ const parsed = approvalDecidedPayloadSchema.safeParse(raw);
5557
+ if (!parsed.success) {
5558
+ this.emit(
5559
+ "client-error",
5560
+ new Error(`approval:decided: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5561
+ );
5562
+ return;
5563
+ }
5564
+ this.applyApprovalDecided(parsed.data);
5565
+ this.emit("approval:decided", parsed.data);
5312
5566
  }
5313
- const changedPaths = [];
5314
- const conflictedPaths = [];
5315
- for (const line of result.stdout.split("\n")) {
5316
- if (line.length === 0) continue;
5317
- const code = line.slice(0, 2);
5318
- const rawPath = line.slice(3);
5319
- const arrowIndex = rawPath.indexOf(" -> ");
5320
- const path = arrowIndex >= 0 ? rawPath.slice(arrowIndex + 4) : rawPath;
5321
- changedPaths.push(path);
5322
- if (CONFLICT_CODES.has(code)) conflictedPaths.push(path);
5567
+ applyApprovalDecided(payload) {
5568
+ const state = readState(this.opts.stateFilePath);
5569
+ const pending = state.pendingApprovals[payload.approvalId];
5570
+ if (!pending) return;
5571
+ if (payload.status === ApprovalStatus.APPROVED && payload.oneTimeToken) {
5572
+ const approvalTtlMin = state.trust?.rules.approvalTtlMin ?? 60;
5573
+ const expiresAt = new Date(this.opts.now().getTime() + approvalTtlMin * 6e4).toISOString();
5574
+ updateState(
5575
+ {
5576
+ ...storeApprovedCommand(
5577
+ state,
5578
+ { command: pending.command, cwd: pending.cwd, token: payload.oneTimeToken, expiresAt },
5579
+ this.opts.now
5580
+ ),
5581
+ ...clearPendingApproval(state, payload.approvalId)
5582
+ },
5583
+ this.opts.stateFilePath
5584
+ );
5585
+ return;
5586
+ }
5587
+ updateState(clearPendingApproval(state, payload.approvalId), this.opts.stateFilePath);
5323
5588
  }
5324
- return gitOk({ clean: changedPaths.length === 0, changedPaths, conflictedPaths });
5589
+ };
5590
+ function trimTrailingSlash2(url) {
5591
+ return url.endsWith("/") ? url.slice(0, -1) : url;
5325
5592
  }
5326
5593
 
5327
- // src/git/commit.ts
5328
- function buildCommitMessage(params) {
5329
- const subject = `${params.commitPrefix} ${params.title}`.trim();
5330
- return `${subject}
5331
-
5332
- ${buildTaskTrailer(params.taskKey)}
5333
- `;
5594
+ // src/socket/send-agent-mode.ts
5595
+ var DEFAULT_TIMEOUT_MS2 = 8e3;
5596
+ function trimTrailingSlash3(url) {
5597
+ return url.endsWith("/") ? url.slice(0, -1) : url;
5334
5598
  }
5335
- async function commitAll(params) {
5336
- const addResult = await runGit(["add", "-A"], { cwd: params.worktreeDir });
5337
- if (!addResult.ok) {
5338
- return gitErr(describeGitFailure("git add -A", addResult));
5339
- }
5340
- const status = await getWorktreeStatus(params.worktreeDir);
5341
- if (!status.ok) return status;
5342
- if (status.data.clean) {
5343
- return gitOk({ committed: false, sha: null });
5344
- }
5345
- const message = buildCommitMessage(params);
5346
- const commitResult = await runGit(["commit", "-m", message], { cwd: params.worktreeDir });
5347
- if (!commitResult.ok) {
5348
- return gitErr(describeGitFailure("git commit", commitResult));
5599
+ async function sendAgentMode(options) {
5600
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
5601
+ const factory = options.createSocket ?? createSocket;
5602
+ const socket = factory(`${trimTrailingSlash3(options.url)}/agent`, {
5603
+ auth: { token: options.token },
5604
+ autoConnect: false,
5605
+ reconnection: false
5606
+ });
5607
+ try {
5608
+ await connectOnce(socket, timeoutMs);
5609
+ const payload = agentModePayloadSchema.parse({ mode: options.mode });
5610
+ const raw = await emitWithAckTimeout(socket, "agent:mode", payload, timeoutMs);
5611
+ if (isAckError(raw)) {
5612
+ return { ok: false, message: `${raw.error.code}: ${raw.error.message}` };
5613
+ }
5614
+ const parsed = agentModeAckSchema.safeParse(raw);
5615
+ if (!parsed.success) {
5616
+ return { ok: false, message: `\u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}` };
5617
+ }
5618
+ if (!parsed.data.ok) {
5619
+ return { ok: false, message: "\u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B agent:mode (ok:false)" };
5620
+ }
5621
+ return { ok: true, mode: parsed.data.mode ?? options.mode };
5622
+ } catch (err) {
5623
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
5624
+ } finally {
5625
+ socket.disconnect();
5349
5626
  }
5350
- const sha = await getHeadSha(params.worktreeDir);
5351
- return gitOk({ committed: true, sha: sha.ok ? sha.data : null });
5352
5627
  }
5353
5628
 
5354
- // src/git/worktree.ts
5355
- import { createHash as createHash2 } from "crypto";
5356
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, realpathSync } from "fs";
5357
- import { dirname as dirname4, isAbsolute, join as join9, relative, resolve as resolve2 } from "path";
5358
- var WORKTREES_RELATIVE_ROOT = join9(".claude", "worktrees");
5359
- function getDefaultWorktreesRoot(repoRoot) {
5360
- return join9(repoRoot, WORKTREES_RELATIVE_ROOT);
5361
- }
5362
- function resolveWorktreeDir(worktreesRoot, repoRoot, branchName) {
5363
- if (isInsideRepo(worktreesRoot, repoRoot)) return join9(worktreesRoot, branchName);
5364
- const repoHash = createHash2("sha1").update(resolve2(repoRoot)).digest("hex").slice(0, 12);
5365
- return join9(worktreesRoot, repoHash, branchName);
5366
- }
5367
- function isInsideRepo(candidate, repoRoot) {
5368
- const rel = relative(resolve2(repoRoot), resolve2(candidate));
5369
- return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
5629
+ // src/mcp/helpers.ts
5630
+ function textResult(text) {
5631
+ return { content: [{ type: "text", text }] };
5370
5632
  }
5371
- function canonicalPath(path) {
5372
- try {
5373
- return realpathSync(path);
5374
- } catch {
5375
- return resolve2(path);
5376
- }
5633
+ function errorResult(text) {
5634
+ return { content: [{ type: "text", text }], isError: true };
5377
5635
  }
5378
- async function listWorktrees(repoRoot) {
5379
- const result = await runGit(["worktree", "list", "--porcelain"], { cwd: repoRoot });
5380
- if (!result.ok) {
5381
- return gitErr(describeGitFailure("git worktree list", result));
5382
- }
5383
- const entries = [];
5384
- let path;
5385
- let branch = null;
5386
- const flush = () => {
5387
- if (path) entries.push({ path, branch });
5388
- path = void 0;
5389
- branch = null;
5390
- };
5391
- for (const line of result.stdout.split("\n")) {
5392
- if (line.length === 0) {
5393
- flush();
5394
- continue;
5395
- }
5396
- if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
5397
- else if (line.startsWith("branch refs/heads/")) branch = line.slice("branch refs/heads/".length);
5398
- }
5399
- flush();
5400
- return gitOk(entries);
5636
+ function leaseLostResult(taskLabel2, reason) {
5637
+ return errorResult(
5638
+ `\u0410\u0440\u0435\u043D\u0434\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 ${taskLabel2} \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430: ${reason}. \u041F\u0440\u0435\u043A\u0440\u0430\u0442\u0438 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430\u0434 \u043D\u0435\u0439 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E \u2014 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0441\u0434\u0430\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430, \u0432\u044B\u0437\u043E\u0432\u0438 next_task \u0437\u0430 \u043D\u043E\u0432\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435\u0439.`
5639
+ );
5401
5640
  }
5402
- async function ensureWorktree(params) {
5403
- const remote = params.remote ?? "origin";
5404
- const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
5405
- if (!branchCheck.ok) return branchCheck;
5406
- const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
5407
- if (!defaultBranchCheck.ok) return defaultBranchCheck;
5408
- const worktreesRoot = params.worktreesRoot ?? getDefaultWorktreesRoot(params.repoRoot);
5409
- const worktreeDir = resolveWorktreeDir(worktreesRoot, params.repoRoot, params.branchName);
5410
- const list = await listWorktrees(params.repoRoot);
5411
- if (!list.ok) return list;
5412
- const existing = list.data.find((wt) => canonicalPath(wt.path) === canonicalPath(worktreeDir));
5413
- if (existing) {
5414
- if (existing.branch === params.branchName) {
5415
- return gitOk({ worktreeDir, reused: true });
5416
- }
5417
- return gitErr(
5418
- `\u041F\u043E \u043F\u0443\u0442\u0438 ${worktreeDir} \u0443\u0436\u0435 \u0435\u0441\u0442\u044C git worktree \u043D\u0430 \u0432\u0435\u0442\u043A\u0435 "${existing.branch ?? "(\u043E\u0442\u0441\u043E\u0435\u0434\u0438\u043D\u0451\u043D\u043D\u044B\u0439 HEAD)"}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}". \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0440\u0443\u043A\u0430\u043C\u0438: "git -C ${params.repoRoot} worktree remove ${worktreeDir}" (\u0434\u043E\u0431\u0430\u0432\u044C "--force", \u0435\u0441\u043B\u0438 \u0442\u0430\u043C \u043D\u0435\u0442 \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u0430\u0436\u043D\u043E\u0433\u043E), \u0437\u0430\u0442\u0435\u043C \u043F\u043E\u0432\u0442\u043E\u0440\u0438.`
5419
- );
5420
- }
5421
- if (existsSync9(worktreeDir)) {
5422
- return gitErr(
5423
- `\u041F\u0443\u0442\u044C ${worktreeDir} \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E git \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0435\u0433\u043E \u0441\u0432\u043E\u0435\u0439 worktree. \u0423\u0431\u0435\u0440\u0438 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u2014 \u0442\u0440\u043E\u0433\u0430\u0442\u044C \u0447\u0443\u0436\u043E\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E.`
5424
- );
5425
- }
5426
- await runGit(["worktree", "prune"], { cwd: params.repoRoot });
5427
- const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
5428
- cwd: params.repoRoot
5429
- });
5430
- if (!fetchResult.ok) {
5431
- return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
5432
- }
5433
- mkdirSync6(dirname4(worktreeDir), { recursive: true });
5434
- const branchExists = await localBranchExists(params.repoRoot, params.branchName);
5435
- const addArgs = branchExists ? ["worktree", "add", worktreeDir, params.branchName] : ["worktree", "add", "-b", params.branchName, worktreeDir, `${remote}/${params.defaultBranch}`];
5436
- const addResult = await runGit(addArgs, { cwd: params.repoRoot });
5437
- if (!addResult.ok) {
5438
- return gitErr(describeGitFailure("git worktree add", addResult));
5439
- }
5440
- return gitOk({ worktreeDir, reused: false });
5641
+ function describeZodIssues(error) {
5642
+ return error.issues.map(
5643
+ (issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message
5644
+ ).join("; ");
5441
5645
  }
5442
- async function removeWorktree(repoRoot, worktreeDir) {
5443
- const result = await runGit(["worktree", "remove", worktreeDir], { cwd: repoRoot });
5444
- if (!result.ok) {
5445
- return gitErr(describeGitFailure("git worktree remove", result));
5646
+ function parseArgsOrError(schema, raw) {
5647
+ const parsed = schema.safeParse(raw);
5648
+ if (!parsed.success) {
5649
+ return { ok: false, message: `\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B: ${describeZodIssues(parsed.error)}` };
5446
5650
  }
5447
- return gitOk(void 0);
5651
+ return { ok: true, data: parsed.data };
5448
5652
  }
5449
-
5450
- // src/git/submit.ts
5451
- async function rebaseOntoDefault(params) {
5452
- const remote = params.remote ?? "origin";
5453
- const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
5454
- if (!defaultBranchCheck.ok) return defaultBranchCheck;
5455
- const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
5456
- cwd: params.worktreeDir
5457
- });
5458
- if (!fetchResult.ok) {
5459
- return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
5653
+ async function callServerOrError(client, event, payload, schema, timeoutMs) {
5654
+ let raw;
5655
+ try {
5656
+ raw = await client.callServer(event, payload, timeoutMs);
5657
+ } catch (err) {
5658
+ return {
5659
+ ok: false,
5660
+ message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B \u043D\u0430 ${event} (${err instanceof Error ? err.message : String(err)}). \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F \u0432 \u0444\u043E\u043D\u0435 \u2014 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439 \u0435\u0449\u0451 \u0440\u0430\u0437 \u0447\u0443\u0442\u044C \u043F\u043E\u0437\u0436\u0435.`
5661
+ };
5460
5662
  }
5461
- const rebaseResult = await runGit(["rebase", `${remote}/${params.defaultBranch}`], {
5462
- cwd: params.worktreeDir
5463
- });
5464
- if (rebaseResult.ok) {
5465
- return gitOk({ outcome: "rebased", conflictedFiles: [] });
5663
+ if (isAckError(raw)) {
5664
+ return {
5665
+ ok: false,
5666
+ message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u0430\u0437\u0430\u043B\u0430 \u043D\u0430 ${event}: ${raw.error.code}: ${raw.error.message}`
5667
+ };
5466
5668
  }
5467
- const status = await getWorktreeStatus(params.worktreeDir);
5468
- if (status.ok && status.data.conflictedPaths.length > 0) {
5469
- const conflictedFiles = status.data.conflictedPaths;
5470
- const abortResult = await runGit(["rebase", "--abort"], { cwd: params.worktreeDir });
5471
- if (!abortResult.ok) {
5472
- return gitErr(
5473
- `\u041A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043F\u0440\u0438 rebase (${conflictedFiles.join(", ")}), \u0438 "git rebase --abort" \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u043B: ${describeGitFailure("git rebase --abort", abortResult)}. Worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u043D\u043E \u0440\u0443\u0447\u043D\u043E\u0435 \u0432\u043C\u0435\u0448\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E.`
5474
- );
5475
- }
5476
- return gitOk({ outcome: "conflict", conflictedFiles });
5669
+ const parsed = schema.safeParse(raw);
5670
+ if (!parsed.success) {
5671
+ return {
5672
+ ok: false,
5673
+ message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043F\u0440\u0438\u0441\u043B\u0430\u043B\u0430 \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 \u043D\u0430 ${event}: ${parsed.error.message}`
5674
+ };
5477
5675
  }
5478
- return gitErr(describeGitFailure("git rebase", rebaseResult));
5676
+ return { ok: true, data: parsed.data };
5479
5677
  }
5480
- async function assertReadyToPush(params) {
5481
- const branch = await currentBranch(params.worktreeDir);
5482
- if (!branch.ok) return branch;
5483
- if (branch.data !== params.branchName) {
5484
- return gitErr(
5485
- `\u041E\u0442\u043A\u0430\u0437: worktree \u0441\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u0432\u0435\u0442\u043A\u0443 "${branch.data}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}" \u2014 \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E "\u0443\u0448\u043B\u043E \u043D\u0435 \u0442\u0443\u0434\u0430", push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
5486
- );
5678
+
5679
+ // src/mcp/ask.ts
5680
+ var askHumanInputShape = {
5681
+ textMd: questionAskPayloadSchema.shape.textMd.describe(
5682
+ "\u0422\u0435\u043A\u0441\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, Markdown. \u041F\u0438\u0448\u0438 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0431\u0435\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430 \u0441 \u043C\u043E\u0434\u0435\u043B\u044C\u044E."
5683
+ ),
5684
+ targetRole: questionAskPayloadSchema.shape.targetRole.describe(
5685
+ '\u0420\u043E\u043B\u044C \u0430\u0434\u0440\u0435\u0441\u0430\u0442\u0430 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, "owner") \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u0443\u0432\u0438\u0434\u044F\u0442 \u0432\u0441\u0435 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441 \u044D\u0442\u043E\u0439 \u0440\u043E\u043B\u044C\u044E, \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u043F\u0435\u0440\u0432\u044B\u0439. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E.'
5686
+ ),
5687
+ targetMemberId: questionAskPayloadSchema.shape.targetMemberId.describe(
5688
+ "ID \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0430\u0434\u0440\u0435\u0441\u0430\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E."
5689
+ ),
5690
+ wantsFiles: questionAskPayloadSchema.shape.wantsFiles.describe(
5691
+ "\u041F\u043E\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u043A \u043E\u0442\u0432\u0435\u0442\u0443 (\u0441\u043A\u0440\u0438\u043D\u0448\u043E\u0442, \u043B\u043E\u0433, \u0432\u044B\u0433\u0440\u0443\u0437\u043A\u0443). \u0424\u043E\u0440\u043C\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443 \u0441 \u0432\u044B\u0431\u043E\u0440\u043E\u043C \u0444\u0430\u0439\u043B\u0430. \u042D\u0442\u043E \u041F\u0420\u041E\u0421\u042C\u0411\u0410, \u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435: \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0432\u043F\u0440\u0430\u0432\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u0431\u0435\u0437 \u0444\u0430\u0439\u043B\u043E\u0432, \u0438 \u0442\u043E\u0433\u0434\u0430 `attachments` \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u0431\u0443\u0434\u0435\u0442 \u043F\u0443\u0441\u0442\u044B\u043C."
5692
+ ),
5693
+ options: questionAskPayloadSchema.shape.options.describe(
5694
+ "\u0412\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u043E\u0442\u0432\u0435\u0442\u0430 (\u043F\u043E\u043A\u0430\u0436\u0443\u0442\u0441\u044F \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438 \u0432 UI/Telegram). \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0439 \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C."
5695
+ ),
5696
+ blocking: questionAskPayloadSchema.shape.blocking.describe(
5697
+ 'true \u2014 \u0442\u0432\u043E\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 \u0441\u0442\u0430\u0442\u0443\u0441 WAITING_HUMAN, \u0438 \u044D\u0442\u043E\u0442 \u0432\u044B\u0437\u043E\u0432 \u0441\u0430\u043C \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442 \u0434\u043E 600 \u0441; \u0435\u0441\u043B\u0438 \u0437\u0430 \u044D\u0442\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435\u0442, \u0432\u044B\u0437\u043E\u0432 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0441 \u043F\u043E\u043C\u0435\u0442\u043A\u043E\u0439 "pending" \u2014 \u0442\u043E\u0433\u0434\u0430 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430, \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430. false \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u043F\u0440\u043E\u0441\u0442\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F, \u0431\u0435\u0437 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0438 \u0431\u0435\u0437 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 (\u0434\u043B\u044F \u043D\u0435 \u0441\u0440\u043E\u0447\u043D\u044B\u0445 \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0439).'
5698
+ )
5699
+ };
5700
+ var BLOCKING_WAIT_SEC = 600;
5701
+ var ASK_ACK_BUFFER_MS = 15e3;
5702
+ async function askHumanHandler(args, runtime) {
5703
+ const state = readState(runtime.stateFilePath);
5704
+ const taskId = state.currentTask?.taskId;
5705
+ const waitSec = args.blocking ? BLOCKING_WAIT_SEC : 0;
5706
+ const parsedArgs = parseArgsOrError(questionAskPayloadSchema, {
5707
+ ...taskId ? { taskId } : {},
5708
+ textMd: args.textMd,
5709
+ ...args.targetRole ? { targetRole: args.targetRole } : {},
5710
+ ...args.targetMemberId ? { targetMemberId: args.targetMemberId } : {},
5711
+ ...args.options && args.options.length > 0 ? { options: args.options } : {},
5712
+ ...args.wantsFiles ? { wantsFiles: true } : {},
5713
+ blocking: args.blocking,
5714
+ waitSec
5715
+ });
5716
+ if (!parsedArgs.ok) return errorResult(parsedArgs.message);
5717
+ if (taskId) {
5718
+ const lease = runtime.client.isLeaseLost(taskId);
5719
+ if (lease.lost) return leaseLostResult(state.currentTask?.taskKey ?? taskId, lease.reason);
5487
5720
  }
5488
- const status = await getWorktreeStatus(params.worktreeDir);
5489
- if (!status.ok) return status;
5490
- if (!status.data.clean) {
5491
- return gitErr(
5492
- `\u041E\u0442\u043A\u0430\u0437: \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E \u0433\u0440\u044F\u0437\u043D\u043E\u0435, \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u043F\u0443\u0442\u0438: ${status.data.changedPaths.join(", ")}. Push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
5721
+ const result = await callServerOrError(
5722
+ runtime.client,
5723
+ "question:ask",
5724
+ parsedArgs.data,
5725
+ questionAskAckSchema,
5726
+ waitSec * 1e3 + ASK_ACK_BUFFER_MS
5727
+ );
5728
+ if (!result.ok) return errorResult(result.message);
5729
+ const settled = describeSettledQuestion(result.data.question);
5730
+ if (settled) return textResult(settled);
5731
+ if (args.blocking) {
5732
+ return textResult(
5733
+ `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0437\u0430\u0434\u0430\u043D, \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043D\u0430 \u0432 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 (WAITING_HUMAN). \u041E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E ${BLOCKING_WAIT_SEC} \u0441 (pending) \u2014 \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}", \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0436\u0434\u0430\u0442\u044C. \u041D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443 \u043F\u043E \u0437\u0430\u0434\u0430\u0447\u0435, \u043F\u043E\u043A\u0430 \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u0448\u044C \u043E\u0442\u0432\u0435\u0442.`
5493
5734
  );
5494
5735
  }
5495
- return gitOk(void 0);
5736
+ return textResult(
5737
+ `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D (\u043D\u0435 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443). \u041E\u0442\u0432\u0435\u0442\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u2014 \u0435\u0441\u043B\u0438 \u043E\u043D \u043D\u0443\u0436\u0435\u043D, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C, \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}".`
5738
+ );
5496
5739
  }
5497
- async function pushFastForward(params) {
5498
- const remote = params.remote ?? "origin";
5499
- const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
5500
- if (!branchCheck.ok) return branchCheck;
5501
- const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
5502
- if (!defaultBranchCheck.ok) return defaultBranchCheck;
5503
- const ready = await assertReadyToPush(params);
5504
- if (!ready.ok) return ready;
5505
- const pushResult = await runGit(["push", remote, `HEAD:refs/heads/${params.defaultBranch}`], {
5506
- cwd: params.worktreeDir
5740
+ var waitAnswerInputShape = {
5741
+ questionId: questionWaitPayloadSchema.shape.questionId.describe(
5742
+ "ID \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u2014 \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430 ask_human (question.id) \u0438\u043B\u0438 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0433\u043E \u0432\u044B\u0437\u043E\u0432\u0430 wait_answer."
5743
+ ),
5744
+ waitSec: questionWaitPayloadSchema.shape.waitSec.optional().describe("\u0421\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0435\u043A\u0443\u043D\u0434 \u0436\u0434\u0430\u0442\u044C \u043E\u0442\u0432\u0435\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u044B\u0437\u043E\u0432\u0435, 0..600. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E 300.")
5745
+ };
5746
+ var DEFAULT_WAIT_ANSWER_SEC = 300;
5747
+ var WAIT_ANSWER_ACK_BUFFER_MS = 15e3;
5748
+ async function waitAnswerHandler(args, runtime) {
5749
+ const waitSec = args.waitSec ?? DEFAULT_WAIT_ANSWER_SEC;
5750
+ const parsedArgs = parseArgsOrError(questionWaitPayloadSchema, {
5751
+ questionId: args.questionId,
5752
+ waitSec
5507
5753
  });
5508
- if (!pushResult.ok) {
5509
- return gitErr(
5510
- describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.defaultBranch}`, pushResult)
5511
- );
5512
- }
5513
- return gitOk({ pushedTo: `${remote}/${params.defaultBranch}` });
5754
+ if (!parsedArgs.ok) return errorResult(parsedArgs.message);
5755
+ const result = await callServerOrError(
5756
+ runtime.client,
5757
+ "question:wait",
5758
+ parsedArgs.data,
5759
+ questionWaitAckSchema,
5760
+ waitSec * 1e3 + WAIT_ANSWER_ACK_BUFFER_MS
5761
+ );
5762
+ if (!result.ok) return errorResult(result.message);
5763
+ const settled = describeSettledQuestion(result.data.question);
5764
+ if (settled) return textResult(settled);
5765
+ return textResult(
5766
+ `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0432\u0441\u0451 \u0435\u0449\u0451 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 (\u0436\u0434\u0430\u043B ${waitSec} \u0441). \u0412\u044B\u0437\u043E\u0432\u0438 wait_answer \u0435\u0449\u0451 \u0440\u0430\u0437 \u0441 \u0442\u0435\u043C \u0436\u0435 questionId, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0443\u043C\u0435\u0441\u0442\u043D\u043E.`
5767
+ );
5514
5768
  }
5515
- async function submitDirectTask(params) {
5516
- const remote = params.remote ?? "origin";
5517
- let worktreeDir;
5518
- if (params.worktreeDir) {
5519
- worktreeDir = params.worktreeDir;
5520
- } else {
5521
- const worktree = await ensureWorktree({
5522
- repoRoot: params.repoRoot,
5523
- branchName: params.branchName,
5524
- defaultBranch: params.defaultBranch,
5525
- remote,
5526
- worktreesRoot: params.worktreesRoot
5527
- });
5528
- if (!worktree.ok) return { status: "refused", message: worktree.message };
5529
- worktreeDir = worktree.data.worktreeDir;
5769
+ function describeAnswerAttachments(question) {
5770
+ if (question.attachments.length === 0) return "";
5771
+ const lines = question.attachments.map(
5772
+ (file) => file.downloadUrl ? `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u043A\u0430\u0447\u0430\u0439: ${file.downloadUrl}` : `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430, \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442`
5773
+ );
5774
+ return `
5775
+
5776
+ \u041A \u043E\u0442\u0432\u0435\u0442\u0443 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u044B \u0444\u0430\u0439\u043B\u044B (${question.attachments.length}). \u042D\u0442\u043E \u0447\u0430\u0441\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u0430: \u0441\u043A\u0430\u0447\u0430\u0439 \u0438 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u041A\u0410\u0416\u0414\u042B\u0419, \u0441\u0441\u044B\u043B\u043A\u0438 \u0436\u0438\u0432\u0443\u0442 ${question.attachments[0]?.expiresInSeconds ?? 300} \u0441 \u2014 \u043D\u0435 \u043E\u0442\u043A\u043B\u0430\u0434\u044B\u0432\u0430\u0439.
5777
+ ` + lines.join("\n");
5778
+ }
5779
+ function describeSettledQuestion(question) {
5780
+ if (question.status === QuestionStatus.ANSWERED) {
5781
+ return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043F\u043E\u043B\u0443\u0447\u0438\u043B \u043E\u0442\u0432\u0435\u0442:
5782
+ ${question.answerMd ?? ""}${describeAnswerAttachments(question)}`;
5530
5783
  }
5531
- const commit = await commitAll({
5532
- worktreeDir,
5533
- commitPrefix: params.commitPrefix,
5534
- title: params.taskTitle,
5535
- taskKey: params.taskKey
5536
- });
5537
- if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
5538
- const rebase = await rebaseOntoDefault({
5539
- worktreeDir,
5540
- defaultBranch: params.defaultBranch,
5541
- remote
5542
- });
5543
- if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
5544
- if (rebase.data.outcome === "conflict") {
5545
- const files = rebase.data.conflictedFiles;
5546
- return {
5547
- status: "conflict",
5548
- worktreeDir,
5549
- conflictedFiles: files,
5550
- message: `Rebase \u043D\u0430 ${remote}/${params.defaultBranch} \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0432 \u0444\u0430\u0439\u043B\u0430\u0445: ${files.join(", ")}. Rebase \u043E\u0442\u043C\u0435\u043D\u0451\u043D ("git rebase --abort"), \u0432\u0435\u0442\u043A\u0430 "${params.branchName}" \u0438 \u0435\u0451 worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`
5551
- };
5784
+ if (question.status === QuestionStatus.DISMISSED) {
5785
+ return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043E\u0442\u043A\u043B\u043E\u043D\u0451\u043D (DISMISSED) \u2014 \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442. \u0420\u0435\u0448\u0438, \u043A\u0430\u043A \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043B\u044C\u0448\u0435, \u0441\u0430\u043C, \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0437\u0430\u043D\u043E\u0432\u043E.`;
5552
5786
  }
5553
- if (params.requireTestsPassed && !params.testsPassed) {
5554
- return {
5555
- status: "refused",
5556
- worktreeDir,
5557
- message: "\u0414\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 requireTests=true, \u0430 testsPassed \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D (\u0438\u043B\u0438 false) \u2014 push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F."
5558
- };
5787
+ return void 0;
5788
+ }
5789
+
5790
+ // package.json
5791
+ var package_default = {
5792
+ name: "@quiel/cli",
5793
+ version: "0.5.3",
5794
+ description: "CLI \u0438 MCP-\u0441\u0435\u0440\u0432\u0435\u0440 Quiel: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0430\u0433\u0435\u043D\u0442\u0430 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 (Claude Code, Codex, Cursor, OpenCode) \u043A \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0435",
5795
+ homepage: "https://quiel.app",
5796
+ repository: {
5797
+ type: "git",
5798
+ url: "git+https://github.com/Quiel-App/quiel-cli.git"
5799
+ },
5800
+ bugs: {
5801
+ url: "https://github.com/Quiel-App/quiel-cli/issues"
5802
+ },
5803
+ keywords: [
5804
+ "quiel",
5805
+ "claude-code",
5806
+ "codex",
5807
+ "cursor",
5808
+ "opencode",
5809
+ "mcp",
5810
+ "mcp-server",
5811
+ "ai-agents",
5812
+ "task-tracker"
5813
+ ],
5814
+ license: "Apache-2.0",
5815
+ type: "module",
5816
+ bin: {
5817
+ quiel: "dist/cli.js"
5818
+ },
5819
+ files: [
5820
+ "dist",
5821
+ "README.md",
5822
+ "LICENSE"
5823
+ ],
5824
+ engines: {
5825
+ node: ">=22"
5826
+ },
5827
+ publishConfig: {
5828
+ access: "public"
5829
+ },
5830
+ scripts: {
5831
+ build: "tsup",
5832
+ dev: "tsup --watch",
5833
+ lint: "biome check .",
5834
+ typecheck: "tsc --noEmit",
5835
+ test: "vitest run --passWithNoTests"
5836
+ },
5837
+ dependencies: {
5838
+ "@modelcontextprotocol/sdk": "1.30.0",
5839
+ commander: "15.0.0",
5840
+ "socket.io-client": "4.8.3",
5841
+ zod: "4.5.4"
5842
+ },
5843
+ devDependencies: {
5844
+ "@quiel/config": "workspace:*",
5845
+ "@quiel/shared": "workspace:*",
5846
+ "@types/node": "20.19.43",
5847
+ tsup: "8.5.1",
5848
+ typescript: "7.0.2",
5849
+ vitest: "5.0.0"
5559
5850
  }
5560
- const push = await pushFastForward({
5561
- worktreeDir,
5562
- branchName: params.branchName,
5563
- defaultBranch: params.defaultBranch,
5564
- remote
5565
- });
5566
- if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
5567
- const sha = await getHeadSha(worktreeDir);
5568
- let worktreeRemoved = false;
5569
- if (params.removeWorktreeOnSuccess !== false) {
5570
- const removed = await removeWorktree(params.repoRoot, worktreeDir);
5571
- if (removed.ok) {
5572
- await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
5573
- worktreeRemoved = true;
5574
- }
5851
+ };
5852
+
5853
+ // src/version.ts
5854
+ var VERSION = package_default.version;
5855
+
5856
+ // src/mcp/constants.ts
5857
+ var MCP_SERVER_NAME = "quiel";
5858
+ var MCP_SERVER_VERSION = VERSION;
5859
+
5860
+ // src/mcp/get-context.ts
5861
+ var ALL_CONTEXT_INCLUDES = [
5862
+ "spec",
5863
+ "decisions",
5864
+ // ВСЕ документы проекта — по умолчанию, а не по особому запросу.
5865
+ // `spec`/`decisions` покрывают два типа из пяти, и человек, положивший
5866
+ // контекст под типом `DATA` или `PLAN`, вправе ожидать, что агент его
5867
+ // увидит. 14 сентября 2026 ровно этого и не случилось.
5868
+ "documents",
5869
+ "related",
5870
+ "repo"
5871
+ ];
5872
+ var getContextInputShape = {
5873
+ taskId: contextGetPayloadSchema.shape.taskId.describe(
5874
+ "ID \u0437\u0430\u0434\u0430\u0447\u0438, \u0434\u043B\u044F \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u043D\u0443\u0436\u0435\u043D \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442. \u0415\u0441\u043B\u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0430\u0433\u0435\u043D\u0442\u0430 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C)."
5875
+ ),
5876
+ include: contextGetPayloadSchema.shape.include.optional().describe(
5877
+ "\u0427\u0442\u043E \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C: spec | decisions | related | repo. \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u0438\u043B\u0438 \u043F\u0443\u0441\u0442 \u2014 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0432\u0441\u0451 \u0441\u0440\u0430\u0437\u0443."
5878
+ )
5879
+ };
5880
+ async function getContextHandler(args, runtime) {
5881
+ const state = readState(runtime.stateFilePath);
5882
+ const taskId = args.taskId ?? state.currentTask?.taskId;
5883
+ if (taskId) {
5884
+ const lease = runtime.client.isLeaseLost(taskId);
5885
+ if (lease.lost) return leaseLostResult(taskId, lease.reason);
5575
5886
  }
5576
- return {
5577
- status: "pushed",
5578
- worktreeDir,
5579
- commitSha: sha.ok ? sha.data : null,
5580
- pushedTo: push.data.pushedTo,
5581
- worktreeRemoved
5582
- };
5887
+ const include = args.include && args.include.length > 0 ? args.include : [...ALL_CONTEXT_INCLUDES];
5888
+ const payload = contextGetPayloadSchema.parse({ ...taskId ? { taskId } : {}, include });
5889
+ const result = await callServerOrError(
5890
+ runtime.client,
5891
+ "context:get",
5892
+ payload,
5893
+ contextPayloadSchema
5894
+ );
5895
+ if (!result.ok) return errorResult(result.message);
5896
+ return textResult(JSON.stringify(result.data, null, 2));
5583
5897
  }
5584
5898
 
5585
- // src/git/submit-pr.ts
5586
- async function pushTaskBranch(params) {
5587
- const remote = params.remote ?? "origin";
5588
- const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
5589
- if (!branchCheck.ok) return branchCheck;
5590
- const ready = await assertReadyToPush(params);
5591
- if (!ready.ok) return ready;
5592
- const pushResult = await runGit(
5593
- ["push", "--set-upstream", remote, `HEAD:refs/heads/${params.branchName}`],
5594
- { cwd: params.worktreeDir }
5595
- );
5596
- if (!pushResult.ok) {
5597
- return gitErr(
5598
- describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.branchName}`, pushResult)
5899
+ // src/mcp/get-task.ts
5900
+ var getTaskInputShape = {
5901
+ taskId: taskGetPayloadSchema.shape.taskId.describe("ID \u0437\u0430\u0434\u0430\u0447\u0438 (Task.id) \u2014 \u043D\u0435 \u043A\u043B\u044E\u0447 \u0432\u0438\u0434\u0430 REL-42")
5902
+ };
5903
+ async function getTaskHandler(args, runtime) {
5904
+ const lease = runtime.client.isLeaseLost(args.taskId);
5905
+ if (lease.lost) return leaseLostResult(args.taskId, lease.reason);
5906
+ const payload = taskGetPayloadSchema.parse({ taskId: args.taskId });
5907
+ const result = await callServerOrError(runtime.client, "task:get", payload, taskGetAckSchema);
5908
+ if (!result.ok) return errorResult(result.message);
5909
+ return textResult(JSON.stringify(result.data, null, 2));
5910
+ }
5911
+
5912
+ // src/mcp/log.ts
5913
+ var logInputShape = {
5914
+ message: taskProgressPayloadSchema.shape.note.describe("\u0421\u0442\u0440\u043E\u043A\u0430 \u0432 \u043B\u0435\u043D\u0442\u0443 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438"),
5915
+ level: taskProgressPayloadSchema.shape.level.optional().describe("debug | info | warn | error, \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E info")
5916
+ };
5917
+ async function logHandler(args, runtime) {
5918
+ const state = readState(runtime.stateFilePath);
5919
+ if (!state.currentTask) {
5920
+ return errorResult(
5921
+ "\u041D\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0437\u0430\u043F\u0438\u0441\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430 \u0432 \u0435\u0451 \u043B\u0435\u043D\u0442\u0443. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0437\u043E\u0432\u0438 next_task."
5599
5922
  );
5600
5923
  }
5601
- return gitOk({ pushedTo: `${remote}/${params.branchName}` });
5602
- }
5603
- async function submitPullRequestTask(params) {
5604
- const remote = params.remote ?? "origin";
5605
- let worktreeDir;
5606
- if (params.worktreeDir) {
5607
- worktreeDir = params.worktreeDir;
5608
- } else {
5609
- const worktree = await ensureWorktree({
5610
- repoRoot: params.repoRoot,
5611
- branchName: params.branchName,
5612
- defaultBranch: params.defaultBranch,
5613
- remote,
5614
- worktreesRoot: params.worktreesRoot
5615
- });
5616
- if (!worktree.ok) return { status: "refused", message: worktree.message };
5617
- worktreeDir = worktree.data.worktreeDir;
5924
+ const lease = runtime.client.isLeaseLost(state.currentTask.taskId);
5925
+ if (lease.lost) {
5926
+ return leaseLostResult(state.currentTask.taskKey ?? state.currentTask.taskId, lease.reason);
5618
5927
  }
5619
- const commit = await commitAll({
5620
- worktreeDir,
5621
- commitPrefix: params.commitPrefix,
5622
- title: params.taskTitle,
5623
- taskKey: params.taskKey
5624
- });
5625
- if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
5626
- const rebase = await rebaseOntoDefault({
5627
- worktreeDir,
5628
- defaultBranch: params.defaultBranch,
5629
- remote
5928
+ const payload = taskProgressPayloadSchema.parse({
5929
+ taskId: state.currentTask.taskId,
5930
+ note: args.message,
5931
+ ...args.level ? { level: args.level } : {}
5630
5932
  });
5631
- if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
5632
- const conflicted = rebase.data.outcome === "conflict";
5633
- const push = await pushTaskBranch({ worktreeDir, branchName: params.branchName, remote });
5634
- if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
5635
- const sha = await getHeadSha(worktreeDir);
5636
- let worktreeRemoved = false;
5637
- if (params.removeWorktreeOnSuccess !== false) {
5638
- const removed = await removeWorktree(params.repoRoot, worktreeDir);
5639
- if (removed.ok) {
5640
- await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
5641
- worktreeRemoved = true;
5642
- }
5643
- }
5644
- return {
5645
- status: "pushed",
5646
- worktreeDir,
5647
- branchName: params.branchName,
5648
- commitSha: sha.ok ? sha.data : null,
5649
- pushedTo: push.data.pushedTo,
5650
- conflicted,
5651
- conflictedFiles: rebase.data.conflictedFiles,
5652
- worktreeRemoved
5653
- };
5933
+ const result = await callServerOrError(
5934
+ runtime.client,
5935
+ "task:progress",
5936
+ payload,
5937
+ taskProgressAckSchema
5938
+ );
5939
+ if (!result.ok) return errorResult(result.message);
5940
+ if (!result.data.ok) return errorResult("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u0437\u0430\u043F\u0438\u0441\u044C \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 (ok:false).");
5941
+ return textResult("\u0417\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438.");
5654
5942
  }
5655
5943
 
5656
5944
  // src/mcp/task-worktree.ts
@@ -5725,7 +6013,9 @@ async function nextTaskHandler(args, runtime) {
5725
6013
  taskId: task.task.id,
5726
6014
  taskKey: task.task.key,
5727
6015
  resumeToken: task.lease.resumeToken,
5728
- gitMode: task.project.gitMode
6016
+ gitMode: task.project.gitMode,
6017
+ branchName: task.project.branchName,
6018
+ commitPrefix: task.project.commitPrefix
5729
6019
  }
5730
6020
  },
5731
6021
  stateFilePath
@@ -5738,7 +6028,12 @@ async function nextTaskHandler(args, runtime) {
5738
6028
  const worktree = await ensureWorktree({
5739
6029
  repoRoot: runtime.cwd,
5740
6030
  branchName: plan.branchName,
5741
- defaultBranch: plan.defaultBranch
6031
+ defaultBranch: plan.defaultBranch,
6032
+ // LIM-03: задачу уже начинали и прервали — рабочее дерево заводим
6033
+ // ОТ ТОЙ ВЕТКИ, а не от основной. Иначе спасённая работа осталась
6034
+ // бы лежать на origin никем не прочитанной, и агент сделал бы её
6035
+ // заново, потратив свой лимит подписки на чужую.
6036
+ continueFromBranch: task.continueFrom?.branchName
5742
6037
  });
5743
6038
  if (worktree.ok) {
5744
6039
  updateState(
@@ -5748,6 +6043,8 @@ async function nextTaskHandler(args, runtime) {
5748
6043
  taskKey: task.task.key,
5749
6044
  resumeToken: task.lease.resumeToken,
5750
6045
  gitMode: task.project.gitMode,
6046
+ branchName: task.project.branchName,
6047
+ commitPrefix: task.project.commitPrefix,
5751
6048
  worktreeDir: worktree.data.worktreeDir
5752
6049
  }
5753
6050
  },
@@ -5766,11 +6063,14 @@ async function nextTaskHandler(args, runtime) {
5766
6063
  const warning = !started.ok ? `
5767
6064
 
5768
6065
  \u0412\u041D\u0418\u041C\u0410\u041D\u0418\u0415: task:start \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C (${started.message}). \u0410\u0440\u0435\u043D\u0434\u0430 \u0442\u0432\u043E\u044F (\u0437\u0430\u0434\u0430\u0447\u0430 \u0432\u044B\u0434\u0430\u043D\u0430), \u043D\u043E \u0441\u0442\u0430\u0442\u0443\u0441 \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043C\u043E\u0436\u0435\u0442 \u043E\u0441\u0442\u0430\u0442\u044C\u0441\u044F ASSIGNED \u0432\u043C\u0435\u0441\u0442\u043E IN_PROGRESS \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043A\u0430\u043A \u043E\u0431\u044B\u0447\u043D\u043E, report \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u0441\u0434\u0430\u0441\u0442 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442.` : !started.data.ok ? "\n\n\u0412\u041D\u0418\u041C\u0410\u041D\u0418\u0415: \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B task:start (ok:false). \u0410\u0440\u0435\u043D\u0434\u0430 \u0442\u0432\u043E\u044F, \u043D\u043E \u0441\u0442\u0430\u0442\u0443\u0441 \u043C\u043E\u0436\u0435\u0442 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0442\u044C \u2014 \u0443\u043F\u043E\u043C\u044F\u043D\u0438 \u044D\u0442\u043E \u0432 report." : "";
6066
+ const continueNote = task.continueFrom ? `
6067
+
6068
+ \u042D\u0422\u0423 \u0417\u0410\u0414\u0410\u0427\u0423 \u0423\u0416\u0415 \u041D\u0410\u0427\u0418\u041D\u0410\u041B\u0418. \u041D\u0435\u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u043B\u0435\u0436\u0438\u0442 \u0432 \u0432\u0435\u0442\u043A\u0435 "${task.continueFrom.branchName}"${task.continueFrom.sha ? ` (\u043A\u043E\u043C\u043C\u0438\u0442 ${task.continueFrom.sha.slice(0, 8)})` : ""}${task.continueFrom.note ? `: ${task.continueFrom.note}` : ""}. \u0420\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E \u0437\u0430\u0432\u0435\u0434\u0435\u043D\u043E \u043E\u0442 \u043D\u0435\u0451 \u2014 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E, \u0438 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439, \u0430 \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0439 \u0437\u0430\u043D\u043E\u0432\u043E. \u0420\u0435\u0448\u0438\u0448\u044C, \u0447\u0442\u043E \u043F\u0440\u043E\u0449\u0435 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0442\u044C, \u2014 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043D\u043E \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438.` : "";
5769
6069
  return textResult(
5770
6070
  `\u0417\u0430\u0434\u0430\u0447\u0430 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0430:
5771
6071
  ${JSON.stringify(task, null, 2)}
5772
6072
 
5773
- ${worktreeNote}${warning}`
6073
+ ${worktreeNote}${continueNote}${warning}`
5774
6074
  );
5775
6075
  }
5776
6076
 
@@ -6010,7 +6310,7 @@ async function settleApproval(ack, commandCwd, runtime, waitSecUsed) {
6010
6310
  }
6011
6311
 
6012
6312
  // src/mcp/set-mode.ts
6013
- import { z as z37 } from "zod";
6313
+ import { z as z38 } from "zod";
6014
6314
 
6015
6315
  // src/cli/mode.ts
6016
6316
  function parseModeArg(raw) {
@@ -6051,7 +6351,7 @@ async function requestModeChange(modeArg, options = {}) {
6051
6351
 
6052
6352
  // src/mcp/set-mode.ts
6053
6353
  var setModeInputShape = {
6054
- mode: z37.enum(["auto", "manual"]).describe("auto \u2014 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u043D\u043E\u043C\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u0430\u0433\u0435\u043D\u0442\u0430, manual \u2014 \u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C (\xA710.2 \u0422\u0417)")
6354
+ mode: z38.enum(["auto", "manual"]).describe("auto \u2014 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u043D\u043E\u043C\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u0430\u0433\u0435\u043D\u0442\u0430, manual \u2014 \u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C (\xA710.2 \u0422\u0417)")
6055
6355
  };
6056
6356
  async function setModeHandler(args, runtime) {
6057
6357
  const result = await requestModeChange(args.mode, {
@@ -6066,20 +6366,20 @@ async function setModeHandler(args, runtime) {
6066
6366
  }
6067
6367
 
6068
6368
  // src/mcp/submit-work.ts
6069
- import { z as z38 } from "zod";
6369
+ import { z as z39 } from "zod";
6070
6370
  var submitWorkInputShape = {
6071
- branchName: z38.string().min(1).describe(
6371
+ branchName: z39.string().min(1).describe(
6072
6372
  "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0431\u0435\u0440\u0438 \u0440\u043E\u0432\u043D\u043E \u0438\u0437 project.branchName \u0432 TaskPayload, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0432\u0435\u0440\u043D\u0443\u043B next_task. \u041D\u0435 \u043F\u0440\u0438\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u0441\u0432\u043E\u0451."
6073
6373
  ),
6074
- defaultBranch: z38.string().min(1).describe("\u041E\u0441\u043D\u043E\u0432\u043D\u0430\u044F \u0432\u0435\u0442\u043A\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u2014 \u0438\u0437 project.repos[].defaultBranch."),
6075
- commitPrefix: z38.string().min(1).describe('\u041F\u0440\u0435\u0444\u0438\u043A\u0441 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u2014 \u0438\u0437 project.commitPrefix, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "ATL-42:".'),
6076
- taskKey: z38.string().min(1).describe("\u041A\u043B\u044E\u0447 \u0437\u0430\u0434\u0430\u0447\u0438, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 ATL-42 \u2014 \u0438\u0437 task.key."),
6077
- taskTitle: z38.string().min(1).describe("\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0438\u0437 task.title."),
6078
- repoRoot: z38.string().min(1).optional().describe("\u041A\u043E\u0440\u0435\u043D\u044C git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u2014 \u0440\u0430\u0431\u043E\u0447\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043F\u0440\u043E\u0435\u043A\u0442\u0430."),
6079
- testsPassed: z38.boolean().optional().describe(
6374
+ defaultBranch: z39.string().min(1).describe("\u041E\u0441\u043D\u043E\u0432\u043D\u0430\u044F \u0432\u0435\u0442\u043A\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u2014 \u0438\u0437 project.repos[].defaultBranch."),
6375
+ commitPrefix: z39.string().min(1).describe('\u041F\u0440\u0435\u0444\u0438\u043A\u0441 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u2014 \u0438\u0437 project.commitPrefix, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "ATL-42:".'),
6376
+ taskKey: z39.string().min(1).describe("\u041A\u043B\u044E\u0447 \u0437\u0430\u0434\u0430\u0447\u0438, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 ATL-42 \u2014 \u0438\u0437 task.key."),
6377
+ taskTitle: z39.string().min(1).describe("\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0438\u0437 task.title."),
6378
+ repoRoot: z39.string().min(1).optional().describe("\u041A\u043E\u0440\u0435\u043D\u044C git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u2014 \u0440\u0430\u0431\u043E\u0447\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043F\u0440\u043E\u0435\u043A\u0442\u0430."),
6379
+ testsPassed: z39.boolean().optional().describe(
6080
6380
  "\u041F\u0440\u043E\u0448\u043B\u0438 \u043B\u0438 \u0442\u0435\u0441\u0442\u044B. \u0415\u0441\u043B\u0438 \u043F\u0440\u043E\u0435\u043A\u0442 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0437\u0435\u043B\u0451\u043D\u044B\u0445 \u0442\u0435\u0441\u0442\u043E\u0432, \u0430 \u0437\u0434\u0435\u0441\u044C false \u2014 \u0441\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442\u0441\u044F, \u0438 \u044D\u0442\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E."
6081
6381
  ),
6082
- requireTestsPassed: z38.boolean().optional().describe("ProjectSettings.requireTests \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0438\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0437\u0430\u0434\u0430\u0447\u0438.")
6382
+ requireTestsPassed: z39.boolean().optional().describe("ProjectSettings.requireTests \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0438\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0437\u0430\u0434\u0430\u0447\u0438.")
6083
6383
  };
6084
6384
  async function submitWorkHandler(args, runtime) {
6085
6385
  const state = readState(runtime.stateFilePath);
@@ -6553,13 +6853,13 @@ function formatConnectResult(result) {
6553
6853
  }
6554
6854
 
6555
6855
  // src/hooks/pre-compact.ts
6556
- import { z as z39 } from "zod";
6557
- var PreCompactInputSchema = z39.object({
6558
- hook_event_name: z39.literal("PreCompact"),
6559
- session_id: z39.string(),
6560
- cwd: z39.string(),
6561
- trigger: z39.enum(["manual", "auto"]),
6562
- custom_instructions: z39.string().nullable()
6856
+ import { z as z40 } from "zod";
6857
+ var PreCompactInputSchema = z40.object({
6858
+ hook_event_name: z40.literal("PreCompact"),
6859
+ session_id: z40.string(),
6860
+ cwd: z40.string(),
6861
+ trigger: z40.enum(["manual", "auto"]),
6862
+ custom_instructions: z40.string().nullable()
6563
6863
  }).passthrough();
6564
6864
  function preCompactHook(_input, state, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
6565
6865
  const lastPreCompact = state.currentTask ? { ...state.currentTask, savedAt: now() } : null;
@@ -6568,14 +6868,14 @@ function preCompactHook(_input, state, now = () => (/* @__PURE__ */ new Date()).
6568
6868
 
6569
6869
  // src/hooks/pre-tool-use.ts
6570
6870
  import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
6571
- import { z as z40 } from "zod";
6572
- var PreToolUseInputSchema = z40.object({
6573
- hook_event_name: z40.literal("PreToolUse"),
6574
- session_id: z40.string(),
6575
- cwd: z40.string(),
6576
- tool_name: z40.string(),
6577
- tool_input: z40.record(z40.string(), z40.unknown()),
6578
- tool_use_id: z40.string()
6871
+ import { z as z41 } from "zod";
6872
+ var PreToolUseInputSchema = z41.object({
6873
+ hook_event_name: z41.literal("PreToolUse"),
6874
+ session_id: z41.string(),
6875
+ cwd: z41.string(),
6876
+ tool_name: z41.string(),
6877
+ tool_input: z41.record(z41.string(), z41.unknown()),
6878
+ tool_use_id: z41.string()
6579
6879
  }).passthrough();
6580
6880
  var NO_TRUST_CACHE_REASON = "\u041A\u044D\u0448 \u043F\u0440\u0430\u0432\u0438\u043B \u0434\u043E\u0432\u0435\u0440\u0438\u044F (TrustRules) \u043F\u0443\u0441\u0442 \u2014 \u043A\u043B\u0438\u0435\u043D\u0442 \u043D\u0438 \u0440\u0430\u0437\u0443 \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0430\u043B \u0438\u0445 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043D\u0435\u0442 agent:hello/trust:updated). \u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 \u043F\u0440\u043E\u0444\u0438\u043B\u044C (full/guarded/approve-all) \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u043D\u0435\u043B\u044C\u0437\u044F, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043F\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (fail-closed \u043F\u0440\u0438 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \xA710.4 \u0422\u0417) \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435: `quiel connect` (\u044D\u0442\u0430 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0430 \u0438\u043C\u0435\u043D\u043D\u043E \u0434\u043B\u044F \u0442\u0430\u043A\u043E\u0433\u043E \u0441\u043B\u0443\u0447\u0430\u044F, \u0441\u043C. isBootstrapCommand) \u2014 \u043F\u043E\u0441\u043B\u0435 \u0440\u0443\u043A\u043E\u043F\u043E\u0436\u0430\u0442\u0438\u044F \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043F\u0440\u0438\u0434\u0443\u0442, \u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u043D\u0430\u0447\u043D\u0443\u0442 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442\u044C. \u0415\u0441\u043B\u0438 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C, \u0432\u044B\u0437\u043E\u0432\u0438 request_approval.";
6581
6881
  var BOOTSTRAP_COMMAND_RE = /^quiel\s+(connect|status)\b/;
@@ -6807,13 +7107,13 @@ function readNewUsage(transcriptPath, offset) {
6807
7107
  }
6808
7108
 
6809
7109
  // src/hooks/session-start.ts
6810
- import { z as z41 } from "zod";
6811
- var SessionStartInputSchema = z41.object({
6812
- hook_event_name: z41.literal("SessionStart"),
6813
- session_id: z41.string(),
6814
- cwd: z41.string(),
6815
- source: z41.enum(["startup", "resume", "clear", "compact", "fork"]),
6816
- model: z41.string().optional()
7110
+ import { z as z42 } from "zod";
7111
+ var SessionStartInputSchema = z42.object({
7112
+ hook_event_name: z42.literal("SessionStart"),
7113
+ session_id: z42.string(),
7114
+ cwd: z42.string(),
7115
+ source: z42.enum(["startup", "resume", "clear", "compact", "fork"]),
7116
+ model: z42.string().optional()
6817
7117
  }).passthrough();
6818
7118
  var SESSION_RESET_SOURCES = /* @__PURE__ */ new Set([
6819
7119
  "startup",
@@ -6852,13 +7152,13 @@ function sessionStartHook(input, state) {
6852
7152
  }
6853
7153
 
6854
7154
  // src/hooks/stop.ts
6855
- import { z as z42 } from "zod";
6856
- var StopInputSchema = z42.object({
6857
- hook_event_name: z42.literal("Stop"),
6858
- session_id: z42.string(),
6859
- cwd: z42.string(),
6860
- stop_hook_active: z42.boolean(),
6861
- last_assistant_message: z42.string().optional()
7155
+ import { z as z43 } from "zod";
7156
+ var StopInputSchema = z43.object({
7157
+ hook_event_name: z43.literal("Stop"),
7158
+ session_id: z43.string(),
7159
+ cwd: z43.string(),
7160
+ stop_hook_active: z43.boolean(),
7161
+ last_assistant_message: z43.string().optional()
6862
7162
  }).passthrough();
6863
7163
  function taskLabel(state) {
6864
7164
  if (!state.currentTask) return "";
@@ -6901,55 +7201,6 @@ function stopHook(input, state) {
6901
7201
  };
6902
7202
  }
6903
7203
 
6904
- // src/hooks/stop-failure.ts
6905
- import { z as z43 } from "zod";
6906
- var StopFailureErrorTypeSchema = z43.enum([
6907
- "rate_limit",
6908
- "overloaded",
6909
- "authentication_failed",
6910
- "oauth_org_not_allowed",
6911
- "account_on_hold",
6912
- "billing_error",
6913
- "invalid_request",
6914
- "model_not_found",
6915
- "server_error",
6916
- "max_output_tokens",
6917
- "unknown"
6918
- ]);
6919
- var StopFailureInputSchema = z43.object({
6920
- hook_event_name: z43.literal("StopFailure"),
6921
- session_id: z43.string(),
6922
- cwd: z43.string(),
6923
- error: StopFailureErrorTypeSchema,
6924
- error_details: z43.unknown().optional(),
6925
- last_assistant_message: z43.string().optional()
6926
- }).passthrough();
6927
- function extractRetryAfterSec(errorDetails) {
6928
- if (typeof errorDetails === "number" && Number.isFinite(errorDetails) && errorDetails >= 0) {
6929
- return errorDetails;
6930
- }
6931
- if (errorDetails && typeof errorDetails === "object") {
6932
- const record = errorDetails;
6933
- for (const key of ["retryAfterSec", "retryAfter", "retry_after", "retry_after_seconds"]) {
6934
- const value = record[key];
6935
- if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
6936
- }
6937
- }
6938
- if (typeof errorDetails === "string") {
6939
- const match = errorDetails.match(/retry[-_ ]?after[^0-9]{0,10}(\d+)/i);
6940
- if (match?.[1] !== void 0) return Number(match[1]);
6941
- }
6942
- return void 0;
6943
- }
6944
- function stopFailureHook(input, _state) {
6945
- const retryAfterSec = extractRetryAfterSec(input.error_details);
6946
- const action = {
6947
- type: "agent:rate_limited",
6948
- payload: retryAfterSec === void 0 ? { error: input.error } : { error: input.error, retryAfterSec }
6949
- };
6950
- return { output: {}, serverActions: [action] };
6951
- }
6952
-
6953
7204
  // src/hooks/run.ts
6954
7205
  var HookInputSchema = z44.discriminatedUnion("hook_event_name", [
6955
7206
  SessionStartInputSchema,
@@ -7453,13 +7704,26 @@ function createProgram() {
7453
7704
  if (result.stderr.length > 0) process.stderr.write(`${result.stderr}
7454
7705
  `);
7455
7706
  process.exitCode = result.exitCode;
7456
- if (result.execution) sendHookActions(result.execution.serverActions);
7707
+ if (result.execution) {
7708
+ const rescued = await runRescueAction(result.execution.serverActions);
7709
+ const rescuedNote = describeRescue(rescued);
7710
+ if (rescuedNote) process.stderr.write(`${rescuedNote}
7711
+ `);
7712
+ sendHookActions(
7713
+ rescued.report ? [...result.execution.serverActions, rescued.report] : result.execution.serverActions
7714
+ );
7715
+ }
7457
7716
  return;
7458
7717
  }
7459
7718
  const execution = runCliHook(raw);
7460
7719
  process.stdout.write(`${execution.stdout}
7461
7720
  `);
7462
7721
  process.exitCode = execution.exitCode;
7722
+ const rescue = await runRescueAction(execution.serverActions);
7723
+ const note = describeRescue(rescue);
7724
+ if (note) process.stderr.write(`${note}
7725
+ `);
7726
+ if (rescue.report) sendHookActions([rescue.report]);
7463
7727
  sendHookActions(execution.serverActions);
7464
7728
  });
7465
7729
  program.command("mcp").description(