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