devez-vibe 1.9.52 → 1.9.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/dvz.exe CHANGED
Binary file
@@ -3,7 +3,7 @@
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { execFile, spawn } from "node:child_process";
5
5
  import { createReadStream, existsSync, readFileSync, readdirSync } from "node:fs";
6
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
7
7
  import { createRequire } from "node:module";
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { dirname, join, resolve as resolvePath, sep } from "node:path";
@@ -1522,13 +1522,18 @@ function runClaudeCommand(params, args, deadlineMs = CLAUDE_COMMAND_TIMEOUT) {
1522
1522
  const output = Buffer.concat(stdout).toString("utf8").trim();
1523
1523
  const detail = Buffer.concat(stderr).toString("utf8").trim();
1524
1524
  if (code === 0) resolve(output);
1525
- else reject(new Error(detail || output || `Claude 명령이 종료 코드 ${code}로 실패했습니다.`));
1525
+ else reject(Object.assign(new Error(detail || output || `Claude 명령이 종료 코드 ${code}로 실패했습니다.`), { exitCode: code }));
1526
1526
  });
1527
1527
  });
1528
1528
  }
1529
1529
 
1530
+ // Windows의 Claude Code는 가끔 출력을 다 낸 뒤에도 0xC0000005 같은 충돌 코드로
1531
+ // 끝난다. 읽기 명령이라 종료 코드로 실패하면 한 번 더 실행한다. 멈춤은 다시 기다리지 않는다.
1530
1532
  async function runClaudeJson(params, args) {
1531
- const output = await runClaudeCommand(params, args);
1533
+ const output = await runClaudeCommand(params, args).catch((error) => {
1534
+ if (error.exitCode === undefined) throw error;
1535
+ return runClaudeCommand(params, args);
1536
+ });
1532
1537
  try { return output ? JSON.parse(output) : null; }
1533
1538
  catch (error) {
1534
1539
  throw new Error(`Claude 플러그인 응답을 해석하지 못했습니다: ${error.message}`);
@@ -1890,6 +1895,13 @@ function questionFallbackMessage(questions) {
1890
1895
  ].join("\n");
1891
1896
  }
1892
1897
 
1898
+ // Windows는 충돌한 프로세스를 0xC0000000 이상의 종료 코드로 알린다(0xC0000005 접근
1899
+ // 위반, 0xC0000374 힙 손상). 설정·인증 오류의 일반 종료 코드와 달리 다시 띄우면 대개 뜬다.
1900
+ function isClaudeCliCrash(error) {
1901
+ const code = /process exited with code (\d+)/.exec(String(error?.message ?? error))?.[1];
1902
+ return Number(code) >= 0xC0000000;
1903
+ }
1904
+
1893
1905
  async function createSession(params, resumeId) {
1894
1906
  const id = resumeId || randomUUID();
1895
1907
  const queue = new AsyncQueue();
@@ -1949,6 +1961,15 @@ async function createSession(params, resumeId) {
1949
1961
  clearUsageLimitWait(session);
1950
1962
  if (session.turn) finishTurn(session, new Error("Claude 연결이 종료되었습니다."));
1951
1963
  clearSubagents(session);
1964
+ // A crashed CLI (Windows reports an access violation as exit code
1965
+ // 3221225477) leaves a query that rejects every later request. Dropping
1966
+ // the session makes the next prompt fail as missing, which the host answers
1967
+ // by resuming it from the transcript instead of the user reopening it.
1968
+ if (sessions.get(session.id) === session) {
1969
+ sessions.delete(session.id);
1970
+ session.queue.close();
1971
+ session.query.close();
1972
+ }
1952
1973
  });
1953
1974
  session.consumer = consumer;
1954
1975
  let initialization;
@@ -2560,12 +2581,16 @@ const SUBAGENT_PULSE_MS = 5000;
2560
2581
  // 자체 시험에서 "오래 조용한 행"을 만들 때 쓰는 나이.
2561
2582
  const IDLE_ROW_AGE_MS = 120_000;
2562
2583
 
2584
+ // 순정 Claude Code처럼 행에 Agent의 name과 description을 함께 보인다. description의
2585
+ // 언어는 매 턴 지침이 화면 언어에 맞춘다.
2563
2586
  function startSubagent(session, block) {
2564
2587
  const input = block.input || {};
2588
+ const description = firstLine(input.description || "", SUBAGENT_DESCRIPTION_LIMIT);
2565
2589
  const existing = findSubagent(session, block.id);
2566
2590
  if (existing) {
2567
2591
  existing.toolUseId = block.id;
2568
2592
  existing.name = firstLine(input.name || input.subagent_type || input.agentType || existing.name || "agent", 40);
2593
+ existing.description = description || existing.description;
2569
2594
  existing.backgroundTask = false;
2570
2595
  existing.lastSeenAt = Date.now();
2571
2596
  emitSubagents(session);
@@ -2577,8 +2602,7 @@ function startSubagent(session, block) {
2577
2602
  taskId: "",
2578
2603
  background: false,
2579
2604
  name: firstLine(input.name || input.subagent_type || input.agentType || "agent", 40),
2580
- // 행은 이름과 경과 시간만 보인다. 무엇을 시켰는지는 이름이 말하게 한다.
2581
- description: "",
2605
+ description,
2582
2606
  tool: "",
2583
2607
  startedAt: Date.now(),
2584
2608
  lastSeenAt: Date.now(),
@@ -2665,7 +2689,7 @@ function resumeBackgroundSubagent(session, toolUseId, pending, result) {
2665
2689
  taskId,
2666
2690
  background: true,
2667
2691
  name: known?.name || "agent",
2668
- description: "",
2692
+ description: known?.description || "",
2669
2693
  tool: "",
2670
2694
  startedAt: Date.now(),
2671
2695
  lastSeenAt: Date.now(),
@@ -2766,7 +2790,7 @@ function upsertStructuredSubagent(session, message) {
2766
2790
  return running;
2767
2791
  }
2768
2792
 
2769
- function finishStructuredSubagent(session, message, kind, text) {
2793
+ function finishStructuredSubagent(session, message) {
2770
2794
  const taskId = firstLine(message.task_id || "", 80);
2771
2795
  const toolUseId = firstLine(message.tool_use_id || "", 80);
2772
2796
  const wasTranscriptHidden = taskId
@@ -2779,7 +2803,6 @@ function finishStructuredSubagent(session, message, kind, text) {
2779
2803
  const running = (toolUseId && findSubagent(session, toolUseId))
2780
2804
  || (taskId && findSubagent(session, taskId));
2781
2805
  if (!running) return wasHidden;
2782
- emitSubagentLine(session, running.id, { kind, text });
2783
2806
  session.subagents.delete(running.id);
2784
2807
  emitSubagents(session);
2785
2808
  return true;
@@ -2879,25 +2902,14 @@ function processSubagentSystemMessage(session, message) {
2879
2902
  if (!running) return false;
2880
2903
  const tool = firstLine(message.last_tool_name || "", 80);
2881
2904
  if (tool) running.tool = tool;
2882
- const summary = firstLine(message.summary || "", 200);
2883
- if (summary && summary !== running.lastSummary) {
2884
- running.lastSummary = summary;
2885
- emitSubagentLine(session, running.id, { kind: "result", text: summary });
2886
- }
2887
2905
  emitSubagents(session);
2888
2906
  return true;
2889
2907
  }
2890
2908
  if (message.subtype === "task_updated") {
2891
2909
  const taskId = firstLine(message.task_id || "", 80);
2892
2910
  const patch = message.patch || {};
2893
- const status = patch.status;
2894
- if (["completed", "failed", "killed"].includes(status)) {
2895
- return finishStructuredSubagent(
2896
- session,
2897
- message,
2898
- status === "completed" ? "result" : "error",
2899
- firstLine(patch.error || status, 200),
2900
- );
2911
+ if (["completed", "failed", "killed"].includes(patch.status)) {
2912
+ return finishStructuredSubagent(session, message);
2901
2913
  }
2902
2914
  const running = taskId && findSubagent(session, taskId);
2903
2915
  if (!running) return false;
@@ -2914,13 +2926,7 @@ function processSubagentSystemMessage(session, message) {
2914
2926
  session.ambientSubagentTasks ||= new Set();
2915
2927
  session.ambientSubagentTasks.add(firstLine(message.task_id, 80));
2916
2928
  }
2917
- const status = firstLine(message.status || "completed", 40);
2918
- return finishStructuredSubagent(
2919
- session,
2920
- message,
2921
- status === "completed" ? "result" : "error",
2922
- firstLine(message.summary || status, 200),
2923
- );
2929
+ return finishStructuredSubagent(session, message);
2924
2930
  }
2925
2931
  if (message.subtype === "background_tasks_changed") {
2926
2932
  syncBackgroundSubagents(session, message.tasks);
@@ -2929,61 +2935,24 @@ function processSubagentSystemMessage(session, message) {
2929
2935
  return false;
2930
2936
  }
2931
2937
 
2932
- // 서브에이전트가 실제로 무엇을 했는지는 자식 메시지에만 남는다. 열람용 기록은 여기서
2933
- // 한 줄씩 흘려보내고, 목록 행에 쓸 현재 도구만 따로 갱신한다.
2938
+ // 서브에이전트가 지금 무슨 도구를 돌리는지는 자식 메시지에만 남는다. 목록 행에 쓸
2939
+ // 현재 도구만 갱신한다.
2934
2940
  // 메시지의 agent_id는 그 하위 에이전트 작업의 task_id라 재개 뒤에도 같은 행을 찾는다.
2935
- function messageSubagent(session, message) {
2936
- return findSubagent(session, message.parent_tool_use_id)
2937
- || (message.agent_id ? findSubagent(session, message.agent_id) : undefined);
2938
- }
2939
-
2940
2941
  function recordSubagentMessage(session, message) {
2941
- const running = messageSubagent(session, message);
2942
+ const running = findSubagent(session, message.parent_tool_use_id)
2943
+ || (message.agent_id ? findSubagent(session, message.agent_id) : undefined);
2942
2944
  if (!running) return;
2943
2945
  running.lastSeenAt = Date.now();
2944
2946
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
2945
2947
  let toolChanged = false;
2946
2948
  for (const block of content) {
2947
- if (block.type === "text") {
2948
- const text = String(block.text || "").trim();
2949
- if (text) emitSubagentLine(session, running.id, { kind: "text", text });
2950
- } else if (block.type === "tool_use") {
2951
- running.tool = subagentToolLabel(block);
2952
- toolChanged = true;
2953
- emitSubagentLine(session, running.id, {
2954
- kind: "tool",
2955
- text: running.tool,
2956
- toolUseId: block.id,
2957
- });
2958
- }
2949
+ if (block.type !== "tool_use") continue;
2950
+ running.tool = subagentToolLabel(block);
2951
+ toolChanged = true;
2959
2952
  }
2960
2953
  if (toolChanged) emitSubagents(session);
2961
2954
  }
2962
2955
 
2963
- function recordSubagentResult(session, message) {
2964
- const running = messageSubagent(session, message);
2965
- if (!running) return;
2966
- running.lastSeenAt = Date.now();
2967
- const content = Array.isArray(message.message?.content) ? message.message.content : [];
2968
- for (const block of content) {
2969
- if (block.type !== "tool_result") continue;
2970
- emitSubagentLine(session, running.id, {
2971
- kind: block.is_error ? "error" : "result",
2972
- text: firstLine(toolOutput(block.content, message.tool_use_result), 200),
2973
- toolUseId: block.tool_use_id,
2974
- });
2975
- }
2976
- }
2977
-
2978
- function emitSubagentLine(session, parentToolUseId, line) {
2979
- notify("turn/subagent/line", {
2980
- threadId: session.id,
2981
- turnId: session.turn?.id,
2982
- parentToolUseId,
2983
- line,
2984
- });
2985
- }
2986
-
2987
2956
  function subagentToolLabel(block) {
2988
2957
  const name = block.name || "Tool";
2989
2958
  const input = block.input || {};
@@ -3167,10 +3136,6 @@ function finishNotifiedSubagents(session, notifications) {
3167
3136
  }
3168
3137
  }
3169
3138
  if (!running) continue;
3170
- emitSubagentLine(session, running.id, {
3171
- kind: notification.status === "completed" ? "result" : "error",
3172
- text: notification.summary || notification.status || "완료됨",
3173
- });
3174
3139
  session.subagents.delete(running.id);
3175
3140
  emitSubagents(session);
3176
3141
  }
@@ -3181,11 +3146,8 @@ function processUser(session, message) {
3181
3146
  // prompts. Re-processing their notification XML would invent a new turn.
3182
3147
  if (message.isReplay === true) return;
3183
3148
  // 자식 tool_result의 tool_use_id는 부모 세션의 것과 다른 공간이므로, 부모 흐름에
3184
- // 섞이기 전에 서브에이전트 기록으로 보낸다.
3185
- if (message.parent_tool_use_id) {
3186
- recordSubagentResult(session, message);
3187
- return;
3188
- }
3149
+ // 섞이지 않게 버린다.
3150
+ if (message.parent_tool_use_id) return;
3189
3151
  const notifications = taskNotifications(message);
3190
3152
  if (notifications.length) {
3191
3153
  if (!session.turn) {
@@ -4113,7 +4075,7 @@ async function transcriptPath(id) {
4113
4075
  * conversation. A resume can re-append earlier messages under their original
4114
4076
  * uuids, so each uuid's first appearance wins.
4115
4077
  */
4116
- function transcriptEntries(raw) {
4078
+ function transcriptEntries(raw, sidechain = false) {
4117
4079
  const messages = [];
4118
4080
  const seen = new Set();
4119
4081
  for (const line of raw.split("\n")) {
@@ -4123,7 +4085,7 @@ function transcriptEntries(raw) {
4123
4085
  } catch {
4124
4086
  continue;
4125
4087
  }
4126
- if (!entry || typeof entry !== "object" || entry.isSidechain) continue;
4088
+ if (!entry || typeof entry !== "object" || (!sidechain && entry.isSidechain)) continue;
4127
4089
  if (entry.type !== "user" && entry.type !== "assistant" && entry.type !== "system") continue;
4128
4090
  if (typeof entry.uuid === "string" && entry.uuid) {
4129
4091
  if (seen.has(entry.uuid)) continue;
@@ -4168,14 +4130,60 @@ function restoreArtifacts(id, messages) {
4168
4130
  });
4169
4131
  }
4170
4132
 
4171
- async function transcriptMessagesAt(path) {
4133
+ async function transcriptMessagesAt(path, sidechain = false) {
4172
4134
  let raw;
4173
4135
  try {
4174
4136
  raw = await readFile(path, "utf8");
4175
4137
  } catch {
4176
4138
  return [];
4177
4139
  }
4178
- return transcriptEntries(raw);
4140
+ return transcriptEntries(raw, sidechain);
4141
+ }
4142
+
4143
+ // 하위 에이전트 기록은 본 기록 옆 `<세션 id>/subagents/agent-<작업 id>.jsonl`에 쌓인다.
4144
+ // 행 id는 도구 호출 id라, 작업 id를 아직 모르거나 이미 끝난 행은 meta 파일의 toolUseId로 찾는다.
4145
+ async function subagentTranscriptPath(session, id) {
4146
+ const main = await transcriptPath(liveSessionId(session.id));
4147
+ if (!main) return null;
4148
+ const dir = join(main.slice(0, -".jsonl".length), "subagents");
4149
+ const taskId = findSubagent(session, id)?.taskId || (id.startsWith("task:") ? id.slice(5) : "");
4150
+ if (taskId) return join(dir, `agent-${taskId}.jsonl`);
4151
+ let names;
4152
+ try {
4153
+ names = await readdir(dir);
4154
+ } catch {
4155
+ return null;
4156
+ }
4157
+ for (const name of names) {
4158
+ if (!name.endsWith(".meta.json")) continue;
4159
+ try {
4160
+ const meta = JSON.parse(await readFile(join(dir, name), "utf8"));
4161
+ if (meta.toolUseId === id) return join(dir, name.replace(/\.meta\.json$/, ".jsonl"));
4162
+ } catch {}
4163
+ }
4164
+ return null;
4165
+ }
4166
+
4167
+ // 화면이 띄운 하위 에이전트 하나만 지켜본다. 기록 파일이 커질 때만 다시 읽어 turns를 보낸다.
4168
+ function watchSubagent(session, id) {
4169
+ clearInterval(session.subagentWatch?.timer);
4170
+ session.subagentWatch = null;
4171
+ if (!id) return;
4172
+ const watch = { id, size: -1 };
4173
+ session.subagentWatch = watch;
4174
+ watch.timer = setInterval(() => sendSubagentHistory(session, watch).catch(() => {}), 1000);
4175
+ watch.timer.unref?.();
4176
+ setImmediate(() => sendSubagentHistory(session, watch).catch(() => {}));
4177
+ }
4178
+
4179
+ async function sendSubagentHistory(session, watch) {
4180
+ if (lookupSession(session.id) !== session) return watchSubagent(session, null);
4181
+ const path = await subagentTranscriptPath(session, watch.id);
4182
+ const size = (path && (await stat(path).catch(() => null))?.size) ?? -1;
4183
+ if (session.subagentWatch !== watch || size === watch.size) return;
4184
+ watch.size = size;
4185
+ const turns = path ? historyTurns(await transcriptMessagesAt(path, true)) : [];
4186
+ notify("turn/subagent/history", { threadId: session.id, id: watch.id, turns });
4179
4187
  }
4180
4188
 
4181
4189
  /** Every message of `id`'s conversation: the transcript file when it exists,
@@ -4352,7 +4360,9 @@ async function dispatch(method, params = {}) {
4352
4360
  return {};
4353
4361
  }
4354
4362
  if (method === "marketplace/upgrade") {
4355
- const output = await runClaudeCommand(params, ["plugin", "marketplace", "update"], CLAUDE_INSTALL_TIMEOUT);
4363
+ const name = String(params.marketplaceName || "");
4364
+ const args = ["plugin", "marketplace", "update", ...(name ? [name] : [])];
4365
+ const output = await runClaudeCommand(params, args, CLAUDE_INSTALL_TIMEOUT);
4356
4366
  return { message: output, consideredRoots: ["Claude marketplaces"], errors: [] };
4357
4367
  }
4358
4368
  if (method === "skills/list") return claudeSkills(params);
@@ -4360,9 +4370,7 @@ async function dispatch(method, params = {}) {
4360
4370
  if (method === "account/read") {
4361
4371
  return await runClaudeJson(params, ["auth", "status", "--json"]) || {};
4362
4372
  }
4363
- if (method === "plugin/reload") {
4364
- return { message: "Claude 플러그인 설정을 다시 읽었습니다. 새 대화부터 적용됩니다." };
4365
- }
4373
+ if (method === "plugin/reload") return {};
4366
4374
  if (method === "permissions/status") return claudePermissionStatus(params);
4367
4375
  if (method === "permissions/update") return updateClaudePermission(params);
4368
4376
  if (method === "permissions/auto-mode") return setClaudeAutoModeDisabled(params);
@@ -4381,7 +4389,12 @@ async function dispatch(method, params = {}) {
4381
4389
  return { known: true, running: Boolean(session.turn), turnId: session.turn?.id ?? null };
4382
4390
  }
4383
4391
  if (method === "session/start") {
4384
- const { session, account, usage } = await createSession(params);
4392
+ // Claude Code가 뜨는 도중 가끔 충돌한다. 새 세션은 아직 쓴 것이 없고 다시 만들 때
4393
+ // 새 id를 받으므로 한 번 더 띄운다.
4394
+ const { session, account, usage } = await createSession(params).catch((error) => {
4395
+ if (!isClaudeCliCrash(error)) throw error;
4396
+ return createSession(params);
4397
+ });
4385
4398
  return {
4386
4399
  id: session.id,
4387
4400
  thread: { id: session.id, turns: [] },
@@ -4451,6 +4464,11 @@ async function dispatch(method, params = {}) {
4451
4464
  nextCursor: null,
4452
4465
  };
4453
4466
  }
4467
+ if (method === "session/watchSubagent") {
4468
+ const session = lookupSession(params.sessionId);
4469
+ if (session) watchSubagent(session, params.id ? String(params.id) : null);
4470
+ return {};
4471
+ }
4454
4472
  if (method === "session/history") {
4455
4473
  const id = liveSessionId(params.sessionId);
4456
4474
  const messages = await sessionMessages(id, params.cwd);
@@ -4802,6 +4820,17 @@ async function runCommandTimeoutSelfTest() {
4802
4820
  30_000,
4803
4821
  );
4804
4822
  if (quick !== "ok") throw new Error(`Claude command self-test returned ${quick}`);
4823
+ // 출력 뒤 실패로 끝난 읽기 명령은 한 번 더 실행해 그 결과를 쓴다.
4824
+ const marker = join(tmpdir(), `dvz-json-retry-${process.pid}`);
4825
+ const flaky = `const fs = require("fs"); const first = !fs.existsSync(${JSON.stringify(marker)});`
4826
+ + ` fs.writeFileSync(${JSON.stringify(marker)}, ""); process.stdout.write(first ? "[0]" : "[1]");`
4827
+ + " process.exitCode = first ? 1 : 0;";
4828
+ try {
4829
+ const retried = await runClaudeJson({ claudePath: process.execPath }, ["-e", flaky]);
4830
+ if (JSON.stringify(retried) !== "[1]") throw new Error(`Claude read retry self-test returned ${JSON.stringify(retried)}`);
4831
+ } finally {
4832
+ await rm(marker, { force: true });
4833
+ }
4805
4834
  }
4806
4835
 
4807
4836
  // 사유가 늘어나면 안내 없이 영어 원문만 남으므로, 설치된 SDK의 목록과 대조한다.
@@ -4885,6 +4914,33 @@ async function runSelfTest() {
4885
4914
  || JSON.stringify(restoredPrompts) !== JSON.stringify(["compact 이전 질문", "compact 이후 질문"])) {
4886
4915
  throw new Error(`Claude compacted transcript self-test failed: ${JSON.stringify({ restored: restored.length, restoredPrompts })}`);
4887
4916
  }
4917
+ // 하위 에이전트 기록 파일은 모든 줄이 sidechain이라, 그 파일을 읽을 때만 남긴다.
4918
+ const subagentPrompt = historyTurns(transcriptEntries(compactedTranscript, true))
4919
+ .flatMap((turn) => turn.items)
4920
+ .some((item) => item.type === "userMessage" && item.content?.[0]?.text === "subagent");
4921
+ if (!subagentPrompt) throw new Error("Claude subagent transcript self-test failed");
4922
+ // 끝나서 작업 id를 모르는 행도 meta 파일의 toolUseId로 그 기록을 찾는다.
4923
+ const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
4924
+ const configDir = join(tmpdir(), `devez-subagent-self-test-${randomUUID()}`);
4925
+ const agentDir = join(configDir, "projects", "D--work", "subagent-session", "subagents");
4926
+ await mkdir(agentDir, { recursive: true });
4927
+ await writeFile(join(configDir, "projects", "D--work", "subagent-session.jsonl"), "");
4928
+ await writeFile(join(agentDir, "agent-a1.meta.json"), JSON.stringify({ toolUseId: "toolu_child" }));
4929
+ await writeFile(join(agentDir, "agent-a1.jsonl"), JSON.stringify({
4930
+ type: "user", uuid: "child-1", isSidechain: true, message: { role: "user", content: "child prompt" },
4931
+ }));
4932
+ process.env.CLAUDE_CONFIG_DIR = configDir;
4933
+ try {
4934
+ const childPath = await subagentTranscriptPath({ id: "subagent-session", subagents: new Map() }, "toolu_child");
4935
+ const childTurns = childPath ? historyTurns(await transcriptMessagesAt(childPath, true)) : [];
4936
+ if (!childTurns.some((turn) => turn.items.some((item) => item.content?.[0]?.text === "child prompt"))) {
4937
+ throw new Error(`Claude subagent transcript lookup self-test failed: ${childPath}`);
4938
+ }
4939
+ } finally {
4940
+ if (previousConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
4941
+ else process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
4942
+ await rm(configDir, { recursive: true, force: true });
4943
+ }
4888
4944
  // A resumed transcript lists its Artifact pages once each at their latest
4889
4945
  // publish, read through the file's toolUseResult spelling.
4890
4946
  const artifactTranscript = [
@@ -4929,6 +4985,12 @@ async function runSelfTest() {
4929
4985
  || !isNewerVersion("2.2.0", "2.1.999")) {
4930
4986
  throw new Error("Claude installed CLI preference self-test failed");
4931
4987
  }
4988
+ // 충돌 종료만 다시 띄운다. 일반 종료 코드와 다른 실패는 그대로 알린다.
4989
+ if (!isClaudeCliCrash(new Error("Claude Code process exited with code 3221226356"))
4990
+ || isClaudeCliCrash(new Error("Claude Code process exited with code 1"))
4991
+ || isClaudeCliCrash(new Error("Claude 세션을 찾을 수 없습니다: abc"))) {
4992
+ throw new Error("Claude CLI crash retry self-test failed");
4993
+ }
4932
4994
  const latestOptions = makeOptions({ cwd: process.cwd(), permissionMode: "plan" }, "00000000-0000-4000-8000-000000000000");
4933
4995
  if (latestOptions.permissionMode !== BOOTSTRAP_PERMISSION_MODE
4934
4996
  || latestOptions.perTaskStopAffordance !== true
@@ -6271,6 +6333,18 @@ async function runSelfTest() {
6271
6333
  if (commandSession.subagents.size !== 0) {
6272
6334
  throw new Error(`Claude foreground row outlived its tool result: ${JSON.stringify([...commandSession.subagents])}`);
6273
6335
  }
6336
+ // 행은 순정처럼 name(없으면 종류)과 description을 함께 싣는다.
6337
+ for (const [input, name] of [
6338
+ [{ name: "auth_review", description: "인증 흐름 조사", subagent_type: "Explore" }, "auth_review"],
6339
+ [{ description: "인증 흐름 조사", subagent_type: "Explore" }, "Explore"],
6340
+ ]) {
6341
+ startSubagent(commandSession, { id: "toolu_named", input });
6342
+ const row = findSubagent(commandSession, "toolu_named");
6343
+ if (row?.name !== name || row.description !== "인증 흐름 조사") {
6344
+ throw new Error(`Claude subagent row name self-test failed: ${JSON.stringify(row)}`);
6345
+ }
6346
+ commandSession.subagents.delete("toolu_named");
6347
+ }
6274
6348
  // 클라우드로 띄운 원격 Agent도 접수 결과에서 줄이 남고, 종료 통지가 거둔다.
6275
6349
  commandSession.tools.set("toolu_remote", { name: "Agent", input: { name: "cloud_review" }, item: { id: "toolu_remote" } });
6276
6350
  startSubagent(commandSession, { id: "toolu_remote", input: { name: "cloud_review" } });
@@ -6498,9 +6572,7 @@ async function runSelfTest() {
6498
6572
  && event.params.item.text === "automatic summary one")
6499
6573
  || !lifecycleEvents.some((event) => event.method === "item/completed"
6500
6574
  && event.params?.item?.type === "agentMessage"
6501
- && event.params.item.text === "automatic summary two")
6502
- || !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
6503
- && event.params?.line?.kind === "error")) {
6575
+ && event.params.item.text === "automatic summary two")) {
6504
6576
  throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
6505
6577
  }
6506
6578
  const openingSession = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.9.52",
3
+ "version": "1.9.54",
4
4
  "type": "module",
5
5
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
6
6
  "keywords": [