devez-vibe 1.9.23 → 1.9.25

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
@@ -39,7 +39,10 @@ const CLAUDE_PROVIDER_SKILLS = new Set([
39
39
  // Built-in commands Claude Code runs only from the user's own message, never
40
40
  // from the model (disableModelInvocation), so they go in as typed `/name` text.
41
41
  const CLAUDE_USER_ONLY_COMMANDS = new Set(["team-onboarding"]);
42
- const OPUS_48_MODEL = "claude-opus-4-8";
42
+ // Claude Code's own safeguard fallback (opus5). The host keeps it for that
43
+ // switch but leaves it out of /model; Opus 4.8 is no longer offered.
44
+ const PREVIOUS_OPUS_MODEL = "claude-opus-5";
45
+ const RETIRED_OPUS_MODEL = "claude-opus-4-8";
43
46
  const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
44
47
  const CLAUDE_TASK_TOOLS = ["TaskCreate", "TaskGet", "TaskUpdate", "TaskList"];
45
48
  let nextHostRequest = 1;
@@ -230,8 +233,8 @@ function modelCapabilities(models, model) {
230
233
  const capabilities = models.find((candidate) =>
231
234
  candidate.value === value || candidate.resolvedModel === value)
232
235
  || (!value ? models.find((candidate) => candidate.value === "default") : undefined);
233
- return value === OPUS_48_MODEL
234
- ? opus48Capabilities(models, capabilities)
236
+ return value === PREVIOUS_OPUS_MODEL
237
+ ? previousOpusCapabilities(models, capabilities)
235
238
  : capabilities;
236
239
  }
237
240
 
@@ -251,10 +254,10 @@ function familyCapabilities(models, model) {
251
254
  .some((name) => String(name || "").toLowerCase().includes(family)));
252
255
  }
253
256
 
254
- function opus48Capabilities(models, existing = {}) {
257
+ function previousOpusCapabilities(models, existing = {}) {
255
258
  // The CLI now exposes the Opus alias as `opus[1m]` (and only `default`
256
259
  // resolves to it), so an exact `value === "opus"` match finds nothing and the
257
- // synthesized Opus 4.8 row loses supportsAutoMode/supportsFastMode. Match the
260
+ // synthesized Opus 5 row loses supportsAutoMode/supportsFastMode. Match the
258
261
  // Opus family instead, falling back to `default`.
259
262
  const opus = models.find((model) => String(model.value || "").toLowerCase().startsWith("opus"))
260
263
  || models.find((model) => model.value === "default")
@@ -271,9 +274,9 @@ function opus48Capabilities(models, existing = {}) {
271
274
  return {
272
275
  ...opus,
273
276
  ...existing,
274
- value: OPUS_48_MODEL,
275
- resolvedModel: OPUS_48_MODEL,
276
- displayName: "Opus 4.8",
277
+ value: PREVIOUS_OPUS_MODEL,
278
+ resolvedModel: PREVIOUS_OPUS_MODEL,
279
+ displayName: "Opus 5",
277
280
  supportsEffort: true,
278
281
  supportedEffortLevels,
279
282
  };
@@ -328,22 +331,23 @@ function catalogEntry(model, defaultResolvedModel) {
328
331
  }
329
332
 
330
333
  function claudeCatalogEntries(models, defaultResolvedModel) {
331
- const catalogModels = models.filter((model) => model.value && model.value !== "default");
334
+ const catalogModels = models.filter((model) => model.value && model.value !== "default"
335
+ && ![model.value, model.resolvedModel]
336
+ .some((name) => stripClaudeModel(String(name || "")).startsWith(RETIRED_OPUS_MODEL)));
332
337
  const entries = catalogModels.map((model) => catalogEntry(model, defaultResolvedModel));
333
338
  const existingIndex = entries.findIndex((entry) =>
334
- stripClaudeModel(entry.model) === OPUS_48_MODEL
335
- || stripClaudeModel(entry.id) === OPUS_48_MODEL
336
- || entry.displayName === "Opus 4.8");
339
+ stripClaudeModel(entry.model) === PREVIOUS_OPUS_MODEL
340
+ || stripClaudeModel(entry.id) === PREVIOUS_OPUS_MODEL);
337
341
  const existing = existingIndex >= 0 ? catalogModels[existingIndex] : {};
338
342
  if (existingIndex >= 0) entries.splice(existingIndex, 1);
339
- const opus48 = catalogEntry(opus48Capabilities(models, existing), defaultResolvedModel);
340
- const opus5Index = entries.findIndex((entry) => entry.displayName === "Opus 5");
341
- const opusIndex = opus5Index >= 0
342
- ? opus5Index
343
- : entries.findIndex((entry) => /\bopus\b/i.test(entry.displayName));
343
+ const previousOpus = {
344
+ ...catalogEntry(previousOpusCapabilities(models, existing), defaultResolvedModel),
345
+ hidden: true,
346
+ };
347
+ const opusIndex = entries.findIndex((entry) => /\bopus\b/i.test(entry.displayName));
344
348
  const fableIndex = entries.findIndex((entry) => /\bfable\b/i.test(entry.displayName));
345
349
  const insertAfter = opusIndex >= 0 ? opusIndex : fableIndex;
346
- entries.splice(insertAfter + 1, 0, opus48);
350
+ entries.splice(insertAfter + 1, 0, previousOpus);
347
351
  return entries;
348
352
  }
349
353
 
@@ -830,13 +834,13 @@ function permissionSuggestionLabel(suggestions) {
830
834
  const values = [...new Set([...rules, ...directories].filter(Boolean))];
831
835
  const destinations = new Set(suggestions.map((suggestion) => suggestion.destination));
832
836
  const scope = destinations.has("userSettings")
833
- ? "모든 프로젝트에서"
837
+ ? "in all projects"
834
838
  : destinations.has("projectSettings") || destinations.has("localSettings")
835
- ? "이 프로젝트에서"
836
- : "이번 세션에서";
839
+ ? "in this project"
840
+ : "for this session";
837
841
  return values.length
838
- ? `${scope} 항상 허용: ${values.join(", ")}`
839
- : `${scope} 다시 묻지 않기`;
842
+ ? `Always allow ${scope}: ${values.join(", ")}`
843
+ : `Don't ask again ${scope}`;
840
844
  }
841
845
 
842
846
  async function requestToolPermission(toolName, input, permission) {
@@ -1486,25 +1490,66 @@ function isKoreanPrompt(input) {
1486
1490
  .filter((item) => item?.type === "text")
1487
1491
  .map((item) => String(item.text || ""))
1488
1492
  .join("\n");
1489
- return /[\uac00-\ud7a3]/.test(prompt);
1493
+ // Jamo too: a bare `\u3147\u314b` reply is still a Korean turn.
1494
+ return /[\uac00-\ud7a3\u3131-\u318e]/.test(prompt);
1490
1495
  }
1491
1496
 
1492
1497
  // `Now the tile view logic.` carries nothing a Korean reader needs, and the
1493
1498
  // stand-in that used to replace it carried even less — the same sentence before
1494
1499
  // every tool call, however many calls the turn made. Drop the line instead; the
1495
- // tool item that follows already names what is being read.
1496
- function normalizeProgressText(turn, text) {
1500
+ // tool item that follows already names what is being read. Any other English
1501
+ // one-liner goes too once a tool call is known to follow it
1502
+ // (`Bundle is ready; running the upload script.`).
1503
+ function normalizeProgressText(turn, text, beforeTool = false) {
1497
1504
  const value = String(text || "");
1498
1505
  const trimmed = value.trim();
1499
- if (turn?.koreanRequest
1500
- && trimmed.length <= 160
1501
- && !/[\uac00-\ud7a3]/.test(trimmed)
1502
- && /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
1506
+ if (isEnglishProgressLine(turn, trimmed)
1507
+ && (beforeTool || /^Now\b/i.test(trimmed))) {
1503
1508
  return "";
1504
1509
  }
1505
1510
  return value;
1506
1511
  }
1507
1512
 
1513
+ function isEnglishProgressLine(turn, text) {
1514
+ const trimmed = String(text || "").trim();
1515
+ return Boolean(turn?.koreanRequest)
1516
+ && trimmed.length <= 160
1517
+ && !/[\r\n]/.test(trimmed)
1518
+ && !/[\uac00-\ud7a3]/.test(trimmed);
1519
+ }
1520
+
1521
+ // A finished English line waits for the next block: a tool call drops it,
1522
+ // anything else shows it as written.
1523
+ function releaseHeldProgress(session, dropped) {
1524
+ const held = session.heldProgress;
1525
+ session.heldProgress = null;
1526
+ if (!held || dropped) return;
1527
+ emitHeldStart(session, held);
1528
+ held.emit(held.text);
1529
+ emitItem(session, "completed", { id: held.id, type: "agentMessage", text: held.text, provider: "Claude" });
1530
+ }
1531
+
1532
+ // Claude Opus 5.5 and Fable 5.1 send the notes they write between tool calls as
1533
+ // progress-update thinking blocks, and the CLI asks for their short summaries.
1534
+ // They are the reply's own words rather than reasoning, so they go out as text:
1535
+ // shown as reasoning, Super Vibe dropped them and the turn went silent before a
1536
+ // question.
1537
+ const PROGRESS_UPDATE_MODEL = /claude-(?:opus-5-5|fable-5-1|mythos-5-1)/;
1538
+
1539
+ function isProgressUpdateModel(model) {
1540
+ return PROGRESS_UPDATE_MODEL.test(String(model || ""));
1541
+ }
1542
+
1543
+ function emitProgressUpdate(session, text) {
1544
+ const visible = normalizeProgressText(session.turn, text, true);
1545
+ if (!visible.trim()) return;
1546
+ const id = nextItemId(session, "text");
1547
+ emitItem(session, "started", { id, type: "agentMessage", text: "", provider: "Claude" });
1548
+ emitDelta(session, "item/agentMessage/delta", id, visible);
1549
+ emitItem(session, "completed", { id, type: "agentMessage", text: visible, provider: "Claude" });
1550
+ session.turn.sawVisibleText = true;
1551
+ }
1552
+
1508
1553
  // Text pacing lives in the host renderer, not here. A timer in this process
1509
1554
  // runs on Node's scheduler, which cannot line up with the terminal's redraw
1510
1555
  // rhythm — the mismatch was itself the visible stutter. Forward each delta as
@@ -1565,11 +1610,14 @@ function emitHeldStart(session, current) {
1565
1610
  async function processStreamEvent(session, message) {
1566
1611
  if (!session.turn || message.parent_tool_use_id) return;
1567
1612
  const event = message.event || {};
1613
+ if (event.type === "message_start" || event.type === "message_stop") releaseHeldProgress(session);
1568
1614
  if (event.type === "message_start") {
1569
1615
  session.streamBlocks.clear();
1616
+ session.streamModel = event.message?.model || session.model;
1570
1617
  }
1571
1618
  if (event.type === "content_block_start") {
1572
1619
  const block = event.content_block || {};
1620
+ releaseHeldProgress(session, block.type === "tool_use" || block.type === "server_tool_use");
1573
1621
  if (block.type !== "text" && block.type !== "thinking") return;
1574
1622
  const id = nextItemId(session, block.type);
1575
1623
  const item = block.type === "text"
@@ -1586,10 +1634,10 @@ async function processStreamEvent(session, message) {
1586
1634
  session.streamBlocks.set(event.index, {
1587
1635
  id,
1588
1636
  type: block.type,
1637
+ progress: block.type === "thinking" && isProgressUpdateModel(session.streamModel),
1589
1638
  text: "",
1590
1639
  emit,
1591
1640
  languagePending: held ? "" : null,
1592
- holdEnglishProgress: false,
1593
1641
  pendingStart: held ? item : null,
1594
1642
  });
1595
1643
  if (!held) emitItem(session, "started", item);
@@ -1606,20 +1654,14 @@ async function processStreamEvent(session, message) {
1606
1654
  if (current.type === "text" && current.languagePending == null) session.turn.sawVisibleText = true;
1607
1655
  if (current.languagePending != null) {
1608
1656
  current.languagePending += delta;
1609
- const probe = current.languagePending.trimStart();
1610
- const lower = probe.toLowerCase();
1611
- if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
1612
- if (/^now(?:\s|$)/i.test(probe)) {
1613
- current.holdEnglishProgress = true;
1614
- return;
1615
- }
1657
+ if (isEnglishProgressLine(session.turn, current.languagePending)) return;
1616
1658
  emitHeldStart(session, current);
1617
1659
  session.turn.sawVisibleText = true;
1618
1660
  current.emit(current.languagePending);
1619
1661
  current.languagePending = null;
1620
1662
  return;
1621
1663
  }
1622
- current.emit(delta);
1664
+ if (!current.progress) current.emit(delta);
1623
1665
  return;
1624
1666
  }
1625
1667
  if (event.type === "content_block_stop") {
@@ -1633,6 +1675,12 @@ async function processStreamEvent(session, message) {
1633
1675
  session.streamBlocks.delete(event.index);
1634
1676
  return;
1635
1677
  }
1678
+ if (current.pendingStart && isEnglishProgressLine(session.turn, visible)) {
1679
+ releaseHeldProgress(session);
1680
+ session.heldProgress = current;
1681
+ session.streamBlocks.delete(event.index);
1682
+ return;
1683
+ }
1636
1684
  emitHeldStart(session, current);
1637
1685
  if (visible.trim()) session.turn.sawVisibleText = true;
1638
1686
  current.emit(visible);
@@ -1640,9 +1688,10 @@ async function processStreamEvent(session, message) {
1640
1688
  emitHeldStart(session, current);
1641
1689
  const item = current.type === "text"
1642
1690
  ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
1643
- : { id: current.id, type: "reasoning", summary: [current.text] };
1691
+ : { id: current.id, type: "reasoning", summary: [current.progress ? "" : current.text] };
1644
1692
  emitItem(session, "completed", item);
1645
1693
  session.streamBlocks.delete(event.index);
1694
+ if (current.progress) emitProgressUpdate(session, current.text);
1646
1695
  }
1647
1696
  }
1648
1697
 
@@ -1687,10 +1736,14 @@ function processAssistant(session, message) {
1687
1736
  // Without partial SDK events, replay completed text before tool items so the
1688
1737
  // visible order still matches the assistant content order.
1689
1738
  if (!session.streamBlocks.size && !session.turn.sawStreamText) {
1690
- for (const block of content) {
1739
+ for (const [index, block] of content.entries()) {
1691
1740
  if (block.type !== "text" && block.type !== "thinking") continue;
1741
+ if (block.type === "thinking" && isProgressUpdateModel(message.message?.model)) {
1742
+ emitProgressUpdate(session, block.thinking);
1743
+ continue;
1744
+ }
1692
1745
  const visible = block.type === "text"
1693
- ? normalizeProgressText(session.turn, block.text)
1746
+ ? normalizeProgressText(session.turn, block.text, content[index + 1]?.type === "tool_use")
1694
1747
  : "";
1695
1748
  if (block.type === "text" && !visible.trim() && String(block.text || "").trim()) continue;
1696
1749
  const id = nextItemId(session, block.type);
@@ -2700,6 +2753,23 @@ const STARTUP_FAILURE_GUIDES = {
2700
2753
  bypass_root: "관리자 계정에서는 권한 우회 모드로 시작할 수 없습니다. 일반 계정으로 실행하세요.",
2701
2754
  };
2702
2755
 
2756
+ /**
2757
+ * A turn can end without the error flag yet still carry no answer: the model
2758
+ * refused, or the response was cut off. Those slip by silently unless surfaced.
2759
+ * Refusal is reported as an error so the host opens its model-switch card on the
2760
+ * `(refusal)` marker; anything else is a plain notice. Normal stops give null.
2761
+ */
2762
+ function abnormalStopOutcome(stopReason) {
2763
+ if (!stopReason || ["end_turn", "tool_use", "stop_sequence"].includes(stopReason)) return null;
2764
+ if (stopReason === "refusal") {
2765
+ return { error: "Claude declined to answer this request. (refusal)" };
2766
+ }
2767
+ if (stopReason === "max_tokens") {
2768
+ return { warning: "The response was cut off at its maximum length. (max_tokens)" };
2769
+ }
2770
+ return { warning: `The response ended unexpectedly. (${stopReason})` };
2771
+ }
2772
+
2703
2773
  async function processResult(session, message) {
2704
2774
  if (!session.turn) return;
2705
2775
  for (const denial of Array.isArray(message.permission_denials) ? message.permission_denials : []) {
@@ -2732,9 +2802,27 @@ async function processResult(session, message) {
2732
2802
  });
2733
2803
  const detail = message.errors?.join("\n") || message.result || message.stop_reason || "Claude 실행 실패";
2734
2804
  const guide = STARTUP_FAILURE_GUIDES[message.startup_failure_reason];
2735
- const error = message.is_error && !interrupted
2805
+ let error = message.is_error && !interrupted
2736
2806
  ? { message: guide ? `${guide}\n${detail}` : detail }
2737
2807
  : null;
2808
+ if (!error && !interrupted) {
2809
+ const outcome = abnormalStopOutcome(message.stop_reason);
2810
+ if (outcome?.error) error = { message: outcome.error };
2811
+ else if (outcome?.warning) {
2812
+ notify("warning", { threadId: session.id, provider: "Claude", message: outcome.warning });
2813
+ }
2814
+ }
2815
+ // An assistant-level error is otherwise kept only in turn state (for retry
2816
+ // decisions) and never shown. Surface it when the result itself carried no
2817
+ // error, so it cannot double with the result error above. Verification was
2818
+ // already announced when the assistant message arrived.
2819
+ const assistantError = session.turn?.assistantError;
2820
+ if (!error && assistantError && assistantError !== "verification_required") {
2821
+ const text = typeof assistantError === "string"
2822
+ ? assistantError
2823
+ : assistantError.message || JSON.stringify(assistantError);
2824
+ notify("warning", { threadId: session.id, provider: "Claude", message: `Claude response error: ${text}` });
2825
+ }
2738
2826
  if (error && waitForUsageLimit(session)) return;
2739
2827
  finishTurn(session, error, message.duration_ms);
2740
2828
  notify("claude/account/updated", {
@@ -2810,6 +2898,7 @@ async function runPendingPrompt(session) {
2810
2898
  function finishTurn(session, error, durationMs) {
2811
2899
  clearUsageLimitWait(session);
2812
2900
  if (!session.turn) return;
2901
+ releaseHeldProgress(session);
2813
2902
  flushPendingPlan(session);
2814
2903
  clearForegroundSubagents(session);
2815
2904
  const turn = { id: session.turn.id, status: error ? "failed" : session.turn.interruptRequested ? "interrupted" : "completed" };
@@ -2870,10 +2959,18 @@ async function consumeMessage(session, message) {
2870
2959
  noteCompactBoundary(session, message.compact_metadata);
2871
2960
  notify("thread/compacted", { threadId: session.id });
2872
2961
  } else if (message.type === "system" && message.subtype === "permission_denied") {
2962
+ const reason = message.decision_reason || message.decision_reason_type;
2873
2963
  rememberPermissionDenial(session, {
2874
2964
  tool: message.tool_name,
2875
2965
  toolUseId: message.tool_use_id,
2876
- reason: message.decision_reason || message.decision_reason_type,
2966
+ reason,
2967
+ });
2968
+ // The /permissions panel keeps the record, but a denial mid-turn otherwise
2969
+ // leaves the screen silent. Show it inline too.
2970
+ notify("warning", {
2971
+ threadId: session.id,
2972
+ provider: "Claude",
2973
+ message: `${message.tool_name || "Tool"} was denied${reason ? `: ${reason}` : "."}`,
2877
2974
  });
2878
2975
  } else if (message.type === "rate_limit_event") {
2879
2976
  if (session.turn) session.turn.rateLimitInfo = message.rate_limit_info;
@@ -3042,13 +3139,17 @@ async function runPrompt(session, params) {
3042
3139
 
3043
3140
  // A background task notification is an internal user message that starts its
3044
3141
  // own Claude response even though the host did not submit a new prompt.
3045
- function beginTurn(session, input = []) {
3142
+ function beginTurn(session, input) {
3046
3143
  const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
3144
+ const koreanRequest = input === undefined
3145
+ ? Boolean(session.lastKoreanRequest)
3146
+ : isKoreanPrompt(input);
3147
+ session.lastKoreanRequest = koreanRequest;
3047
3148
  session.turn = {
3048
3149
  id: turnId,
3049
3150
  sawStreamText: false,
3050
3151
  sawVisibleText: false,
3051
- koreanRequest: isKoreanPrompt(input),
3152
+ koreanRequest,
3052
3153
  };
3053
3154
  session.lastContextUsage = null;
3054
3155
  notify("turn/started", { threadId: session.id, turn: { id: turnId } });
@@ -3168,8 +3269,14 @@ function historyState(messages) {
3168
3269
  if (!visible.trim() && String(block.text || "").trim()) continue;
3169
3270
  turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: visible, provider: "Claude" });
3170
3271
  }
3272
+ else if (block.type === "thinking" && isProgressUpdateModel(message.message?.model)) {
3273
+ const visible = normalizeProgressText(historyTurn, block.thinking, true);
3274
+ if (visible.trim()) turn.items.push({ id: `${message.uuid}-thinking`, type: "agentMessage", text: visible, provider: "Claude" });
3275
+ }
3171
3276
  else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
3172
3277
  else if (block.type === "tool_use") {
3278
+ const last = turn.items.at(-1);
3279
+ if (last?.type === "agentMessage" && !normalizeProgressText(historyTurn, last.text, true).trim()) turn.items.pop();
3173
3280
  const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
3174
3281
  tools.set(block.id, pending);
3175
3282
  if (block.name === "TaskCreate") {
@@ -3915,6 +4022,18 @@ async function runSelfTest() {
3915
4022
  await runPermissionModeSelfTest();
3916
4023
  runToolPolicySelfTest();
3917
4024
  await runCommandTimeoutSelfTest();
4025
+ // A turn that ends with no answer must reach the user: refusal as an error
4026
+ // carrying the marker the host's switch card keys off, the rest as a notice.
4027
+ const refusalStop = abnormalStopOutcome("refusal");
4028
+ const truncatedStop = abnormalStopOutcome("max_tokens");
4029
+ const unknownStop = abnormalStopOutcome("model_context_window_exceeded");
4030
+ if (abnormalStopOutcome("end_turn") !== null
4031
+ || abnormalStopOutcome(undefined) !== null
4032
+ || !refusalStop?.error?.includes("(refusal)")
4033
+ || !truncatedStop?.warning?.includes("max_tokens")
4034
+ || !unknownStop?.warning?.includes("model_context_window_exceeded")) {
4035
+ throw new Error(`Claude abnormal stop notice self-test failed: ${JSON.stringify({ refusalStop, truncatedStop, unknownStop })}`);
4036
+ }
3918
4037
  const planUsage = { rate_limits: { five_hour: { utilization: 25 } }, behaviors: null };
3919
4038
  let usageOptions;
3920
4039
  const fetchedUsage = await safeUsage({
@@ -4195,6 +4314,26 @@ async function runSelfTest() {
4195
4314
  || englishNowText !== "Now the answer starts.") {
4196
4315
  throw new Error(`Claude resumed progress normalization self-test failed: ${JSON.stringify({ resumedProgressText, englishNowText })}`);
4197
4316
  }
4317
+ const jamoProgress = historyTurns([
4318
+ user("jamo-user", "ㅇㅋ"),
4319
+ {
4320
+ type: "assistant",
4321
+ uuid: "jamo-assistant",
4322
+ message: {
4323
+ role: "assistant",
4324
+ model: "claude-opus-5",
4325
+ content: [
4326
+ { type: "text", text: "Bundle is ready; running the upload script." },
4327
+ { type: "tool_use", id: "jamo-run", name: "PowerShell", input: { command: "upload" } },
4328
+ ],
4329
+ },
4330
+ },
4331
+ taskResult("jamo-result", "jamo-run", { content: "ok" }, "ok"),
4332
+ assistant("jamo-final", "claude-opus-5", "Uploaded v1.2.74."),
4333
+ ])[0]?.items.filter((item) => item.type === "agentMessage").map((item) => item.text);
4334
+ if (JSON.stringify(jamoProgress) !== JSON.stringify(["Uploaded v1.2.74."])) {
4335
+ throw new Error(`Claude English line before tool self-test failed: ${JSON.stringify(jamoProgress)}`);
4336
+ }
4198
4337
  const taskMessages = [user("plan", "작업을 진행해")];
4199
4338
  for (let index = 1; index <= 6; index++) {
4200
4339
  const id = String(24 + index);
@@ -4366,29 +4505,33 @@ async function runSelfTest() {
4366
4505
  const catalogModels = [
4367
4506
  {
4368
4507
  value: "opus[1m]",
4369
- resolvedModel: "claude-opus-5[1m]",
4370
- displayName: "Opus 5",
4508
+ resolvedModel: "claude-opus-5-5[1m]",
4509
+ displayName: "Opus 5.5",
4371
4510
  supportsEffort: true,
4372
4511
  supportedEffortLevels: ["high", "max"],
4373
4512
  supportsAutoMode: true,
4374
4513
  },
4375
4514
  { value: "sonnet", resolvedModel: "claude-sonnet-5", displayName: "Sonnet 5" },
4376
4515
  {
4377
- value: "claude-opus-4-8",
4378
- resolvedModel: "claude-opus-4-8",
4379
- displayName: "Opus 4.8",
4516
+ value: "claude-opus-5",
4517
+ resolvedModel: "claude-opus-5",
4518
+ displayName: "Opus 5",
4380
4519
  supportsEffort: false,
4381
4520
  supportedEffortLevels: [],
4382
4521
  },
4522
+ { value: "claude-opus-4-8", resolvedModel: "claude-opus-4-8", displayName: "Opus 4.8" },
4383
4523
  ];
4384
4524
  const catalog = claudeCatalogEntries(catalogModels, "claude-sonnet-5");
4385
- if (catalog[0]?.displayName !== "Opus 5"
4386
- || catalog[1]?.displayName !== "Opus 4.8"
4387
- || catalog[1]?.model !== "claude:claude-opus-4-8"
4525
+ if (catalog.length !== 3
4526
+ || catalog[0]?.displayName !== "Opus 5.5"
4527
+ || catalog[1]?.displayName !== "Opus 5"
4528
+ || catalog[1]?.model !== "claude:claude-opus-5"
4529
+ || catalog[1]?.hidden !== true
4530
+ || catalog.some((entry) => entry.displayName === "Opus 4.8")
4388
4531
  || catalog[1]?.supportsAutoMode !== true
4389
4532
  || catalog[1]?.supportedReasoningEfforts?.[1]?.reasoningEffort !== "max"
4390
4533
  || supportedEffort(
4391
- modelCapabilities(catalogModels, "claude:claude-opus-4-8"),
4534
+ modelCapabilities(catalogModels, "claude:claude-opus-5"),
4392
4535
  "max",
4393
4536
  ) !== "max") {
4394
4537
  throw new Error(`Claude pinned model self-test failed: ${JSON.stringify(catalog)}`);
@@ -5282,6 +5425,30 @@ async function runSelfTest() {
5282
5425
  if (openingMessageIndex >= 0 || openingToolIndex < 0) {
5283
5426
  throw new Error(`Claude tool-first turn self-test failed: ${JSON.stringify(openingEvents)}`);
5284
5427
  }
5428
+ const continuationSession = {
5429
+ ...openingSession,
5430
+ turn: null,
5431
+ turnSequence: 1,
5432
+ automaticTurnsPending: 0,
5433
+ steerPending: 0,
5434
+ streamBlocks: new Map(),
5435
+ };
5436
+ beginTurn(continuationSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
5437
+ continuationSession.turn = null;
5438
+ continuationSession.automaticTurnsPending = 1;
5439
+ beginUntrackedTurn(continuationSession, { type: "assistant", parent_tool_use_id: null });
5440
+ if (!continuationSession.turn?.koreanRequest
5441
+ || normalizeProgressText(continuationSession.turn, "Now continue the implementation.") !== "") {
5442
+ throw new Error("Claude automatic turn lost Korean progress filtering");
5443
+ }
5444
+ beginTurn(continuationSession, [{ type: "text", text: "Update the provider menu" }]);
5445
+ continuationSession.turn = null;
5446
+ continuationSession.automaticTurnsPending = 1;
5447
+ beginUntrackedTurn(continuationSession, { type: "assistant", parent_tool_use_id: null });
5448
+ if (continuationSession.turn?.koreanRequest
5449
+ || normalizeProgressText(continuationSession.turn, "Now continue the implementation.") === "") {
5450
+ throw new Error("Claude automatic turn changed English progress filtering");
5451
+ }
5285
5452
  const contextCaptured = [];
5286
5453
  process.stdout.write = (chunk) => {
5287
5454
  contextCaptured.push(String(chunk));
@@ -5406,6 +5573,58 @@ async function runSelfTest() {
5406
5573
  if (keptStarted !== 0 || keptCompleted?.params?.item?.text !== "타일 보기 로직을 고쳤습니다.") {
5407
5574
  throw new Error(`Claude held Korean text self-test failed: ${JSON.stringify(keptEvents)}`);
5408
5575
  }
5576
+ const englishLineEvents = async (next) => {
5577
+ const captured = [];
5578
+ process.stdout.write = (chunk) => {
5579
+ captured.push(String(chunk));
5580
+ return true;
5581
+ };
5582
+ try {
5583
+ openingSession.streamBlocks.clear();
5584
+ await processStreamEvent(openingSession, {
5585
+ event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
5586
+ });
5587
+ await processStreamEvent(openingSession, {
5588
+ event: { type: "content_block_delta", index: 0, delta: { text: "Bundle is ready; running the upload script." } },
5589
+ });
5590
+ await processStreamEvent(openingSession, { event: { type: "content_block_stop", index: 0 } });
5591
+ await processStreamEvent(openingSession, { event: next });
5592
+ } finally {
5593
+ process.stdout.write = stdoutWrite;
5594
+ }
5595
+ return captured.join("").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5596
+ };
5597
+ const beforeTool = await englishLineEvents({ type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "t", name: "Read" } });
5598
+ const atEnd = await englishLineEvents({ type: "message_stop" });
5599
+ const endText = atEnd.find((event) => event.method === "item/completed")?.params?.item?.text;
5600
+ if (beforeTool.length || endText !== "Bundle is ready; running the upload script.") {
5601
+ throw new Error(`Claude English progress line self-test failed: ${JSON.stringify({ beforeTool, atEnd })}`);
5602
+ }
5603
+ const progressEvents = [];
5604
+ process.stdout.write = (chunk) => {
5605
+ progressEvents.push(String(chunk));
5606
+ return true;
5607
+ };
5608
+ try {
5609
+ openingSession.streamBlocks.clear();
5610
+ for (const event of [
5611
+ { type: "message_start", message: { model: "claude-opus-5-5" } },
5612
+ { type: "content_block_start", index: 0, content_block: { type: "thinking" } },
5613
+ { type: "content_block_delta", index: 0, delta: { thinking: "노트 초안 6항목을 정리했습니다." } },
5614
+ { type: "content_block_stop", index: 0 },
5615
+ { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "q", name: "AskUserQuestion" } },
5616
+ ]) await processStreamEvent(openingSession, { event });
5617
+ } finally {
5618
+ process.stdout.write = stdoutWrite;
5619
+ openingSession.streamModel = undefined;
5620
+ }
5621
+ const progressItems = progressEvents.join("").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line))
5622
+ .filter((event) => event.method === "item/completed").map((event) => event.params.item);
5623
+ if (progressItems.find((item) => item.type === "reasoning")?.summary?.join("")
5624
+ || progressItems.find((item) => item.type === "agentMessage")?.text !== "노트 초안 6항목을 정리했습니다.") {
5625
+ throw new Error(`Claude progress update self-test failed: ${JSON.stringify(progressItems)}`);
5626
+ }
5627
+ if (!isKoreanPrompt([{ type: "text", text: "ㅇㅋ" }])) throw new Error("Claude jamo prompt self-test failed");
5409
5628
  const explicitSkillContent = await inputContent([
5410
5629
  { type: "text", text: "$debug investigate" },
5411
5630
  { type: "skill", name: "debug", path: "claude-command://debug" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.9.23",
3
+ "version": "1.9.25",
4
4
  "type": "module",
5
5
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
6
6
  "keywords": [
@@ -45,6 +45,6 @@
45
45
  "postinstall": "node install-skills.mjs && node prepare-search-deps.mjs"
46
46
  },
47
47
  "dependencies": {
48
- "@anthropic-ai/claude-agent-sdk": "0.3.276"
48
+ "@anthropic-ai/claude-agent-sdk": "0.3.280"
49
49
  }
50
50
  }