@buzzni/saycode-cli 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -2
  2. package/index.mjs +1400 -197
  3. package/package.json +1 -1
package/index.mjs CHANGED
@@ -40,9 +40,9 @@ function _mergeNamespaces(n, m) {
40
40
  }
41
41
  return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
42
42
  }
43
- const version = "0.3.3";
43
+ const version$1 = "0.4.0";
44
44
  const manifest = {
45
- version
45
+ version: version$1
46
46
  };
47
47
  const AGENT_CONTROL_VERBS = ["steer", "stop"];
48
48
  function parseAgentControlArgs(verb, target, rest, hasPositionalTarget) {
@@ -75,12 +75,23 @@ function parseAgentArgs(flags, positionals) {
75
75
  case "whoami":
76
76
  return { command: "agent", verb: "whoami" };
77
77
  case "ls":
78
- return { command: "agent", verb: "ls" };
78
+ return { command: "agent", verb: "ls", ...flags.status ? { status: true } : {} };
79
79
  case "read": {
80
80
  if (!target) return fail$1("agent read needs a session id (see `saycode agent ls`)");
81
- return { command: "agent", verb: "read", sessionId: target, ...flags.last !== void 0 ? { last: flags.last } : {} };
81
+ return {
82
+ command: "agent",
83
+ verb: "read",
84
+ sessionId: target,
85
+ ...flags.last !== void 0 ? { last: flags.last } : {},
86
+ ...flags.since !== void 0 ? { since: flags.since } : {}
87
+ };
82
88
  }
83
89
  case "wait": {
90
+ if (flags.any) {
91
+ if (rest.length < 2) return fail$1("agent wait --any needs two or more session ids");
92
+ if (!flags.until) return fail$1("agent wait needs --until turn-end|waiting-input|completed");
93
+ return { command: "agent", verb: "wait", sessionIds: [...rest], until: flags.until, ...timeout };
94
+ }
84
95
  if (!target) return fail$1("agent wait needs a session id (see `saycode agent ls`)");
85
96
  if (!flags.until) return fail$1("agent wait needs --until turn-end|waiting-input|completed");
86
97
  return { command: "agent", verb: "wait", sessionId: target, until: flags.until, ...timeout };
@@ -316,7 +327,7 @@ const agentBoardKo = {
316
327
  "agentBoard.columnReviewHint": "카드를 놓으면 리뷰를 요청합니다",
317
328
  "agentBoard.columnDone": "완료",
318
329
  "agentBoard.columnDoneHint": "카드를 놓으면 대화를 완료로 이동합니다",
319
- "agentBoard.columnMerged": "PR Merged",
330
+ "agentBoard.columnMerged": "PR 병합됨",
320
331
  "agentBoard.columnMergedHint": "PR이 머지된 대화",
321
332
  "agentBoard.title": "에이전트 보드",
322
333
  "agentBoard.description": "실행 중인 모든 에이전트 대화를 한눈에 보고, 카드를 옮겨 다음 작업을 지시하세요.",
@@ -370,7 +381,7 @@ const agentBoardKo = {
370
381
  "agentBoard.mergeDialogTitle": "Pull request를 머지할까요?",
371
382
  "agentBoard.mergeCopy": "PR #{prNumber}을 squash 방식으로 머지합니다. 이 작업은 GitHub 저장소에 즉시 반영됩니다.",
372
383
  "agentBoard.mergeCancel": "취소",
373
- "agentBoard.mergeConfirm": "Merge",
384
+ "agentBoard.mergeConfirm": "병합",
374
385
  "agentBoard.cardReopenTitle": "완료를 해제하고 이어서 작업합니다. 완료 시 삭제된 작업 트리는 브랜치 기준으로 복구합니다.",
375
386
  "agentBoard.cardReopen": "완료 해제",
376
387
  "agentBoard.cardCreatePrTitle": "에이전트에게 commit 및 PR 생성을 요청",
@@ -399,7 +410,7 @@ const agentBoardKo = {
399
410
  "agentBoard.findSessionError": "대화를 찾을 수 없습니다. 새로고침 후 다시 시도해주세요.",
400
411
  "agentBoard.autopilot.enable": "오토파일럿",
401
412
  "agentBoard.autopilot.unavailableWorktree": "오토파일럿 사용 불가 · worktree 필요",
402
- "agentBoard.autopilot.enableTitle": "작업이 완료되면 독립 리뷰를 시키고, 발견 0건이면 Commit & PR까지 자동 진행합니다.",
413
+ "agentBoard.autopilot.enableTitle": "작업이 완료되면 현재 에이전트에 셀프 리뷰를 시키고, medium 이상 지적이 없으면 Commit & PR까지 자동 진행합니다.",
403
414
  "agentBoard.autopilot.waiting": "오토파일럿 · 턴 종료 대기",
404
415
  "agentBoard.autopilot.reviewRequested": "오토파일럿 · 리뷰 시작 중",
405
416
  "agentBoard.autopilot.reviewing": "오토파일럿 · 독립 리뷰 중",
@@ -419,6 +430,8 @@ const agentBoardKo = {
419
430
  "agentBoard.autopilot.blocked.commitPrFailed": "오토파일럿 차단 · PR 요청 전송 실패",
420
431
  "agentBoard.autopilot.blocked.noCompletionSignal": "오토파일럿 차단 · 완료 신호 없음",
421
432
  "agentBoard.autopilot.blocked.loopDetected": "오토파일럿 차단 · 반복 감지로 중지됨",
433
+ "agentBoard.autopilot.blocked.qualityGateFailed": "품질 게이트 실패 · 같은 세션에서 수정 중",
434
+ "agentBoard.autopilot.blocked.qualityGateBlocked": "오토파일럿 차단 · 품질 게이트 실패",
422
435
  "agentBoard.autopilot.loopStopFailed": "반복을 감지했지만 세션 중지에 실패했습니다: {detail}",
423
436
  "agentBoard.autopilot.forceProceed": "그래도 진행",
424
437
  "agentBoard.autopilot.forceProceedTitle": "리뷰어가 완료를 명시적으로 선언하지 않았습니다. 그래도 Commit & PR을 진행합니다.",
@@ -432,7 +445,12 @@ const agentBoardKo = {
432
445
  "agentBoard.verify.phase.bootstrap": "준비",
433
446
  "agentBoard.verify.phase.build": "빌드",
434
447
  "agentBoard.verify.phase.test": "테스트",
435
- "agentBoard.verify.phase.start": "구동 확인"
448
+ "agentBoard.verify.phase.start": "구동 확인",
449
+ "agentBoard.verify.detail.noRecipe": "실행할 레시피가 없습니다. package.json 스크립트, Makefile, 또는 .aplus/verify.json이 필요합니다.",
450
+ "agentBoard.verify.detail.phaseFailed": "{phase} 실패 · {reason}",
451
+ "agentBoard.verify.detail.phaseTimeout": "{phase} 시간 초과",
452
+ "agentBoard.verify.detail.notReady": "구동은 됐지만 포트가 응답하지 않습니다",
453
+ "agentBoard.verify.phase.deploy": "배포 확인"
436
454
  };
437
455
  const fanoutLifecycleKo = {
438
456
  "fanoutSection.restoreGroupAria": "병렬 그룹 복원: {title} · 실행 {run}",
@@ -572,7 +590,7 @@ const orcaPortKo = {
572
590
  "ciFix.promptIntro": "아래 PR의 CI가 실패했습니다. 실패 로그를 확인하고 원인을 수정해주세요:",
573
591
  "shortcuts.action.quickOpen": "Quick Open 팔레트",
574
592
  "shortcuts.action.quickOpenDescription": "프로젝트·세션·대화를 한 검색창에서 찾아 바로 이동합니다.",
575
- "quickOpen.title": "Quick Open",
593
+ "quickOpen.title": "빠른 열기",
576
594
  "quickOpen.inputAria": "Quick Open 검색",
577
595
  "quickOpen.placeholder": "프로젝트, 세션, 대화 검색…",
578
596
  "quickOpen.resultsAria": "Quick Open 결과",
@@ -623,6 +641,8 @@ const scheduledAutomationsKo = {
623
641
  "scheduledAutomations.nameLabel": "이름",
624
642
  "scheduledAutomations.promptLabel": "프롬프트",
625
643
  "scheduledAutomations.agentLabel": "실행 에이전트",
644
+ "scheduledAutomations.personaLabel": "실행 페르소나",
645
+ "scheduledAutomations.personaNone": "없음",
626
646
  "scheduledAutomations.scheduleLabel": "스케줄",
627
647
  "scheduledAutomations.scheduleInterval": "주기 (분마다)",
628
648
  "scheduledAutomations.scheduleDaily": "매일 지정 시각",
@@ -670,6 +690,8 @@ const scheduledAutomationsKo = {
670
690
  "scheduledAutomations.outcomeSkippedOverlap": "이전 실행 진행 중(스킵)",
671
691
  "scheduledAutomations.outcomeSilent": "이상 없음([SILENT])",
672
692
  "scheduledAutomations.outcomeError": "실패",
693
+ "scheduledAutomations.requestedModel": "요청 모델: {model}",
694
+ "scheduledAutomations.fallbackModelUsed": "폴백 모델 사용됨: {model}",
673
695
  "scheduledAutomations.validationRequired": "이름과 프롬프트를 입력하세요.",
674
696
  "scheduledAutomations.migrationTitle": "기존 자동화 이전",
675
697
  "scheduledAutomations.migrationHint": "기존 앱·daemon 자동화를 Happy Server로 안전하게 이전합니다.",
@@ -753,6 +775,10 @@ const githubTriggersKo = {
753
775
  "githubTriggers.actionLabel": "액션",
754
776
  "githubTriggers.actionNotify": "알림만 (LLM 비용 없음)",
755
777
  "githubTriggers.actionStartSession": "세션 시작",
778
+ "githubTriggers.actionVerifyMerge": "머지 검증 (실패 시에만 세션)",
779
+ "mergeVerify.noticePassed": 'PR #{pr} "{title}" 머지 후 검증을 통과했습니다 ({revision})',
780
+ "mergeVerify.revisionMergeCommit": "머지 커밋 {sha}",
781
+ "mergeVerify.revisionBaseFallback": "base 브랜치 tip — 머지 커밋을 확인하지 못해 폴백",
756
782
  "githubTriggers.agentLabel": "실행 에이전트",
757
783
  "githubTriggers.modelLabel": "모델 (비우면 자동)",
758
784
  "githubTriggers.effortLabel": "추론 강도",
@@ -781,13 +807,15 @@ const githubTriggersKo = {
781
807
  "githubTriggers.outcomeSessionStarted": "세션 시작",
782
808
  "githubTriggers.outcomeSkippedOverlap": "이전 세션 진행 중(스킵)",
783
809
  "githubTriggers.outcomeError": "실패",
810
+ "githubTriggers.outcomeVerifyPassed": "검증 통과",
811
+ "githubTriggers.outcomeVerifyFailed": "검증 실패 → 수정 세션",
784
812
  "githubTriggers.pollError": "GitHub 조회 실패: {detail} — 다음 주기에 재시도합니다.",
785
813
  "githubTriggers.validationRequired": "이름과 템플릿을 입력하세요."
786
814
  };
787
815
  const memoryHubKo = {
788
816
  "memoryHub.button": "메모리",
789
817
  "memoryHub.ariaLabel": "메모리 허브",
790
- "memoryHub.title": "Memory Hub",
818
+ "memoryHub.title": "메모리 허브",
791
819
  "memoryHub.refresh": "새로고침",
792
820
  "memoryHub.openDashboard": "전체 대시보드 열기",
793
821
  "memoryHub.loading": "프로젝트 메모리를 불러오는 중…",
@@ -830,7 +858,19 @@ const extensionInstallKo = {
830
858
  "artifactPublication.open": "링크 열기",
831
859
  "artifactPublication.revoke": "공개 중지",
832
860
  "artifactPublication.revoking": "중지 중…",
833
- "artifactPublication.failed": "공개 링크 작업에 실패했습니다."
861
+ "artifactPublication.failed": "공개 링크 작업에 실패했습니다.",
862
+ "artifactPublication.error.failed": "공개 링크 작업에 실패했습니다.",
863
+ "artifactPublication.error.cancelled": "공개 링크 작업이 취소되었습니다.",
864
+ "artifactPublication.error.timeout": "서버 응답 시간이 초과되었습니다. 잠시 후 다시 시도해 주세요.",
865
+ "artifactPublication.error.network": "서버에 연결할 수 없습니다. 네트워크 상태를 확인해 주세요.",
866
+ "artifactPublication.error.unauthenticated": "로그인 세션이 만료되었습니다. 다시 로그인한 뒤 시도해 주세요.",
867
+ "artifactPublication.error.notFound": "파일을 찾을 수 없습니다. 파일이 저장되었는지 확인해 주세요.",
868
+ "artifactPublication.error.tooLarge": "파일이 너무 큽니다. 공개할 HTML 파일은 10MB 이하여야 합니다.",
869
+ "artifactPublication.error.rateLimited": "공개 링크 요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.",
870
+ "artifactPublication.error.rejected": "서버가 이 파일의 공개 요청을 거부했습니다.",
871
+ "artifactPublication.error.serverError": "서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
872
+ "artifactPublication.error.extensionUnavailable": "확장이 비활성화되었거나 공개 권한이 없습니다. 설정에서 Artifact Publisher를 확인해 주세요.",
873
+ "artifactPublication.error.invalidResponse": "서버 응답을 확인할 수 없어 중단했습니다."
834
874
  };
835
875
  const personalChatsLandingKo = {
836
876
  "personalChats.heroTitle": "무엇을 도와드릴까요?",
@@ -864,7 +904,7 @@ const personalChatsKo = {
864
904
  "personalChats.creating": "채팅을 시작하는 중…",
865
905
  "personalChats.noOnlineMachine": "온라인 머신이 없습니다. 채팅을 시작하려면 머신을 시작해주세요.",
866
906
  "personalChats.localMachinePreparing": "이 컴퓨터를 Saycode에 연결하는 중이에요. 잠시 후 바로 시작할 수 있어요.",
867
- "personalChats.localMachineAsk": "이 컴퓨터를 바로 사용할 수 있어요. 클릭 한 번으로 연결을 마치세요.",
907
+ "personalChats.localMachineAsk": "이 컴퓨터에 Saycode CLI를 설치하면 바로 채팅을 시작할 수 있어요.",
868
908
  "personalChats.localMachineFailed": "이 컴퓨터 자동 연결에 실패했어요. 다른 방법으로 연결할 수 있어요.",
869
909
  "personalChats.pickMachine": "이 채팅을 실행할 머신을 선택하세요",
870
910
  "personalChats.back": "뒤로",
@@ -928,16 +968,45 @@ const agentLoopDetectionKo = {
928
968
  "agentBoard.loopDetectedTitle": "같은 툴 호출 또는 텍스트가 짧은 시간 안에 반복되고 있습니다. 멈춰 있을 수 있습니다.",
929
969
  "settings.agentLoopAutoStop.enable": "반복 감지 시 세션 자동 중지",
930
970
  "settings.agentLoopAutoStop.help": "오토파일럿이 관리하는 세션이 같은 툴 호출이나 텍스트를 반복하며 멈춘 것으로 보이면 자동으로 응답을 중지합니다. 기본은 꺼짐 — 오탐으로 정상 작업을 멈추는 것이 놓치는 것보다 나쁩니다.",
931
- "settings.agentLoopAutoStop.saveFailed": "설정을 저장하지 못했습니다."
971
+ "settings.agentLoopAutoStop.saveFailed": "설정을 저장하지 못했습니다.",
972
+ "settings.autonomousQualityGate.enable": "오토파일럿 자율 품질 게이트 사용",
973
+ "settings.autonomousQualityGate.help": "리뷰와 Commit & PR 전에 프로젝트 검증을 실행하고, 실패하면 같은 세션에 제한된 수정 요청을 보냅니다. 기본은 꺼짐입니다.",
974
+ "settings.autonomousQualityGate.saveFailed": "품질 게이트 설정을 저장하지 못했습니다.",
975
+ "qualityGate.statusAria": "자율 품질 게이트 상태",
976
+ "qualityGate.stage": "품질 게이트 · {stage}",
977
+ "qualityGate.attempt": "시도 {attempt}/{max}",
978
+ "qualityGate.usage": "수정 {continuations}/{maxContinuations} · 턴 {turns}/{maxTurns} · 토큰 {tokens}/{maxTokens}",
979
+ "qualityGate.lastPhase": "{phase} · {result}",
980
+ "qualityGate.limit": "한도 도달: {reason}",
981
+ "qualityGate.pause": "품질 게이트 일시정지",
982
+ "qualityGate.resume": "품질 게이트 재개",
983
+ "qualityGate.stop": "품질 게이트 중지"
932
984
  };
933
985
  const autopilotSelfReviewRoundsKo = {
934
986
  "settings.autopilotSelfReviewRounds.label": "오토파일럿 셀프 리뷰 반복 횟수",
935
- "settings.autopilotSelfReviewRounds.help": '오토파일럿이 "현재 에이전트로 점검"을 몇 번까지 이어서 시킬지 정합니다. 점검 결과에 아직 해결하지 못한 문제가 남아 있으면 남은 횟수만큼 다시 점검하게 하고, 문제가 없어지면 남은 횟수와 상관없이 바로 Commit & PR로 넘어갑니다. 지정한 횟수를 다 쓰고도 문제가 남으면 지금처럼 멈추고 사람에게 넘깁니다. 기본은 1회(지금까지와 동일).',
987
+ "settings.autopilotSelfReviewRounds.help": '오토파일럿이 "현재 에이전트로 점검"을 몇 번까지 이어서 시킬지 정합니다. 한 라운드에서 medium 이상 지적이 나오면 남은 횟수만큼 다시 점검하게 하고, low 이하만 남으면 남은 횟수와 상관없이 바로 Commit & PR로 넘어갑니다. 지정한 횟수를 다 쓰고도 medium 이상이 남으면 멈추고 사람에게 넘깁니다. 기본은 7회.',
936
988
  "settings.autopilotSelfReviewRounds.once": "1회 (반복 없음)",
937
989
  "settings.autopilotSelfReviewRounds.times": "{count}회",
938
990
  "settings.autopilotSelfReviewRounds.saveFailed": "설정을 저장하지 못했습니다.",
939
991
  "agentBoard.autopilot.reviewingSelfRound": "오토파일럿 · 셀프 리뷰 중 {round}/{total}"
940
992
  };
993
+ const memoryGuardrailsKo = {
994
+ "settings.memoryGuardrails.help": "기억으로 남기지 말아야 할 항목을 정합니다. 기본 규칙은 끌 수 있고, 프로젝트 전용 규칙을 추가할 수 있습니다.",
995
+ "settings.memoryGuardrails.saveFailed": "설정을 저장하지 못했습니다.",
996
+ "settings.memoryGuardrails.tooLong": "규칙 전체 길이가 {max}자를 넘어 저장할 수 없습니다.",
997
+ "settings.memoryGuardrails.removeAria": "{text} 삭제",
998
+ "settings.memoryGuardrails.remove": "삭제",
999
+ "settings.memoryGuardrails.addLabel": "새 금지 항목",
1000
+ "settings.memoryGuardrails.addPlaceholder": "예: 이 프로젝트의 임시 브랜치 이름은 기억하지 않는다",
1001
+ "settings.memoryGuardrails.add": "추가"
1002
+ };
1003
+ const memoryEventsKo = {
1004
+ "settings.memoryEvents.ariaLabel": "기억 열람",
1005
+ "settings.memoryEvents.unavailable": "기억 대시보드가 실행 중이 아닙니다.",
1006
+ "settings.memoryEvents.searchLabel": "기억 검색",
1007
+ "settings.memoryEvents.prevPage": "이전",
1008
+ "settings.memoryEvents.nextPage": "다음"
1009
+ };
941
1010
  const externalWorktreeInboxKo = {
942
1011
  "externalWorktreeInbox.title": "앱 밖에서 만든 worktree",
943
1012
  "externalWorktreeInbox.description": "이 저장소에 앱이 만들지 않은 worktree 가 있습니다. 채택하면 목록과 새 대화 시작에서 고를 수 있어요.",
@@ -968,6 +1037,99 @@ const externalSessionVaultKo = {
968
1037
  "externalSessionVault.rejectedNotOwned": "이 프로젝트 폴더에서 돌린 대화가 아니라서 이어가지 않았습니다.",
969
1038
  "externalSessionVault.resumeFailed": "그 대화를 이어가지 못했습니다."
970
1039
  };
1040
+ const orgSearchKo = {
1041
+ "orgAdmin.tab.search": "검색 (Elasticsearch)",
1042
+ "orgSearch.admin.title": "Elasticsearch 기반 조직 검색",
1043
+ "orgSearch.admin.description": "조직의 대화를 조직 소유 Elasticsearch에 색인해 멤버가 키워드로 검색할 수 있게 합니다. 각 멤버는 자기가 참여한 프로젝트만 볼 수 있으며, 이는 멤버별 필터 alias와 API 키로 Elasticsearch가 직접 강제합니다.",
1044
+ "orgSearch.admin.unsupported": "이 서버 또는 데스크탑 버전은 아직 조직 검색을 지원하지 않습니다.",
1045
+ "orgSearch.admin.readOnly": "조직 owner/admin만 이 연동을 변경할 수 있습니다.",
1046
+ "orgSearch.form.url": "Elasticsearch 주소",
1047
+ "orgSearch.form.urlPlaceholder": "http://elasticsearch.internal:9200",
1048
+ "orgSearch.form.username": "관리자 ID",
1049
+ "orgSearch.form.password": "관리자 비밀번호",
1050
+ "orgSearch.form.passwordHint": "전용 운영자 사용자를 만드는 데 한 번만 쓰입니다. 관리자 비밀번호는 저장하지 않습니다.",
1051
+ "orgSearch.form.profile": "analyzer 프로필",
1052
+ "orgSearch.form.profile.nori": "nori + ngram (한국어 형태소, 권장)",
1053
+ "orgSearch.form.profile.standard": "standard + ngram (nori 플러그인 없음)",
1054
+ "orgSearch.form.test": "연결 테스트",
1055
+ "orgSearch.form.testing": "테스트 중…",
1056
+ "orgSearch.form.save": "연결하고 인덱스 만들기",
1057
+ "orgSearch.form.saving": "연결 중…",
1058
+ "orgSearch.probe.version": "Elasticsearch 버전",
1059
+ "orgSearch.probe.security": "보안 활성",
1060
+ "orgSearch.probe.license": "라이선스",
1061
+ "orgSearch.probe.nori": "analysis-nori 플러그인",
1062
+ "orgSearch.probe.privileges": "보안 관리 권한 (운영자 사용자 생성)",
1063
+ "orgSearch.probe.yes": "예",
1064
+ "orgSearch.probe.no": "아니오",
1065
+ "orgSearch.probe.unknown": "알 수 없음",
1066
+ "orgSearch.verdict.ok": "연결에 성공했습니다. 저장하면 운영자 사용자와 인덱스를 만듭니다.",
1067
+ "orgSearch.verdict.unreachable": "Elasticsearch에 닿을 수 없습니다. URL과 이 머신이 같은 네트워크(VPN)에 있는지 확인하세요.",
1068
+ "orgSearch.verdict.unauthorized": "인증에 실패했습니다. 관리자 ID와 비밀번호를 확인하세요.",
1069
+ "orgSearch.verdict.unsupported-version": "Elasticsearch 8 이상이 필요합니다.",
1070
+ "orgSearch.verdict.security-disabled": "이 클러스터는 보안이 꺼져 있어 프로젝트별 접근 권한을 강제할 수 없습니다. xpack.security를 먼저 켜세요.",
1071
+ "orgSearch.verdict.insufficient-privileges": "이 사용자는 보안을 관리할 수 없습니다. manage_security와 manage_api_key 권한이 있는 사용자를 쓰세요.",
1072
+ "orgSearch.saved.title": "조직 검색이 연결되었습니다.",
1073
+ "orgSearch.saved.detail": "운영자 사용자 {operator} · 인덱스 {index}",
1074
+ "orgSearch.saved.existingIndex": "매핑이 일치하는 기존 인덱스를 그대로 사용했습니다.",
1075
+ "orgSearch.summary.url": "Elasticsearch 주소",
1076
+ "orgSearch.summary.index": "인덱스",
1077
+ "orgSearch.summary.profile": "analyzer 프로필",
1078
+ "orgSearch.summary.operator": "운영자 사용자",
1079
+ "orgSearch.summary.updated": "갱신",
1080
+ "orgSearch.disconnect": "연동 해제",
1081
+ "orgSearch.disconnect.title": "조직 검색 연동을 해제할까요?",
1082
+ "orgSearch.disconnect.body": "Elasticsearch에서 멤버 키·alias·조직 인덱스를 삭제하고, 멤버의 조직 검색 탭은 즉시 사라집니다. 대화 자체에는 영향이 없습니다.",
1083
+ "orgSearch.disconnect.operatorSection": "운영자 사용자도 삭제 (관리자 계정이 다시 필요)",
1084
+ "orgSearch.disconnect.confirm": "해제하고 삭제",
1085
+ "orgSearch.disconnect.working": "해제 중…",
1086
+ "orgSearch.disconnect.cancel": "취소",
1087
+ "orgSearch.disconnected": "해제했습니다. 인덱스 {indices}개, alias {aliases}개, 멤버 키 {keys}개를 삭제했습니다.",
1088
+ "orgSearch.disconnected.operatorKept": "관리자 자격증명이 없어 운영자 사용자는 남겨 두었습니다.",
1089
+ "orgSearch.error.title": "마지막 작업이 실패했습니다",
1090
+ "orgSearch.acl.title": "권한 동기화 (멤버별 alias·키)",
1091
+ "orgSearch.acl.upToDate": "권한이 최신입니다",
1092
+ "orgSearch.acl.waiting": "{count}건 대기",
1093
+ "orgSearch.acl.members": "멤버 {count}명",
1094
+ "orgSearch.acl.tick.applied": "마지막 동기화: 변경 {actions}건 적용, 실패 {failures}건.",
1095
+ "orgSearch.acl.tick.inSync": "마지막 동기화에서 변경할 것이 없었습니다.",
1096
+ "orgSearch.acl.tick.elsewhere": "다른 관리자의 데스크탑이 동기화를 맡고 있습니다.",
1097
+ "orgSearch.acl.tick.failed": "마지막 동기화 실패: {message}",
1098
+ "orgSearch.progress.title": "색인 진행률",
1099
+ "orgSearch.progress.phase.idle": "색인할 대화 없음",
1100
+ "orgSearch.progress.phase.waiting": "색인할 멤버 대기 중",
1101
+ "orgSearch.progress.phase.indexing": "색인 중",
1102
+ "orgSearch.progress.phase.done": "색인 완료",
1103
+ "orgSearch.progress.sessions": "세션 {done}/{total} ({percent}%)",
1104
+ "orgSearch.progress.messages": "메시지 {count}개 색인됨",
1105
+ "orgSearch.progress.failed": "실패 {count}건",
1106
+ "orgSearch.progress.leases": "활성 색인기 {count}개",
1107
+ "orgSearch.progress.updated": "{time} 갱신",
1108
+ "orgSearch.progress.project": "프로젝트",
1109
+ "orgSearch.progress.phaseColumn": "상태",
1110
+ "orgSearch.progress.sessionsColumn": "세션",
1111
+ "orgSearch.progress.leader": "색인기",
1112
+ "search.tabOrg": "조직",
1113
+ "orgSearch.tab.keyPending": "조직 관리자의 데스크탑이 검색 키를 발급하는 중입니다. 잠시 후 다시 시도하세요.",
1114
+ "orgSearch.tab.unavailable": "지금은 조직 검색을 사용할 수 없습니다.",
1115
+ "orgSearch.tab.error.unauthorized": "검색 키가 거부되었습니다. 새 키를 요청했으니 잠시 후 다시 시도하세요.",
1116
+ "orgSearch.tab.error.forbidden": "Elasticsearch가 이 검색을 거부했습니다. 권한 동기화가 늦었을 수 있으며 관리자 데스크탑이 곧 맞춥니다.",
1117
+ "orgSearch.tab.error.unreachable": "조직 Elasticsearch에 닿을 수 없습니다. 같은 네트워크(VPN)에 있는지 확인하세요.",
1118
+ "orgSearch.tab.error.timeout": "Elasticsearch가 제때 응답하지 않았습니다.",
1119
+ "orgSearch.tab.error.not-configured": "이 데스크탑에는 아직 조직 검색이 설정되지 않았습니다.",
1120
+ "orgSearch.tab.error.error": "검색에 실패했습니다.",
1121
+ "orgSearch.eval.title": "검색 품질 평가",
1122
+ "orgSearch.eval.description": "색인된 내 프롬프트 {count}건을 표본으로 뽑아 실제 검색어처럼 변형(부분 문자열·키워드·오타·식별자 등)한 뒤, 각 검색어가 자기 대화를 찾는지를 Elasticsearch와 이 데스크탑의 로컬 인덱스에서 나란히 확인합니다. 프롬프트는 앱 밖으로 나가지 않습니다.",
1123
+ "orgSearch.eval.run": "품질 평가 실행",
1124
+ "orgSearch.eval.running": "평가 중…",
1125
+ "orgSearch.eval.emptyIndex": "로컬 인덱스에 표본으로 쓸 프롬프트가 아직 없습니다.",
1126
+ "orgSearch.eval.sampled": "프롬프트 {prompts}건, 검색어 {cases}건",
1127
+ "orgSearch.eval.kind": "변형 종류",
1128
+ "orgSearch.eval.engine.es": "Elasticsearch(조직)",
1129
+ "orgSearch.eval.engine.local": "로컬 인덱스",
1130
+ "orgSearch.eval.overall": "전체",
1131
+ "orgSearch.eval.legend": "칸: Recall@1 / @5 / @10 · MRR · p95 지연. 같은 문구를 여러 대화에서 썼다면 다른 대화가 1위인 것도 정상이라, 100% 미만이 곧 결함은 아닙니다."
1132
+ };
971
1133
  const operationModelRetryKo = {
972
1134
  "review.job.fallbackModelUsed": "폴백 모델 사용됨: {model}"
973
1135
  };
@@ -1106,7 +1268,7 @@ const machineSettingsConsoleKo = {
1106
1268
  "machineConsole.daemonUnknown": "daemon 미확인",
1107
1269
  "machineConsole.daemonStatus": "daemon 상태",
1108
1270
  "machineConsole.daemonVersion": "daemon 버전",
1109
- "machineConsole.daemonVersionValue": "daemon {version}",
1271
+ "machineConsole.daemonVersionValue": "데몬 {version}",
1110
1272
  "machineConsole.rpcStatus": "RPC 상태",
1111
1273
  "machineConsole.rpcAvailableStatus": "RPC 사용 가능",
1112
1274
  "machineConsole.rpcUnavailableStatus": "RPC 사용 불가",
@@ -1194,7 +1356,17 @@ const newSessionProfileKo = {
1194
1356
  "newSession.profile.fallbackAgent": "{value} 에이전트를 지금 쓸 수 없어 현재 선택을 유지했습니다.",
1195
1357
  "newSession.profile.fallbackModel": "{value} 모델이 목록에 없어 기본값으로 시작합니다.",
1196
1358
  "newSession.profile.fallbackEffort": "{value} 강도를 이 모델에서 쓸 수 없어 기본값으로 시작합니다.",
1197
- "newSession.profile.fallbackWorktree": "이 프로젝트는 git 저장소가 아니라 작업 사본을 끕니다."
1359
+ "newSession.profile.fallbackWorktree": "이 프로젝트는 git 저장소가 아니라 작업 사본을 끕니다.",
1360
+ // specs/desktop-session-personas T9
1361
+ "newSession.profile.editPersona": "{name} 페르소나 편집",
1362
+ "newSession.profile.instructionsLabel": "{name} 지시문",
1363
+ "newSession.profile.instructionsPlaceholder": "예: 리뷰 관점에서만 답하라",
1364
+ "newSession.profile.instructionsTooLong": "지시문이 너무 깁니다 ({max}자 이하로 줄여주세요).",
1365
+ "newSession.profile.instructionsSave": "{name} 지시문 저장",
1366
+ "newSession.profile.skillsLabel": "배포할 스킬",
1367
+ "newSession.profile.badgeIconLabel": "{name} 표식 아이콘",
1368
+ "newSession.profile.badgeColorLabel": "{name} 표식 색상",
1369
+ "newSession.profile.badgeSave": "{name} 표식 저장"
1198
1370
  };
1199
1371
  const agentBoardBroadcastKo = {
1200
1372
  "agentBoard.broadcastSelected": "선택됨",
@@ -1252,6 +1424,16 @@ const officeViewerKo = {
1252
1424
  "officeViewer.unsupported": "미리보기를 지원하지 않는 형식입니다.",
1253
1425
  "officeViewer.unsupportedHint": "원본 앱에서 열어 확인하세요."
1254
1426
  };
1427
+ const additionalDirectoryMountWarningKo = {
1428
+ "additionalDirectories.mountMissing": "추가 디렉터리 {count}개를 자동 마운트에서 제외하고 세션을 시작했습니다.",
1429
+ "additionalDirectories.mountRuntimeUnsupported": "이 머신의 Happy CLI가 추가 디렉터리 마운트를 지원하지 않아 기본 세션으로 시작했습니다.",
1430
+ "additionalDirectories.mountLimit": "추가 디렉터리 마운트 상한을 넘어 {count}개를 제외하고 세션을 시작했습니다.",
1431
+ "additionalDirectories.relatedMountMissing": "관련 프로젝트 디렉터리 {count}개를 자동 마운트에서 제외하고 세션을 시작했습니다.",
1432
+ "additionalDirectories.relatedMountLinkLookupFailed": "관련 프로젝트 목록을 불러오지 못해 자동 마운트 없이 세션을 시작했습니다.",
1433
+ "additionalDirectories.relatedMountLookupFailed": "관련 프로젝트 {count}개를 불러오지 못해 제외하고 세션을 시작했습니다.",
1434
+ "additionalDirectories.relatedMountRuntimeUnsupported": "이 머신의 Happy CLI가 관련 프로젝트 자동 마운트를 지원하지 않아 기본 세션으로 시작했습니다.",
1435
+ "additionalDirectories.relatedMountLimit": "관련 프로젝트 디렉터리 상한을 넘어 {count}개를 제외하고 세션을 시작했습니다."
1436
+ };
1255
1437
  const additionalDirectoriesKo = {
1256
1438
  "additionalDirectories.title": "추가 디렉터리",
1257
1439
  "additionalDirectories.add": "디렉터리 추가",
@@ -1272,11 +1454,7 @@ const additionalDirectoriesKo = {
1272
1454
  "additionalDirectories.machineMismatch": "현재 머신과 다른 위치에서 선택한 디렉터리입니다.",
1273
1455
  "additionalDirectories.saveFailed": "디렉터리를 등록하지 못했습니다.",
1274
1456
  "additionalDirectories.folderLoadFailed": "디렉터리를 불러오지 못했습니다.",
1275
- "additionalDirectories.relatedMountMissing": "관련 프로젝트 디렉터리 {count}개를 자동 마운트에서 제외하고 세션을 시작했습니다.",
1276
- "additionalDirectories.relatedMountLinkLookupFailed": "관련 프로젝트 목록을 불러오지 못해 자동 마운트 없이 세션을 시작했습니다.",
1277
- "additionalDirectories.relatedMountLookupFailed": "관련 프로젝트 {count}개를 불러오지 못해 제외하고 세션을 시작했습니다.",
1278
- "additionalDirectories.relatedMountRuntimeUnsupported": "이 머신의 Happy CLI가 관련 프로젝트 자동 마운트를 지원하지 않아 기본 세션으로 시작했습니다.",
1279
- "additionalDirectories.relatedMountLimit": "관련 프로젝트 디렉터리 상한을 넘어 {count}개를 제외하고 세션을 시작했습니다."
1457
+ ...additionalDirectoryMountWarningKo
1280
1458
  };
1281
1459
  const agentHandoffKo = {
1282
1460
  "agentHandoff.open": "다른 에이전트로 이어가기",
@@ -1357,6 +1535,24 @@ const chatFolderUploadKo = {
1357
1535
  "upload.partialIssue": "업로드 실패 {failed}개 · 건너뜀 {skipped}개",
1358
1536
  "upload.invalidPath": "안전하지 않은 첨부 경로입니다."
1359
1537
  };
1538
+ const composerAddMenuKo = {
1539
+ "composer.addMenu.trigger": "추가",
1540
+ "composer.addMenu.fileOrPhoto": "파일 또는 사진 추가",
1541
+ "composer.addMenu.folder": "폴더 추가",
1542
+ "composer.addMenu.disabled": "지금은 파일이나 폴더를 추가할 수 없습니다.",
1543
+ "shortcuts.action.composerAttachFile": "파일 또는 사진 추가",
1544
+ "shortcuts.action.composerAttachFileDescription": "포커스된 메시지 작성란의 파일 선택기를 엽니다."
1545
+ };
1546
+ const composerQuickCommandsKo = {
1547
+ "composer.quickCommands.trigger": "Quick Commands",
1548
+ "composer.quickCommands.saveCurrent": "현재 입력을 이 프로젝트의 Quick Command로 저장",
1549
+ "composer.quickCommands.saveCurrentGlobal": "현재 입력을 모든 프로젝트의 Quick Command로 저장",
1550
+ "composer.quickCommands.storageWriteFailed": "이 기기에 저장하지 못했습니다 — 앱을 다시 시작하면 목록이 사라질 수 있습니다.",
1551
+ "composer.quickCommands.empty": "저장된 Quick Command가 없습니다.",
1552
+ "composer.quickCommands.projectScope": "이 프로젝트",
1553
+ "composer.quickCommands.globalScope": "모든 프로젝트",
1554
+ "composer.quickCommands.remove": '"{label}" 삭제'
1555
+ };
1360
1556
  const aiCommitPrKo = {
1361
1557
  "aiCommitPr.stagedCount": "스테이지된 파일 {count}개",
1362
1558
  "aiCommitPr.noStaged": "스테이지된 변경이 없습니다.",
@@ -1436,8 +1632,180 @@ const connectorReadinessKo = {
1436
1632
  "connectorReadiness.state.configMissing": "설정 없음",
1437
1633
  "connectorReadiness.state.unknown": "상태 미확인"
1438
1634
  };
1439
- const catalogExtensionsKo = { ...taskNotificationKo, ...machineUsageKo, ...completionInboxKo, ...agentBoardKo, ...fanoutLifecycleKo, ...conversationSearchKo, ...browserCookieImportKo, ...orcaPortKo, ...usageAnalyticsKo, ...outboundWebhookKo, ...scheduledAutomationsKo, ...githubTriggersKo, ...memoryHubKo, ...extensionPanelKo, ...extensionInstallKo, ...personalChatsKo, ...newSessionStarterKo, ...browserConsoleCaptureKo, ...completionSignalKo, ...agentLoopDetectionKo, ...operationModelRetryKo, ...skillCatalogKo, ...ciAutoFixKo, ...collaborationKo, ...backgroundTaskKo, ...sessionTitleKo, ...accountSettingsKo, ...projectScopeKo, ...machineSettingsConsoleKo, ...sessionTokenUsageKo, ...worktreeSetupKo, ...connectionIndicatorKo, ...voiceDictationKo, ...newSessionProfileKo, ...agentBoardBroadcastKo, ...judgementEvidenceKo, ...releaseNoteKo, ...notificationSoundKo, ...officeViewerKo, ...additionalDirectoriesKo, ...agentHandoffKo, ...sidebarUpdateKo, ...autopilotSelfReviewRoundsKo, ...aiCommitPrKo, ...externalWorktreeInboxKo, ...externalSessionVaultKo, ...terminalFontKo, ...globalPauseKo, ...subagentKo, ...reviewNavigationKo, ...projectLinksKo, ...projectHandoffKo, ...chatFolderUploadKo, ...connectorReadinessKo };
1635
+ const checkpointKo = {
1636
+ "checkpoint.title": "파일 체크포인트",
1637
+ "checkpoint.status.protected": "보호됨",
1638
+ "checkpoint.status.legacy": "보호되지 않음",
1639
+ "checkpoint.status.unavailable": "보호 사용 불가",
1640
+ "checkpoint.refresh": "새로고침",
1641
+ "checkpoint.timeline": "기록",
1642
+ "checkpoint.timeline.empty": "아직 체크포인트가 없습니다.",
1643
+ "checkpoint.timeline.snapshot": "스냅샷",
1644
+ "checkpoint.timeline.rewind": "복원",
1645
+ "checkpoint.preview": "미리보기",
1646
+ "checkpoint.preview.title": "복원 미리보기",
1647
+ "checkpoint.preview.empty": "되돌릴 파일 변경이 없습니다.",
1648
+ "checkpoint.preview.confirm": "이 미리보기의 파일 변경을 확인했습니다",
1649
+ "checkpoint.restore": "파일 상태 복원",
1650
+ "checkpoint.restore.running": "파일 상태를 복원하는 중…",
1651
+ "checkpoint.restore.completed": "파일 상태 복원이 완료되었습니다.",
1652
+ "checkpoint.restore.partial": "일부 파일을 복원하지 못했습니다.",
1653
+ "checkpoint.restore.stale": "파일 상태가 바뀌었습니다. 새 미리보기를 확인하세요.",
1654
+ "checkpoint.restore.retry": "실패한 복원 다시 시도",
1655
+ "checkpoint.restore.cancel": "복원 취소",
1656
+ "checkpoint.pending.title": "보호 정책 확인 필요",
1657
+ "checkpoint.pending.partialWarning": "이번 turn이 일부 실행되었을 수 있습니다.",
1658
+ "checkpoint.pending.externalWarning": "외부 부작용은 다시 실행될 수 있습니다.",
1659
+ "checkpoint.pending.cancel": "보호 유지하고 취소",
1660
+ "checkpoint.pending.disable": "보호 해제",
1661
+ "checkpoint.restartRequired": "계속하려면 daemon 세션을 재시작해야 합니다.",
1662
+ "checkpoint.restart": "보호 없이 세션 재시작",
1663
+ "checkpoint.restart.running": "세션을 재시작하는 중…",
1664
+ "checkpoint.error.action": "체크포인트 작업을 완료하지 못했습니다.",
1665
+ "checkpoint.action.restore": "되돌리기",
1666
+ "checkpoint.action.delete": "삭제",
1667
+ "checkpoint.action.skip": "보존",
1668
+ "checkpoint.action.conflict": "충돌",
1669
+ "checkpoint.failure.mutation-failed": "파일 변경 실패",
1670
+ "checkpoint.failure.mutation-outcome-unknown": "결과를 알 수 없어 재확인이 필요함"
1671
+ };
1672
+ const localLoopKo = {
1673
+ "preview.reasonNoProject": "선택된 프로젝트가 없습니다.",
1674
+ "preview.reasonNoPort": "프로젝트에 미리보기 포트가 설정되어 있지 않습니다.",
1675
+ "preview.reasonNoMachine": "프로젝트가 머신에 연결되어 있지 않습니다.",
1676
+ "preview.reasonMachineMissing": "프로젝트의 머신을 찾을 수 없습니다.",
1677
+ "preview.reasonMachineOffline": "{host} 머신이 오프라인입니다.",
1678
+ "preview.reasonWorktreeNotRunning": "worktree 미리보기가 아직 실행 중이 아닙니다.",
1679
+ "preview.reasonWorktreeMachineOffline": "worktree 미리보기 머신이 오프라인입니다.",
1680
+ "preview.standaloneNoPortHint": "로컬 모드에서는 터미널이나 에이전트로 dev 서버를 실행하면 프로젝트 안에서 열린 포트를 감지해 여기에 표시합니다.",
1681
+ "workCompletion.commitNoRemote": "원격 저장소가 없어 push와 PR 생성 없이 커밋까지만 요청합니다.",
1682
+ "workCompletion.commitOnlyAction": "바로 커밋",
1683
+ "projectSettings.workspaceDefaultPlaceholder": "기본 경로: {path}",
1684
+ "preview.standaloneHostNotEmbeddable": "{host} 은(는) 앱 안에 표시할 수 없는 주소입니다. 이 컴퓨터(127.0.0.1)나 .local 호스트만 직접 표시합니다."
1685
+ };
1686
+ const localizationGapKo = {
1687
+ "systemPrompt.loading": "프롬프트를 불러오는 중...",
1688
+ "systemPrompt.presetsAria": "시스템 프롬프트 프리셋",
1689
+ "systemPrompt.instructionsLabel": "지시문",
1690
+ "systemPrompt.placeholder": "데스크탑 세션에 적용할 사용자 지시문",
1691
+ "systemPrompt.charCount": "{count}자",
1692
+ "projectInfo.projectId": "프로젝트 ID",
1693
+ "projectInfo.workspace": "작업 폴더",
1694
+ "projectInfo.devCommand": "개발 서버 명령",
1695
+ "projectInfo.machinePlatform": "머신 플랫폼",
1696
+ "projectInfo.electron": "Electron 버전",
1697
+ "projectInfo.unknown": "알 수 없음",
1698
+ "projectInfo.notConfigured": "설정되지 않음",
1699
+ "projectInfo.unbound": "연결된 머신 없음",
1700
+ "projectMeta.source": "소스",
1701
+ "projectMeta.preview": "미리보기",
1702
+ "projectMeta.machineUnbound": "미지정",
1703
+ "projectMeta.previewUnavailable": "사용 불가",
1704
+ "machineDialog.eyebrow": "세션 대상",
1705
+ "machineDialog.title": "머신 선택",
1706
+ "machineDialog.cancelAria": "머신 선택 취소",
1707
+ "machineDialog.unknownPlatform": "플랫폼 미확인",
1708
+ "newProject.repoInitFailed": "프로젝트는 만들었지만 Git 저장소 초기화에 실패했습니다: {detail}",
1709
+ "standalone.defaultProjectName": "로컬 프로젝트"
1710
+ };
1711
+ const settingsInfoStructureKo = {
1712
+ "about.mode": "모드",
1713
+ "about.mode.demo": "데모 워크스페이스",
1714
+ "about.mode.local": "로컬 워크스페이스",
1715
+ "about.mode.guest": "로컬 워크스페이스 (Guest)",
1716
+ "about.mode.cloud": "Saycode 계정 워크스페이스",
1717
+ "about.appVersion": "앱 버전",
1718
+ "about.userData": "사용자 데이터",
1719
+ "about.unknown": "알 수 없음",
1720
+ "about.updates": "업데이트",
1721
+ "about.versionLine": "현재 {current} · 최신 {latest}",
1722
+ "about.check": "업데이트 확인",
1723
+ "about.download": "업데이트 다운로드",
1724
+ "about.installRestart": "설치 후 재시작",
1725
+ "about.status.checking": "업데이트를 확인하는 중…",
1726
+ "about.status.available": "{version} 버전을 사용할 수 있습니다.",
1727
+ "about.status.downloading": "업데이트 다운로드 중 {percent}%",
1728
+ "about.status.downloaded": "업데이트를 내려받았습니다. 재시작하면 설치됩니다.",
1729
+ "about.status.latest": "최신 버전을 사용 중입니다.",
1730
+ "about.status.unsupported": "업데이트는 패키징된 macOS 빌드에서 제공됩니다.",
1731
+ "about.status.error": "업데이트 확인에 실패했습니다.",
1732
+ "about.status.idle": "업데이트는 자동으로 확인됩니다.",
1733
+ "homeCard.owner": "소유자",
1734
+ "homeCard.branch": "브랜치",
1735
+ "homeCard.updated": "업데이트",
1736
+ "homeCard.preview": "프리뷰",
1737
+ "homeCard.previewAvailable": "사용 가능",
1738
+ "homeCard.previewNotReady": "준비 안 됨",
1739
+ "homeCard.deployed": "배포 시각",
1740
+ "theme.title": "테마",
1741
+ "theme.help": "자동은 macOS 시스템 설정을 따릅니다. 홈 화면 오른쪽 위 토글과 같은 설정입니다.",
1742
+ "theme.auto": "자동",
1743
+ "theme.light": "라이트",
1744
+ "theme.dark": "다크",
1745
+ "theme.autoTitle": "시스템 설정 따르기",
1746
+ "theme.lightTitle": "라이트 모드",
1747
+ "theme.darkTitle": "다크 모드",
1748
+ "standaloneConnectivity.localServer": "로컬 happy-server",
1749
+ "standaloneConnectivity.zrokShare": "zrok 공유 주소",
1750
+ "standaloneConnectivity.zrokFixedName": "zrok 고정 이름",
1751
+ "memoryLayerPanel.remoteNotice": "메모리 레이어 작업은 선택한 프로젝트의 원격 머신에서 실행됩니다.",
1752
+ "memoryLayerPanel.remoteNoticeWithMachine": "메모리 레이어 작업은 선택한 프로젝트의 원격 머신({machine})에서 실행됩니다.",
1753
+ "memoryLayerPanel.targetUnavailable": "원격 머신 대상을 사용할 수 없습니다.",
1754
+ "memoryLayerPanel.checking": "claude-memory-layer를 확인하는 중…",
1755
+ "memoryLayerPanel.installed": "설치됨",
1756
+ "memoryLayerPanel.missing": "미설치",
1757
+ "memoryLayerPanel.updateAvailable": "업데이트 가능",
1758
+ "memoryLayerPanel.version": "버전 {version}",
1759
+ "memoryLayerPanel.versionUnknown": "버전 알 수 없음",
1760
+ "memoryLayerPanel.latest": "최신 {version}",
1761
+ "memoryLayerPanel.notFound": "찾을 수 없음",
1762
+ "memoryLayerPanel.unknown": "알 수 없음",
1763
+ "memoryLayerPanel.notAssigned": "지정되지 않음",
1764
+ "memoryLayerPanel.remoteMachine": "원격 머신",
1765
+ "memoryLayerPanel.installGlobally": "전역 설치",
1766
+ "memoryLayerPanel.updatePackage": "패키지 업데이트",
1767
+ "memoryLayerPanel.reinstallHooks": "훅 다시 설치",
1768
+ "memoryLayerPanel.installing": "설치하는 중…",
1769
+ "memoryLayerPanel.installed.notice": "원격 머신에 claude-memory-layer가 준비됐습니다.",
1770
+ "memoryLayerPanel.agentsTitle": "MCP 에이전트",
1771
+ "memoryLayerPanel.detected": "감지됨",
1772
+ "memoryLayerPanel.registered": "등록됨",
1773
+ "memoryLayerPanel.register": "등록",
1774
+ "memoryLayerPanel.registering": "등록하는 중…",
1775
+ "memoryLayerPanel.registrationUpdated": "{agent} MCP 등록을 원격 머신에 반영했습니다.",
1776
+ "memoryLayerPanel.noAgents": "에이전트 상태를 확인할 수 없습니다.",
1777
+ "memoryLayerPanel.dashboardTitle": "프로젝트 대시보드",
1778
+ "memoryLayerPanel.project": "프로젝트",
1779
+ "memoryLayerPanel.noProject": "선택한 프로젝트 없음",
1780
+ "memoryLayerPanel.path": "경로",
1781
+ "memoryLayerPanel.machine": "머신",
1782
+ "memoryLayerPanel.dashboard": "대시보드",
1783
+ "memoryLayerPanel.remotePort": "원격 포트 {port}",
1784
+ "memoryLayerPanel.notRunning": "실행 중 아님",
1785
+ "memoryLayerPanel.open": "열기",
1786
+ "memoryLayerPanel.stop": "중지",
1787
+ "memoryLayerPanel.starting": "시작하는 중…",
1788
+ "memoryLayerPanel.startDashboard": "대시보드 시작",
1789
+ "memoryLayerPanel.dashboardStarted": "원격 메모리 대시보드를 시작했습니다.",
1790
+ "memoryLayerPanel.dashboardStopped": "원격 메모리 대시보드를 중지했습니다.",
1791
+ "memoryLayerPanel.lastOutput": "마지막 명령 출력",
1792
+ "machineConsole.ownerMe": "나",
1793
+ "overflowMenu.aria": "추가 액션 메뉴",
1794
+ "overflowMenu.title": "추가 액션"
1795
+ };
1796
+ const catalogExtensionsKo = { ...taskNotificationKo, ...machineUsageKo, ...completionInboxKo, ...agentBoardKo, ...fanoutLifecycleKo, ...conversationSearchKo, ...browserCookieImportKo, ...orcaPortKo, ...usageAnalyticsKo, ...outboundWebhookKo, ...scheduledAutomationsKo, ...githubTriggersKo, ...memoryHubKo, ...extensionPanelKo, ...extensionInstallKo, ...personalChatsKo, ...newSessionStarterKo, ...browserConsoleCaptureKo, ...completionSignalKo, ...agentLoopDetectionKo, ...operationModelRetryKo, ...skillCatalogKo, ...ciAutoFixKo, ...collaborationKo, ...backgroundTaskKo, ...sessionTitleKo, ...accountSettingsKo, ...projectScopeKo, ...machineSettingsConsoleKo, ...sessionTokenUsageKo, ...worktreeSetupKo, ...connectionIndicatorKo, ...voiceDictationKo, ...newSessionProfileKo, ...agentBoardBroadcastKo, ...judgementEvidenceKo, ...releaseNoteKo, ...notificationSoundKo, ...officeViewerKo, ...additionalDirectoriesKo, ...agentHandoffKo, ...sidebarUpdateKo, ...autopilotSelfReviewRoundsKo, ...aiCommitPrKo, ...externalWorktreeInboxKo, ...externalSessionVaultKo, ...orgSearchKo, ...terminalFontKo, ...globalPauseKo, ...subagentKo, ...reviewNavigationKo, ...projectLinksKo, ...projectHandoffKo, ...chatFolderUploadKo, ...composerAddMenuKo, ...composerQuickCommandsKo, ...connectorReadinessKo, ...settingsInfoStructureKo, ...localLoopKo, ...localizationGapKo, ...checkpointKo, ...memoryGuardrailsKo, ...memoryEventsKo };
1440
1797
  const onboardingKo = {
1798
+ "boot.stage.app": "앱 준비 중…",
1799
+ "boot.stage.localRuntime": "로컬 작업공간 준비 중…",
1800
+ "login.dialogTitle": "Saycode 계정으로 로그인",
1801
+ "login.dialogDescription": "로컬 데이터는 이 컴퓨터에 그대로 남고, 현재 창만 클라우드 작업공간으로 전환됩니다.",
1802
+ "login.dialogClose": "로그인 닫기",
1803
+ "guestRecovery.title": "로컬 작업공간을 준비하지 못했어요",
1804
+ "guestRecovery.body": "Saycode가 로컬 런타임을 시작하지 못했습니다. 다시 시도하거나 계정으로 로그인해 클라우드 작업공간을 사용하세요.",
1805
+ "guestRecovery.retry": "다시 시도",
1806
+ "guestRecovery.retrying": "로컬 작업공간 준비 중…",
1807
+ "account.guest": "게스트",
1808
+ "account.localWorkspace": "로컬 작업공간",
1441
1809
  "onboarding.dialogAria": "시작 설정",
1442
1810
  "onboarding.eyebrow": "처음 시작하기",
1443
1811
  "onboarding.title": "첫 작업까지 함께 준비해요",
@@ -1460,9 +1828,9 @@ const onboardingKo = {
1460
1828
  "onboarding.oneCommand.advanced": "고급: 수동 설치",
1461
1829
  "onboarding.localMachine.registering": "이 컴퓨터를 등록하는 중…",
1462
1830
  "onboarding.localMachine.registered": "✓ 이 컴퓨터가 등록되었습니다. 바로 작업할 수 있어요.",
1463
- "onboarding.localMachine.askTitle": "이 컴퓨터를 바로 사용",
1464
- "onboarding.localMachine.askBody": "이 컴퓨터에 이미 Happy 설정이 있습니다. 지금 설정으로 바꾸기 전에 원래 파일을 백업해 두고, 실패하면 되돌립니다.",
1465
- "onboarding.localMachine.askAction": "이 컴퓨터를 바로 사용",
1831
+ "onboarding.localMachine.askTitle": "Saycode CLI 설치",
1832
+ "onboarding.localMachine.askBody": "버튼을 누르면 이 컴퓨터의 기존 설정을 안전하게 백업한 뒤 Saycode CLI를 설치하고 연결합니다.",
1833
+ "onboarding.localMachine.askAction": "Saycode CLI 설치",
1466
1834
  "onboarding.localMachine.failed": "자동 등록에 실패했습니다. 아래 명령으로 직접 설정할 수 있어요.",
1467
1835
  "onboarding.localMachine.retry": "다시 시도",
1468
1836
  "onboarding.localMachine.remoteHint": "다른 컴퓨터나 조직 공유 머신은 머신 설정에서 추가할 수 있어요.",
@@ -1485,7 +1853,101 @@ const onboardingKo = {
1485
1853
  "onboarding.refresh": "상태 다시 확인",
1486
1854
  "onboarding.allDone": "준비가 끝났습니다. 이제 첫 작업을 시작할 수 있어요.",
1487
1855
  "onboarding.resume": "시작 설정 이어하기",
1488
- "onboarding.preflight": "{provider}를 사용하기 전에 로그인을 완료해 주세요."
1856
+ "onboarding.preflight": "{provider}를 사용하기 전에 로그인을 완료해 주세요.",
1857
+ "onboarding.page.happy": "Saycode CLI",
1858
+ "onboarding.page.agents": "AI 도구",
1859
+ "onboarding.page.notifications": "알림",
1860
+ "onboarding.page.happyTitle": "Saycode CLI 준비",
1861
+ "onboarding.page.happyBody": "설치와 버전을 확인하고, 필요한 작업만 이 화면에서 실행합니다.",
1862
+ "onboarding.page.agentsTitle": "Claude Code와 Codex",
1863
+ "onboarding.page.agentsBody": "사용할 AI 도구를 설치하고 계정 로그인을 완료하세요.",
1864
+ "onboarding.page.notificationsTitle": "알림 설정",
1865
+ "onboarding.page.notificationsBody": "응답 요청과 작업 완료를 놓치지 않도록 알림과 사운드를 설정하세요.",
1866
+ "onboarding.page.back": "이전",
1867
+ "onboarding.page.continue": "계속",
1868
+ "onboarding.page.addProject": "첫 프로젝트 추가",
1869
+ "onboarding.page.configured": "설정됨",
1870
+ "onboarding.page.optional": "선택",
1871
+ "onboarding.page.testNotification": "테스트 알림 보내기",
1872
+ "onboarding.page.testNotificationSending": "테스트 알림 보내는 중…",
1873
+ "onboarding.page.testNotificationSuccess": "테스트 알림을 보냈습니다.",
1874
+ "onboarding.page.testNotificationUnsupported": "이 환경에서는 OS 알림을 사용할 수 없습니다.",
1875
+ "onboarding.page.testNotificationFailed": "테스트 알림을 보내지 못했습니다.",
1876
+ "onboarding.page.testNotificationTitle": "Saycode 알림 테스트",
1877
+ "onboarding.page.testNotificationBody": "알림이 잘 도착하고 있어요.",
1878
+ "onboarding.cli.installed": "설치됨",
1879
+ "onboarding.cli.standaloneBody": "Standalone 앱에 포함된 로컬 런타임을 사용합니다.",
1880
+ "onboarding.cli.currentBody": "이 컴퓨터의 Saycode CLI가 최신 버전입니다.",
1881
+ "onboarding.cli.installTitle": "이 컴퓨터에 CLI가 필요합니다",
1882
+ "onboarding.cli.installBody": "Saycode가 설치, 계정 연결, 최종 확인을 순서대로 진행합니다.",
1883
+ "onboarding.cli.installAction": "Saycode CLI 설치",
1884
+ "onboarding.cli.installing": "설치 중…",
1885
+ "onboarding.cli.verifying": "설치 상태 확인 중…",
1886
+ "onboarding.cli.stage.prepare": "설치 준비",
1887
+ "onboarding.cli.stage.install": "CLI 설치 및 연결",
1888
+ "onboarding.cli.stage.verify": "설치 상태 확인",
1889
+ "onboarding.cli.updateTitle": "업데이트가 필요합니다",
1890
+ "onboarding.cli.updateAction": "업데이트 실행",
1891
+ "onboarding.cli.updating": "업데이트 후 설치 상태를 확인하는 중…",
1892
+ "onboarding.cli.checking": "설치 상태 확인 중…",
1893
+ "onboarding.cli.checkFailedTitle": "설치 상태를 확인하지 못했습니다",
1894
+ "onboarding.cli.checkFailedBody": "연결을 확인한 뒤 다시 시도해 주세요.",
1895
+ "onboarding.cli.retry": "다시 확인",
1896
+ "onboarding.cli.nav.checking": "확인 중",
1897
+ "onboarding.cli.nav.install-required": "설치 필요",
1898
+ "onboarding.cli.nav.installing": "설치 중",
1899
+ "onboarding.cli.nav.installed": "설치됨",
1900
+ "onboarding.cli.nav.update-required": "업데이트 필요",
1901
+ "onboarding.cli.nav.updating": "업데이트 중",
1902
+ "onboarding.cli.nav.failed": "확인 필요",
1903
+ "onboarding.provider.checking": "확인 중",
1904
+ "onboarding.provider.connected": "연결됨",
1905
+ "onboarding.provider.install-required": "설치 필요",
1906
+ "onboarding.provider.login-required": "로그인 필요",
1907
+ "onboarding.provider.failed": "확인 실패",
1908
+ "onboarding.provider.needsConnection": "연결 필요",
1909
+ "firstProject.title": "프로젝트 추가",
1910
+ "firstProject.description": "Saycode를 시작하려면 프로젝트를 추가하세요.",
1911
+ "firstProject.close": "프로젝트 추가 창 닫기",
1912
+ "firstProject.host": "호스트",
1913
+ "firstProject.selectHost": "호스트 선택",
1914
+ "firstProject.noHost": "사용 가능한 머신 없음",
1915
+ "firstProject.thisComputer": "이 컴퓨터",
1916
+ "firstProject.remoteHost": "원격 머신",
1917
+ "firstProject.online": "온라인",
1918
+ "firstProject.offline": "오프라인",
1919
+ "firstProject.addMachine": "다른 머신 추가",
1920
+ "firstProject.addMachineHint": "원격 머신 또는 공유 머신 연결",
1921
+ "firstProject.startMethod": "프로젝트 시작 방법",
1922
+ "firstProject.otherWays": "프로젝트를 추가하는 다른 방법",
1923
+ "firstProject.source.existing-folder.title": "기존 폴더 열기",
1924
+ "firstProject.source.existing-folder.description": "이 컴퓨터나 선택한 머신의 폴더를 사용합니다.",
1925
+ "firstProject.source.zip.title": "ZIP으로 가져오기",
1926
+ "firstProject.source.zip.description": "압축 파일에서 로컬 프로젝트를 만듭니다.",
1927
+ "firstProject.source.git.title": "Git URL에서 복제",
1928
+ "firstProject.source.git.description": "원격 Git 저장소를 선택한 머신에 복제합니다.",
1929
+ "firstProject.source.blank.title": "새 프로젝트 만들기",
1930
+ "firstProject.source.blank.description": "빈 폴더에서 새 프로젝트를 시작합니다.",
1931
+ "firstProject.form.back": "이전",
1932
+ "firstProject.form.machine": "프로젝트를 만들 머신",
1933
+ "firstProject.form.blank.title": "새 프로젝트",
1934
+ "firstProject.form.blank.description": "이름을 정하면 로컬에 새 프로젝트를 만듭니다.",
1935
+ "firstProject.form.git.title": "코드 저장소 가져오기",
1936
+ "firstProject.form.git.description": "Git 주소를 입력하면 이 컴퓨터에 저장소를 복제합니다.",
1937
+ "firstProject.form.existing-folder.title": "기존 폴더 사용",
1938
+ "firstProject.form.existing-folder.description": "로컬 폴더를 Saycode 프로젝트로 연결합니다.",
1939
+ "firstProject.form.zip.title": "ZIP으로 가져오기",
1940
+ "firstProject.form.zip.description": "압축 파일을 선택하면 프로젝트 이름을 자동으로 정합니다.",
1941
+ "firstProject.form.zip.pick": "ZIP 파일 선택",
1942
+ "firstProject.form.zip.drop": "ZIP 파일을 선택하거나 여기로 드래그하세요",
1943
+ "guestRecovery.detailsSummary": "진단 정보 보기",
1944
+ "onboarding.cli.globalInstallTitle": "터미널에서도 Saycode CLI 사용하기",
1945
+ "onboarding.cli.globalInstallBody": "선택 사항입니다. npm으로 전역 happy 명령을 설치합니다: npm i -g @buzzni/happy-cli",
1946
+ "onboarding.cli.globalInstallAction": "전역 CLI 설치",
1947
+ "onboarding.cli.globalInstalling": "설치 중… 몇 분 걸릴 수 있습니다",
1948
+ "onboarding.cli.globalInstalled": "전역 CLI 설치됨 ({version})",
1949
+ "onboarding.cli.globalInstallFailed": "전역 CLI 설치 실패: {detail}",
1950
+ "onboarding.cli.globalInstalledUnversioned": "전역 CLI 설치됨"
1489
1951
  };
1490
1952
  const standaloneRemoteOnboardingKo = {
1491
1953
  "onboarding.optional": " (선택)",
@@ -1596,7 +2058,7 @@ const ko = {
1596
2058
  "happyCliUpdate.cancel": "취소",
1597
2059
  // Boot screen
1598
2060
  "boot.preparing": "Saycode 준비 중...",
1599
- "boot.stage.credentials": "자격 증명 확인 중…",
2061
+ "boot.stage.credentials": "로그인 상태 확인 중…",
1600
2062
  "boot.stage.workspace": "워크스페이스 불러오는 중…",
1601
2063
  // Language selection (first run)
1602
2064
  "language.select.title": "언어를 선택하세요",
@@ -1661,8 +2123,8 @@ const ko = {
1661
2123
  "zipImport.transferFailed": "{count}개 파일을 전송하지 못했습니다. ({path}: {reason})",
1662
2124
  "zipImport.machineInfoMissing": "머신 정보(homeDir/workspaceRoot)가 부족해 가져올 위치를 결정할 수 없습니다.",
1663
2125
  "zipImport.emptyArchive": "압축 파일 안에 가져올 파일이 없습니다.",
1664
- "sidebarPanel.showAllChats": "전체 대화 보기 ({count}개 더 있음)",
1665
- "sidebarPanel.showAllChatsAria": "{project}의 전체 대화 보기",
2126
+ "sidebarPanel.showAllChats": "대화 더보기 ({count}개 남음)",
2127
+ "sidebarPanel.showAllChatsAria": "{project}에서 대화 더 보기",
1666
2128
  "sidebarPanel.newChatAria": "{name} 프로젝트에서 새 대화 시작",
1667
2129
  "home.eyebrow": "워크스페이스 홈",
1668
2130
  "home.newProject": "새 프로젝트",
@@ -1717,7 +2179,6 @@ const ko = {
1717
2179
  "newProject.gitNeedUrl": "Git 저장소 주소를 입력해 주세요.",
1718
2180
  "newProject.importDone": "'{name}' 프로젝트에 파일 {count}개를 가져왔습니다.",
1719
2181
  "newProject.importFailed": "'{name}' 프로젝트는 생성했지만 파일을 가져오지 못했습니다. {detail}",
1720
- "composer.attachFile": "파일 첨부",
1721
2182
  "composer.attachNeedMachine": "첨부하려면 머신 연결이 필요합니다",
1722
2183
  "composer.attachNoMachine": "파일을 업로드할 머신을 찾을 수 없습니다.",
1723
2184
  "composer.attachUploadFailed": "파일을 업로드하지 못했습니다.",
@@ -1744,7 +2205,7 @@ const ko = {
1744
2205
  "workspace.orgAdminMenuEntry": "조직 관리",
1745
2206
  // Org admin console (specs/desktop-org-admin-console)
1746
2207
  "orgAdmin.title": "조직 관리",
1747
- "orgAdmin.eyebrow": "Organization",
2208
+ "orgAdmin.eyebrow": "조직",
1748
2209
  "orgAdmin.close": "닫기",
1749
2210
  "orgAdmin.tab.members": "멤버 관리",
1750
2211
  "orgAdmin.tab.settings": "조직 설정",
@@ -1773,7 +2234,7 @@ const ko = {
1773
2234
  "orgAdmin.settingsSaved": "조직 설정이 저장되었습니다.",
1774
2235
  "orgAdmin.tab.audit": "감사 로그",
1775
2236
  "orgAdmin.tab.machineMonitor": "머신 모니터링",
1776
- "orgAdmin.tab.skill": "Skill",
2237
+ "orgAdmin.tab.skill": "스킬",
1777
2238
  "orgAdmin.tab.projects": "프로젝트",
1778
2239
  "orgAdmin.skillSearch": "Skill 검색",
1779
2240
  "orgAdmin.skillSearchPlaceholder": "Skill 또는 설명 검색...",
@@ -1888,7 +2349,7 @@ const ko = {
1888
2349
  "orgAdmin.githubOwnerPlaceholder": "예: myteam",
1889
2350
  "orgAdmin.label": "레이블 (선택)",
1890
2351
  "orgAdmin.labelPlaceholder": "Main Account, Backup Account 등",
1891
- "orgAdmin.githubPat": "Personal Access Token",
2352
+ "orgAdmin.githubPat": "개인 액세스 토큰",
1892
2353
  "orgAdmin.githubPatRequired": "PAT를 입력하세요.",
1893
2354
  "orgAdmin.githubPatPlaceholder": "github_pat_... 형식의 PAT",
1894
2355
  "orgAdmin.githubPatUnchanged": "변경 없음 (기존 PAT 유지)",
@@ -2011,7 +2472,7 @@ const ko = {
2011
2472
  // Super admin console (specs/desktop-org-admin-console)
2012
2473
  "systemAdmin.menuEntry": "슈퍼 관리자",
2013
2474
  "systemAdmin.title": "슈퍼 관리자",
2014
- "systemAdmin.eyebrow": "System",
2475
+ "systemAdmin.eyebrow": "시스템",
2015
2476
  "systemAdmin.companiesTitle": "전체 조직",
2016
2477
  "systemAdmin.createCompany": "조직 생성",
2017
2478
  "systemAdmin.create": "생성",
@@ -2086,7 +2547,7 @@ const ko = {
2086
2547
  "panel.recovery.loginDesc": "로그인 후 터미널을 시작할 수 있습니다.",
2087
2548
  // Chat composer
2088
2549
  "composer.messageAria": "메시지",
2089
- "composer.placeholder": "데스크톱에서 보낼 메시지를 입력",
2550
+ "composer.placeholder": "메시지를 입력하세요",
2090
2551
  "composer.promptSuggestionPaused": "{window} 사용량 {percent}% — 사용량 경고 중에는 Claude가 추천 질문을 만들지 않습니다",
2091
2552
  "composer.promptSuggestionPausedReset": "{reset} 초기화",
2092
2553
  "composer.starting": "시작 중...",
@@ -2139,6 +2600,7 @@ const ko = {
2139
2600
  "sessionWorktree.branch": "브랜치: {branch}",
2140
2601
  "sessionWorktree.path": "경로: {path}",
2141
2602
  "sessionWorktree.merged": "병합됨",
2603
+ "chatPullRequest.open": "{repository} PR #{number} 열기",
2142
2604
  // Project git controls
2143
2605
  "gitControls.branchLoading": "Git branch 확인 중",
2144
2606
  "gitControls.noRemote": "origin remote 또는 branch를 찾을 수 없습니다.",
@@ -2148,9 +2610,9 @@ const ko = {
2148
2610
  "gitControls.manageBranchTitle": "Branch 변경 및 생성",
2149
2611
  "gitControls.manageBranchTitleWithDesc": "Branch 변경 및 생성 · {description}",
2150
2612
  "gitControls.pullAria": "{branch} 브랜치 pull",
2151
- "gitControls.pullTitle": "{branch} pull",
2613
+ "gitControls.pullTitle": "{branch} 가져오기(pull)",
2152
2614
  "gitControls.pushAria": "{branch} 브랜치 push",
2153
- "gitControls.pushTitle": "{branch} push",
2615
+ "gitControls.pushTitle": "{branch} 올리기(push)",
2154
2616
  "gitControls.manageWorktreeAria": "{name} worktree 관리",
2155
2617
  "gitControls.manageWorktreeTitle": "Worktree 관리",
2156
2618
  "gitControls.operationResultDialogTitle": "{label} 결과",
@@ -2595,6 +3057,12 @@ const ko = {
2595
3057
  // Owner workspace sync (team share) messages
2596
3058
  "ownerSync.hint.credentialsUnavailable": "원격 머신 / 인증 정보가 없어요. 프로젝트 메뉴에서 머신을 한 번 연결한 뒤 다시 시도해 주세요.",
2597
3059
  "ownerSync.successConnectionUpdated": "저장소 연결을 변경했습니다.",
3060
+ "ownerSync.deferred.workspaceDirNotFound": "저장소 연결은 완료됐지만 워크스페이스 폴더를 찾지 못해 코드는 아직 저장하지 않았어요. 프로젝트를 연 뒤 다시 저장해 주세요.",
3061
+ "ownerSync.deferred.workspaceEmpty": "저장소 연결은 완료됐지만 저장할 파일이 없어 코드는 아직 저장하지 않았어요. 파일을 만든 뒤 다시 저장해 주세요.",
3062
+ "ownerSync.deferred.credentialsUnavailable": "저장소 연결은 완료됐지만 머신 자격 증명을 사용할 수 없어 코드는 아직 저장하지 않았어요. 자격 증명을 확인한 뒤 다시 저장해 주세요.",
3063
+ "ownerSync.deferred.machineHomeUnresolvable": "저장소 연결은 완료됐지만 머신 홈 폴더를 확인할 수 없어 코드는 아직 저장하지 않았어요. 머신 홈 폴더를 확인한 뒤 다시 저장해 주세요.",
3064
+ "ownerSync.deferred.machineUnavailable": "저장소 연결은 완료됐지만 머신이 오프라인이라 코드는 아직 저장하지 않았어요. 머신을 다시 연결한 뒤 저장해 주세요.",
3065
+ "ownerSync.deferred.default": "저장소 연결은 완료됐지만 코드는 아직 저장하지 않았어요. 프로젝트와 머신 상태를 확인한 뒤 다시 저장해 주세요.",
2598
3066
  "ownerSync.hint.machineHomeUnresolvable": "원격 머신을 찾을 수 없어요. happy daemon 이 실행 중인지 확인해 주세요.",
2599
3067
  "ownerSync.hint.machineUnavailable": "원격 머신 daemon 이 오프라인이에요. happy daemon 을 시작한 뒤 다시 시도해 주세요.",
2600
3068
  "ownerSync.hint.missingTools": "원격 머신에 git / tar 가 설치되어 있지 않아요. 운영자에게 설치 요청을 보내주세요.",
@@ -2857,6 +3325,22 @@ const ko = {
2857
3325
  "community.cloneFailed": "커뮤니티 프로젝트를 받아오지 못했습니다.",
2858
3326
  "login.credentialsRequired": "이메일과 비밀번호를 입력해주세요.",
2859
3327
  "login.loginFailed": "로그인에 실패했습니다.",
3328
+ "login.mfaTitle": "추가 인증",
3329
+ "login.mfaLead": "이 계정은 2단계 인증이 필요합니다. 인증 앱의 6자리 코드를 입력해주세요.",
3330
+ "login.mfaMethodsLead": "이 계정은 2단계 인증이 필요합니다. 패스키 또는 등록한 인증 앱으로 인증해주세요.",
3331
+ "login.mfaPasskey": "패스키로 인증",
3332
+ "login.mfaPasskeyRetry": "패스키 인증 다시 열기",
3333
+ "login.mfaPasskeyWaiting": "시스템 브라우저에서 iPhone 또는 이 Mac의 패스키 인증을 완료해주세요.",
3334
+ "login.mfaTotpFallback": "브라우저를 닫아도 인증 앱 코드로 계속할 수 있습니다.",
3335
+ "login.mfaCodeLabel": "인증 코드",
3336
+ "login.mfaCodePlaceholder": "6자리 숫자",
3337
+ "login.mfaSubmit": "인증하고 로그인",
3338
+ "login.mfaCancel": "다른 계정으로 로그인",
3339
+ "login.mfaInvalidCode": "인증 코드가 올바르지 않습니다. 다시 시도해주세요.",
3340
+ "login.mfaExpired": "인증 시간이 만료되었습니다. 다시 로그인해주세요.",
3341
+ "login.mfaRateLimited": "시도가 너무 많습니다. 잠시 후 다시 시도해주세요.",
3342
+ "login.mfaUnsupported": "이 계정에서 사용할 수 있는 2단계 인증 방법이 없습니다. 웹에서 인증 앱 또는 패스키를 등록한 뒤 다시 시도해주세요.",
3343
+ "login.mfaPasskeyFailed": "패스키 인증을 시작하지 못했습니다. 다시 시도하거나 인증 앱 코드를 사용해주세요.",
2860
3344
  "standalone.startFailed": "로컬 standalone 서버를 시작하지 못했습니다.",
2861
3345
  "workspace.loadFailed": "워크스페이스를 불러오지 못했습니다.",
2862
3346
  "standalone.modeStartFailed": "로컬 standalone 모드를 시작하지 못했습니다.",
@@ -3044,6 +3528,12 @@ const ko = {
3044
3528
  "worktreeOp.merging": "머지 중...",
3045
3529
  "worktreeOp.mergeFailed": "머지에 실패했습니다.",
3046
3530
  "worktreeOp.mergeDone": "머지 완료. 이제 worktree를 정리할 수 있습니다.",
3531
+ "worktreeOp.demoAutoMergeBlocked": "데모 모드에서는 auto-merge를 사용할 수 없습니다.",
3532
+ "worktreeOp.autoMergeEnabling": "auto-merge 켜는 중...",
3533
+ "worktreeOp.autoMergeDisabling": "auto-merge 끄는 중...",
3534
+ "worktreeOp.autoMergeEnabled": "auto-merge가 켜졌습니다. 머지 조건(체크·필수 리뷰 등)을 모두 충족하면 자동으로 머지됩니다.",
3535
+ "worktreeOp.autoMergeDisabled": "auto-merge가 꺼졌습니다.",
3536
+ "worktreeOp.autoMergeFailed": "auto-merge 설정을 바꾸지 못했습니다.",
3047
3537
  "worktreeOp.checkingStatus": "worktree 상태 확인 중...",
3048
3538
  "worktreeOp.forceDeleteUnpushed": "worktree에 푸시되지 않은 커밋 {count}개가 있습니다. 강제로 삭제할까요? 커밋이 사라집니다.",
3049
3539
  "worktreeOp.forceDeleteDirty": "worktree에 정리되지 않은 변경이 있습니다. 강제로 삭제할까요? 변경 사항이 사라집니다.",
@@ -3665,6 +4155,8 @@ const ko = {
3665
4155
  // ProjectDocumentViewer
3666
4156
  "inlinePreview.openInViewer": "파일 뷰어로 열기",
3667
4157
  "inlinePreview.loading": "미리보기를 불러오는 중",
4158
+ "inlinePreview.loadFailed": "미리보기를 불러오지 못했습니다.",
4159
+ "inlinePreview.retry": "다시 시도",
3668
4160
  "documentViewer.docxConvertFailed": "DOCX 문서를 변환하지 못했습니다.",
3669
4161
  "documentViewer.pdfViewer": "{name} PDF 뷰어",
3670
4162
  "documentViewer.pdfPreparing": "PDF를 준비하는 중...",
@@ -3715,7 +4207,7 @@ const ko = {
3715
4207
  "connectors.browserHint": "브라우저에서 승인을 마친 뒤 앱으로 돌아오세요.",
3716
4208
  "connectors.availableNextTurn": "연결됐습니다. 다음 메시지부터 사용할 수 있습니다.",
3717
4209
  "connectors.readOnlyNote": "읽기 전용으로만 연결합니다.",
3718
- "connectors.apiKeyLabel": "KNOI API Key",
4210
+ "connectors.apiKeyLabel": "KNOI API 키",
3719
4211
  "connectors.apiKeyEnter": "API Key 입력",
3720
4212
  "connectors.apiKeyReplace": "API Key 교체",
3721
4213
  "connectors.apiKeySave": "API Key 저장",
@@ -3728,7 +4220,7 @@ const ko = {
3728
4220
  "remoteFile.saveFailed": "파일을 저장하지 못했습니다.",
3729
4221
  "fileDownload.invalidData": "다운로드할 파일 데이터가 올바르지 않습니다.",
3730
4222
  "fileDownload.tooLarge": "다운로드 파일은 100 MiB 이하만 지원합니다.",
3731
- "codex.needsLatestCli": "GPT-5.6을 사용하려면 최신 Codex CLI가 필요합니다. `npm install -g @openai/codex@latest`로 업데이트한 뒤 새 대화를 시작해 주세요.",
4223
+ "codex.needsLatestCli": "선택한 모델을 사용하려면 최신 Codex CLI가 필요합니다. `npm install -g @openai/codex@latest`로 업데이트한 뒤 새 대화를 시작해 주세요.",
3732
4224
  "codex.error": "Codex 오류: {detail}",
3733
4225
  // specs/desktop-error-surface — 실패 알림의 계층 배지와 복구 액션
3734
4226
  "errorSurface.layer.provider": "Provider 오류",
@@ -3858,7 +4350,7 @@ const ko = {
3858
4350
  "settingsTab.network": "네트워크 데이터 절약",
3859
4351
  "settingsTab.env": "환경 변수",
3860
4352
  "settingsTab.security": "보안",
3861
- "settingsTab.memory": "Memory",
4353
+ "settingsTab.memory": "메모리",
3862
4354
  "settingsTab.connectors": "커넥터",
3863
4355
  "settingsTab.myAiCredentials": "내 AI 계정",
3864
4356
  "settingsTab.extensions": "확장",
@@ -3891,7 +4383,9 @@ const ko = {
3891
4383
  "extensions.developmentSource": "개발 빌드: {directory}",
3892
4384
  "extensions.apiDeprecated": "업데이트 필요",
3893
4385
  "extensions.apiDeprecatedHint": "이전 확장 API({apiVersion})로 동작 중이며, 다음 업데이트에서 지원이 끝납니다.",
3894
- "extensions.runtime.inactive": "비활성",
4386
+ "extensions.enablement.enabled": "활성화됨",
4387
+ "extensions.enablement.disabled": "비활성화됨",
4388
+ "extensions.runtime.inactive": "대기 중",
3895
4389
  "extensions.runtime.active": "실행 중",
3896
4390
  "extensions.runtime.quarantined": "격리됨",
3897
4391
  "shortcuts.title": "단축키",
@@ -3984,7 +4478,7 @@ const ko = {
3984
4478
  "review.self.disabledWaiting": "먼저 대기 중인 질문이나 권한 요청에 응답해주세요.",
3985
4479
  "review.self.rounds": "반복 횟수",
3986
4480
  "review.self.roundsHint": "medium 이상 지적이 나오면 남은 횟수만큼 리뷰를 자동으로 이어갑니다. low·nit만 남으면 바로 끝냅니다.",
3987
- "review.self.appOpenHint": "앱이 실행 중일 때만 이어집니다. 앱을 완전히 종료하면 멈추고, 다시 열면 남은 횟수부터 이어갑니다.",
4481
+ "review.self.appOpenHint": "상시 실행 에이전트가 최신 버전이면 앱을 완전히 종료해도 계속됩니다. 지원하지 않는 환경에서는 앱이 켜져 있어야 합니다.",
3988
4482
  "review.self.loopActive": "반복 리뷰 {round}/{total} 진행 중",
3989
4483
  "review.self.loopStop": "반복 중지",
3990
4484
  "review.self.loopDismiss": "기록 지우기",
@@ -3995,7 +4489,7 @@ const ko = {
3995
4489
  "review.self.loopReason.roundsExhausted": "지정한 횟수를 모두 사용",
3996
4490
  "review.self.loopReason.unstructured": "결과를 판정할 수 없어 중단",
3997
4491
  "review.self.loopReason.sendFailed": "다음 라운드를 보내지 못함",
3998
- "review.self.loopReason.cancelled": "다른 메시지가 들어와 중단",
4492
+ "review.self.loopReason.cancelled": "사용자 요청으로 중단",
3999
4493
  "review.history.title": "독립 리뷰 기록",
4000
4494
  "review.storage.writeFailed": "이 기기에 리뷰 기록을 저장하지 못했습니다. 앱을 다시 켜면 이 목록이 비어 있을 수 있습니다.",
4001
4495
  "review.recovery.title": "기록 복구",
@@ -4055,7 +4549,7 @@ const ko = {
4055
4549
  "review.error.sourceProjectMissing": "리뷰 원본 프로젝트를 찾을 수 없습니다.",
4056
4550
  "review.error.sourceSessionMissing": "리뷰 원본 대화를 찾을 수 없습니다.",
4057
4551
  // Project processes (specs/desktop-process-ownership R4)
4058
- "projectProcesses.eyebrow": "Running Processes",
4552
+ "projectProcesses.eyebrow": "실행 중인 프로세스",
4059
4553
  "projectProcesses.dialogAria": "{name} 프로세스 보기",
4060
4554
  "projectProcesses.closeAria": "프로세스 보기 닫기",
4061
4555
  "projectProcesses.listAria": "{name}의 실행 중인 프로세스 목록",
@@ -4233,6 +4727,7 @@ function interruptDecision(state) {
4233
4727
  return state.phase === "idle" ? "exit-cli" : "open-menu";
4234
4728
  }
4235
4729
  const AGENT_UNTIL_VALUES = ["turn-end", "waiting-input", "completed"];
4730
+ const STATE_WINDOW_MESSAGES = 50;
4236
4731
  const DEFAULT_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
4237
4732
  const MAX_WAIT_TIMEOUT_MS = 60 * 60 * 1e3;
4238
4733
  function isAgentUntil(value2) {
@@ -4433,6 +4928,12 @@ function collectFlags(argv, startIndex) {
4433
4928
  flags.last = takeNumber(argv, index, token);
4434
4929
  index += 1;
4435
4930
  break;
4931
+ case "--status":
4932
+ flags.status = true;
4933
+ break;
4934
+ case "--any":
4935
+ flags.any = true;
4936
+ break;
4436
4937
  case "--wait":
4437
4938
  flags.wait = true;
4438
4939
  break;
@@ -4588,12 +5089,13 @@ Options
4588
5089
 
4589
5090
  Agent commands (machine-readable JSON on stdout, exit 3 on failure)
4590
5091
  agent whoami Show this session's contact point and limits
4591
- agent ls List sibling sessions in the same directory tree
4592
- agent read <session> [--last n] Recent messages + completion signal
5092
+ agent ls [--status] List sessions in scope; --status adds state/pending/cursor
5093
+ agent read <session> [--last n] [--since seq] Recent messages + completion signal
4593
5094
  agent wait <session> --until <state> turn-end | waiting-input | completed
5095
+ agent wait --any <s1> <s2>... --until <state> Block until any of them holds it
4594
5096
  agent prompt <session> <text> [--wait] Send a prompt, optionally wait for it
4595
- agent steer <session> <text> Redirect an owned child's active response
4596
- agent stop <session> Stop an owned child's active response
5097
+ agent steer <session> <text> Redirect an active response (depth 0: any in scope)
5098
+ agent stop <session> Stop an active response (depth 0: any in scope)
4597
5099
  agent spawn --prompt <text> [--agent n] [--model n] [--effort n]
4598
5100
  Start a sibling with an explicit execution choice
4599
5101
  agent spawn --prompt <text> --file <path> ...with context files it reads first
@@ -6983,6 +7485,87 @@ function bounded(value2, maxLength) {
6983
7485
  if (typeof value2 !== "string" || value2.length === 0 || value2.length > maxLength) return void 0;
6984
7486
  return value2;
6985
7487
  }
7488
+ function boundedAllowEmpty(value2, maxLength) {
7489
+ if (typeof value2 !== "string" || value2.length > maxLength) return void 0;
7490
+ return value2;
7491
+ }
7492
+ function parsePasswordLoginResponse(raw) {
7493
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && raw.mfaRequired === true) {
7494
+ const value2 = raw;
7495
+ const loginToken = bounded(value2.loginToken, 512);
7496
+ if (loginToken === void 0) throw new Error("CLOUD_LOGIN_RESPONSE_INVALID");
7497
+ const methods = Array.isArray(value2.methods) ? value2.methods.filter((method) => typeof method === "string") : [];
7498
+ return {
7499
+ kind: "mfa-required",
7500
+ challenge: {
7501
+ loginToken,
7502
+ ...typeof value2.expiresAt === "string" ? { expiresAt: value2.expiresAt } : {},
7503
+ methods,
7504
+ // 형이 어긋나면 보수적으로 "등록 필요" — 코드 입력을 헛되이 시키지 않는다.
7505
+ enrollmentRequired: value2.enrollmentRequired !== false
7506
+ }
7507
+ };
7508
+ }
7509
+ return { kind: "success", result: parseCloudLoginResult(raw) };
7510
+ }
7511
+ function parseKdfParams(value2) {
7512
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return void 0;
7513
+ const entries = Object.entries(value2);
7514
+ if (entries.length > 8) return void 0;
7515
+ const parsed = {};
7516
+ for (const [key, entry] of entries) {
7517
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(key)) return void 0;
7518
+ if (typeof entry === "string") {
7519
+ if (entry.length > 128) return void 0;
7520
+ } else if (typeof entry !== "number" && typeof entry !== "boolean") return void 0;
7521
+ parsed[key] = entry;
7522
+ }
7523
+ return parsed;
7524
+ }
7525
+ function parseUnlockMethod(value2) {
7526
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return void 0;
7527
+ const method = value2;
7528
+ const id = bounded(method.id, 128);
7529
+ const type = method.type;
7530
+ const kdfAlgorithm = bounded(method.kdfAlgorithm, 128);
7531
+ const kdfParams = parseKdfParams(method.kdfParams);
7532
+ const salt = boundedAllowEmpty(method.salt, 256);
7533
+ const wrappedMasterKey = bounded(method.wrappedMasterKey, 16384);
7534
+ if (!id || !/^[A-Za-z0-9_-]+$/.test(id) || type !== "password" && type !== "recovery" && type !== "device" || !kdfAlgorithm || !kdfParams || salt === void 0 || !wrappedMasterKey) return void 0;
7535
+ const revision = method.revision;
7536
+ if (revision !== void 0 && (!Number.isSafeInteger(revision) || revision < 1)) return void 0;
7537
+ return {
7538
+ id,
7539
+ type,
7540
+ kdfAlgorithm,
7541
+ kdfParams,
7542
+ salt,
7543
+ wrappedMasterKey,
7544
+ ...revision === void 0 ? {} : { revision }
7545
+ };
7546
+ }
7547
+ function parseBundledAccountKey(value2) {
7548
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return void 0;
7549
+ const payload = value2;
7550
+ if (payload.mode !== "bundled" || !payload.material || typeof payload.material !== "object") return void 0;
7551
+ const material = payload.material;
7552
+ const keyVersion = material.keyVersion;
7553
+ const publicKey = bounded(material.publicKey, 128);
7554
+ const encryptedPrivateKey = bounded(material.encryptedPrivateKey, 16384);
7555
+ if (!Number.isSafeInteger(keyVersion) || keyVersion < 1 || !publicKey || !encryptedPrivateKey || !Array.isArray(material.unlockMethods) || material.unlockMethods.length > 16) return void 0;
7556
+ return {
7557
+ mode: "bundled",
7558
+ material: {
7559
+ keyVersion,
7560
+ publicKey,
7561
+ encryptedPrivateKey,
7562
+ unlockMethods: material.unlockMethods.flatMap((method) => {
7563
+ const parsed = parseUnlockMethod(method);
7564
+ return parsed ? [parsed] : [];
7565
+ })
7566
+ }
7567
+ };
7568
+ }
6986
7569
  function parseCloudLoginResult(raw) {
6987
7570
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6988
7571
  throw new Error("CLOUD_LOGIN_RESPONSE_INVALID");
@@ -6995,15 +7578,58 @@ function parseCloudLoginResult(raw) {
6995
7578
  };
6996
7579
  const accountPublicKey = bounded(value2.accountPublicKey, 128);
6997
7580
  const accountKeySalt = bounded(value2.accountKeySalt, 128);
7581
+ const mfaSessionToken = bounded(value2.mfaSessionToken, 512);
7582
+ const accountKey = parseBundledAccountKey(value2.accountKey);
6998
7583
  return {
6999
7584
  userId: required("userId", 256),
7000
7585
  username: required("username", 256),
7001
7586
  token: required("token", 8192),
7002
7587
  secret: required("secret", 8192),
7588
+ ...mfaSessionToken ? { mfaSessionToken } : {},
7003
7589
  ...accountPublicKey ? { accountPublicKey } : {},
7004
- ...accountKeySalt ? { accountKeySalt } : {}
7590
+ ...accountKeySalt ? { accountKeySalt } : {},
7591
+ ...accountKey ? { accountKey } : {}
7005
7592
  };
7006
7593
  }
7594
+ const HANDOFF_ID_PATTERN = /^mph-[A-Za-z0-9_-]{22}$/;
7595
+ const CAPABILITY_PATTERN = /^[A-Za-z0-9_-]{43}$/;
7596
+ function record$5(value2) {
7597
+ return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
7598
+ }
7599
+ function validExpiry(value2) {
7600
+ return typeof value2 === "number" && Number.isSafeInteger(value2) && value2 > 0;
7601
+ }
7602
+ function parseMfaPasskeyHandoff(raw, apiBaseUrl) {
7603
+ const value2 = record$5(raw);
7604
+ if (!value2 || typeof value2.handoffId !== "string" || !HANDOFF_ID_PATTERN.test(value2.handoffId) || typeof value2.exchangeToken !== "string" || !CAPABILITY_PATTERN.test(value2.exchangeToken) || typeof value2.authorizationUrl !== "string" || !validExpiry(value2.expiresAt)) {
7605
+ throw new Error("MFA_PASSKEY_HANDOFF_RESPONSE_INVALID");
7606
+ }
7607
+ let authorizationUrl;
7608
+ try {
7609
+ authorizationUrl = new URL(value2.authorizationUrl);
7610
+ const expectedOrigin = new URL(apiBaseUrl).origin;
7611
+ const fragmentParams = new URLSearchParams(authorizationUrl.hash.slice(1));
7612
+ const fragmentToken = fragmentParams.get("browserToken");
7613
+ if (authorizationUrl.origin !== expectedOrigin || authorizationUrl.username !== "" || authorizationUrl.password !== "" || authorizationUrl.pathname !== "/mfa/passkey-handoff" || [...authorizationUrl.searchParams].length !== 1 || authorizationUrl.searchParams.get("handoffId") !== value2.handoffId || [...fragmentParams].length !== 1 || !fragmentToken || !CAPABILITY_PATTERN.test(fragmentToken) || authorizationUrl.href.includes(value2.exchangeToken)) {
7614
+ throw new Error("invalid");
7615
+ }
7616
+ } catch {
7617
+ throw new Error("MFA_PASSKEY_HANDOFF_RESPONSE_INVALID");
7618
+ }
7619
+ return {
7620
+ handoffId: value2.handoffId,
7621
+ authorizationUrl: authorizationUrl.toString(),
7622
+ exchangeToken: value2.exchangeToken,
7623
+ expiresAt: value2.expiresAt
7624
+ };
7625
+ }
7626
+ function parsePendingMfaPasskeyExchange(raw) {
7627
+ const value2 = record$5(raw);
7628
+ if (!value2 || value2.status !== "pending" || !validExpiry(value2.expiresAt)) {
7629
+ throw new Error("MFA_PASSKEY_HANDOFF_RESPONSE_INVALID");
7630
+ }
7631
+ return { kind: "pending", expiresAt: value2.expiresAt };
7632
+ }
7007
7633
  const ENVELOPE_VERSION = 0;
7008
7634
  const EPHEMERAL_PUBLIC_KEY_LENGTH = 32;
7009
7635
  const ENVELOPE_NONCE_LENGTH = 24;
@@ -7059,9 +7685,21 @@ async function encryptDataKeyContent(dataEncryptionKey, value2) {
7059
7685
  bundle.set(ciphertextWithTag, 1 + GCM_NONCE_LENGTH);
7060
7686
  return encodeBase64(bundle);
7061
7687
  }
7688
+ let candidates = [];
7689
+ let version = 0;
7690
+ function getCompanyKeyCandidates() {
7691
+ return candidates.map((candidate) => candidate.privateKey);
7692
+ }
7693
+ function companyKeyChainVersion() {
7694
+ return version;
7695
+ }
7062
7696
  const dekBySessionId = /* @__PURE__ */ new Map();
7063
7697
  const lockedSessionIds = /* @__PURE__ */ new Set();
7064
7698
  const fingerprints = /* @__PURE__ */ new Map();
7699
+ let boundaryGeneration = 0;
7700
+ function sessionDekRegistryGeneration() {
7701
+ return boundaryGeneration;
7702
+ }
7065
7703
  function registerSessionDek(session, accountPrivateKey) {
7066
7704
  const envelope = session.dataEncryptionKey ?? null;
7067
7705
  if (!envelope) {
@@ -7070,17 +7708,25 @@ function registerSessionDek(session, accountPrivateKey) {
7070
7708
  fingerprints.delete(session.id);
7071
7709
  return;
7072
7710
  }
7073
- const fingerprint = `${envelope}|${accountPrivateKey ? "1" : "0"}`;
7711
+ const fingerprint = `${envelope}|${accountPrivateKey ? "1" : "0"}|${companyKeyChainVersion()}`;
7074
7712
  if (fingerprints.get(session.id) === fingerprint) return;
7713
+ if (!accountPrivateKey && dekBySessionId.has(session.id) && fingerprints.get(session.id)?.startsWith(`${envelope}|1|`)) return;
7714
+ const previousFingerprint = fingerprints.get(session.id);
7075
7715
  fingerprints.set(session.id, fingerprint);
7076
- const dek = accountPrivateKey ? unwrapDataEncryptionKey(envelope, accountPrivateKey) : null;
7716
+ const candidates2 = accountPrivateKey ? [accountPrivateKey, ...getCompanyKeyCandidates()] : [];
7717
+ let dek = null;
7718
+ for (const candidate of candidates2) {
7719
+ dek = unwrapDataEncryptionKey(envelope, candidate);
7720
+ if (dek) break;
7721
+ }
7077
7722
  if (dek) {
7078
7723
  dekBySessionId.set(session.id, dek);
7079
7724
  lockedSessionIds.delete(session.id);
7080
- } else {
7081
- dekBySessionId.delete(session.id);
7082
- lockedSessionIds.add(session.id);
7725
+ return;
7083
7726
  }
7727
+ if (dekBySessionId.has(session.id) && previousFingerprint?.startsWith(`${envelope}|`)) return;
7728
+ dekBySessionId.delete(session.id);
7729
+ lockedSessionIds.add(session.id);
7084
7730
  }
7085
7731
  function getSessionDek(sessionId) {
7086
7732
  return dekBySessionId.get(sessionId) ?? null;
@@ -7105,18 +7751,20 @@ function cacheSet(store, sessionId, ciphertext, plaintext) {
7105
7751
  function getCachedDataKeyMetadataJson(sessionId, ciphertext) {
7106
7752
  return cacheGet(headerCache, sessionId, ciphertext);
7107
7753
  }
7108
- async function preDecryptSessionHeaders(sessions, accountPrivateKey) {
7754
+ async function preDecryptSessionHeaders(sessions, accountPrivateKey, sinceGeneration = sessionDekRegistryGeneration()) {
7755
+ const boundaryIntact = () => sessionDekRegistryGeneration() === sinceGeneration;
7109
7756
  await Promise.all(sessions.map(async (session) => {
7757
+ if (!boundaryIntact()) return;
7110
7758
  registerSessionDek(session, accountPrivateKey);
7111
7759
  const dek = getSessionDek(session.id);
7112
7760
  if (!dek) return;
7113
7761
  if (session.metadata && getCachedDataKeyMetadataJson(session.id, session.metadata) === null) {
7114
7762
  const json = await decryptDataKeyToString(dek, session.metadata);
7115
- if (json !== null) cacheSet(headerCache, session.id, session.metadata, json);
7763
+ if (json !== null && boundaryIntact()) cacheSet(headerCache, session.id, session.metadata, json);
7116
7764
  }
7117
7765
  if (session.agentState && getCachedDataKeyMetadataJson(session.id, session.agentState) === null) {
7118
7766
  const json = await decryptDataKeyToString(dek, session.agentState);
7119
- if (json !== null) cacheSet(headerCache, session.id, session.agentState, json);
7767
+ if (json !== null && boundaryIntact()) cacheSet(headerCache, session.id, session.agentState, json);
7120
7768
  }
7121
7769
  }));
7122
7770
  }
@@ -7238,69 +7886,69 @@ function personalMachinesPath(companyId) {
7238
7886
  function myMachinesPath(companyId) {
7239
7887
  return `/api/admin/companies/${encodeURIComponent(companyId)}/my-machines`;
7240
7888
  }
7241
- function record$2(value2) {
7889
+ function record$4(value2) {
7242
7890
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
7243
7891
  }
7244
7892
  function array(value2) {
7245
7893
  return Array.isArray(value2) ? value2 : [];
7246
7894
  }
7247
- function str(value2, fallback = "") {
7895
+ function str$1(value2, fallback = "") {
7248
7896
  return typeof value2 === "string" ? value2 : fallback;
7249
7897
  }
7250
- function num(value2, fallback = 0) {
7898
+ function num$1(value2, fallback = 0) {
7251
7899
  return typeof value2 === "number" && Number.isFinite(value2) ? value2 : fallback;
7252
7900
  }
7253
7901
  function nullableNum(value2) {
7254
7902
  return typeof value2 === "number" && Number.isFinite(value2) ? value2 : null;
7255
7903
  }
7256
7904
  function provider(value2) {
7257
- return PROVIDERS.includes(str(value2)) ? str(value2) : null;
7905
+ return PROVIDERS.includes(str$1(value2)) ? str$1(value2) : null;
7258
7906
  }
7259
7907
  function parseBundle(value2) {
7260
- const row2 = record$2(value2);
7261
- const id = str(row2.id);
7908
+ const row2 = record$4(value2);
7909
+ const id = str$1(row2.id);
7262
7910
  const bundleProvider = provider(row2.provider);
7263
7911
  if (!id || !bundleProvider) return null;
7264
7912
  return {
7265
7913
  id,
7266
- companyId: str(row2.companyId),
7914
+ companyId: str$1(row2.companyId),
7267
7915
  provider: bundleProvider,
7268
- label: str(row2.label, id),
7269
- formatVersion: num(row2.formatVersion, 1),
7270
- accountCount: num(row2.accountCount),
7271
- contentHash: str(row2.contentHash),
7272
- version: num(row2.version, 1),
7273
- createdById: str(row2.createdById),
7274
- createdAt: num(row2.createdAt),
7275
- updatedAt: num(row2.updatedAt)
7916
+ label: str$1(row2.label, id),
7917
+ formatVersion: num$1(row2.formatVersion, 1),
7918
+ accountCount: num$1(row2.accountCount),
7919
+ contentHash: str$1(row2.contentHash),
7920
+ version: num$1(row2.version, 1),
7921
+ createdById: str$1(row2.createdById),
7922
+ createdAt: num$1(row2.createdAt),
7923
+ updatedAt: num$1(row2.updatedAt)
7276
7924
  };
7277
7925
  }
7278
7926
  function parseDeployment(value2) {
7279
- const row2 = record$2(value2);
7280
- const machineId = str(row2.machineId);
7281
- const bundleId = str(row2.bundleId);
7927
+ const row2 = record$4(value2);
7928
+ const machineId = str$1(row2.machineId);
7929
+ const bundleId = str$1(row2.bundleId);
7282
7930
  if (!machineId || !bundleId) return null;
7283
- const status = str(row2.status);
7931
+ const status = str$1(row2.status);
7284
7932
  return {
7285
- id: str(row2.id),
7286
- companyId: str(row2.companyId),
7933
+ id: str$1(row2.id),
7934
+ companyId: str$1(row2.companyId),
7287
7935
  bundleId,
7288
7936
  machineId,
7289
7937
  bundleVersion: nullableNum(row2.bundleVersion),
7290
- attemptedBundleVersion: num(row2.attemptedBundleVersion, num(row2.bundleVersion)),
7938
+ attemptedBundleVersion: num$1(row2.attemptedBundleVersion, num$1(row2.bundleVersion)),
7291
7939
  status: status === "deployed" || status === "failed" || status === "pending" ? status : "pending",
7292
7940
  errorKind: typeof row2.errorKind === "string" ? row2.errorKind : null,
7293
7941
  deployedAt: nullableNum(row2.deployedAt),
7294
- createdAt: num(row2.createdAt),
7295
- updatedAt: num(row2.updatedAt)
7942
+ createdAt: num$1(row2.createdAt),
7943
+ updatedAt: num$1(row2.updatedAt)
7296
7944
  };
7297
7945
  }
7298
7946
  function parseStatus(value2) {
7299
- const row2 = record$2(value2);
7947
+ const row2 = record$4(value2);
7300
7948
  const statusProvider = provider(row2.provider);
7301
7949
  if (!statusProvider) return void 0;
7302
- const rotationRow = record$2(row2.rotation);
7303
- const state = str(rotationRow.state);
7950
+ const rotationRow = record$4(row2.rotation);
7951
+ const state = str$1(rotationRow.state);
7304
7952
  const rotation = ROTATION_STATES.includes(state) ? {
7305
7953
  state,
7306
7954
  lastErrorKind: typeof rotationRow.lastErrorKind === "string" ? rotationRow.lastErrorKind : null,
@@ -7320,11 +7968,11 @@ function parseStatus(value2) {
7320
7968
  };
7321
7969
  }
7322
7970
  function parseMachineResult(value2) {
7323
- const row2 = record$2(value2);
7324
- const machineId = str(row2.machineId);
7971
+ const row2 = record$4(value2);
7972
+ const machineId = str$1(row2.machineId);
7325
7973
  if (!machineId) return null;
7326
7974
  if (row2.ok !== true) {
7327
- return { machineId, ok: false, errorKind: str(row2.errorKind, "UNKNOWN_ERROR") };
7975
+ return { machineId, ok: false, errorKind: str$1(row2.errorKind, "UNKNOWN_ERROR") };
7328
7976
  }
7329
7977
  const status = parseStatus(row2.status);
7330
7978
  return {
@@ -7335,54 +7983,54 @@ function parseMachineResult(value2) {
7335
7983
  };
7336
7984
  }
7337
7985
  function parseAiCredentialVaultSnapshot(value2) {
7338
- const body = record$2(value2);
7986
+ const body = record$4(value2);
7339
7987
  return {
7340
7988
  bundles: array(body.bundles).map(parseBundle).filter((bundle) => bundle !== null),
7341
7989
  deployments: array(body.deployments).map(parseDeployment).filter((deployment) => deployment !== null)
7342
7990
  };
7343
7991
  }
7344
7992
  function parseAiCredentialCaptureResponse(value2) {
7345
- const body = record$2(value2);
7993
+ const body = record$4(value2);
7346
7994
  const bundle = parseBundle(body.bundle);
7347
7995
  if (!bundle) throw new Error("AI credential capture response did not contain a bundle");
7348
7996
  return { bundle, warnings: array(body.warnings).filter((warning) => typeof warning === "string") };
7349
7997
  }
7350
7998
  function parseAiCredentialBatchResult(value2) {
7351
- const body = record$2(value2);
7999
+ const body = record$4(value2);
7352
8000
  return {
7353
8001
  results: array(body.results).map(parseMachineResult).filter((result) => result !== null),
7354
8002
  warnings: array(body.warnings).filter((warning) => typeof warning === "string")
7355
8003
  };
7356
8004
  }
7357
8005
  function parseSelfServiceMachines(value2) {
7358
- return array(record$2(value2).machines).map((entry) => {
7359
- const row2 = record$2(entry);
8006
+ return array(record$4(value2).machines).map((entry) => {
8007
+ const row2 = record$4(entry);
7360
8008
  return {
7361
- machineId: str(row2.machineId),
8009
+ machineId: str$1(row2.machineId),
7362
8010
  type: row2.type === "shared" ? "shared" : "personal",
7363
8011
  displayName: typeof row2.displayName === "string" ? row2.displayName : null,
7364
8012
  host: typeof row2.host === "string" ? row2.host : null,
7365
8013
  workspaceRoot: typeof row2.workspaceRoot === "string" ? row2.workspaceRoot : null,
7366
- registeredAt: num(row2.registeredAt),
8014
+ registeredAt: num$1(row2.registeredAt),
7367
8015
  online: typeof row2.online === "boolean" ? row2.online : null
7368
8016
  };
7369
8017
  }).filter((machine) => machine.machineId !== "");
7370
8018
  }
7371
8019
  function parsePersonalMachines(value2) {
7372
- return array(record$2(value2).machines).map((entry) => {
7373
- const row2 = record$2(entry);
8020
+ return array(record$4(value2).machines).map((entry) => {
8021
+ const row2 = record$4(entry);
7374
8022
  const type = row2.type === "shared" ? "shared" : "personal";
7375
8023
  return {
7376
- machineId: str(row2.machineId),
7377
- ownerId: str(row2.ownerId),
8024
+ machineId: str$1(row2.machineId),
8025
+ ownerId: str$1(row2.ownerId),
7378
8026
  ownerUsername: typeof row2.ownerUsername === "string" ? row2.ownerUsername : null,
7379
8027
  ownerDisplayName: typeof row2.ownerDisplayName === "string" ? row2.ownerDisplayName : null,
7380
8028
  type,
7381
8029
  accessList: array(row2.accessList).filter((id) => typeof id === "string"),
7382
8030
  displayName: typeof row2.displayName === "string" ? row2.displayName : null,
7383
8031
  workspaceRoot: typeof row2.workspaceRoot === "string" ? row2.workspaceRoot : null,
7384
- registeredAt: num(row2.registeredAt),
7385
- updatedAt: num(row2.updatedAt),
8032
+ registeredAt: num$1(row2.registeredAt),
8033
+ updatedAt: num$1(row2.updatedAt),
7386
8034
  online: typeof row2.online === "boolean" ? row2.online : null,
7387
8035
  activeAt: nullableNum(row2.activeAt),
7388
8036
  host: typeof row2.host === "string" ? row2.host : null,
@@ -7395,6 +8043,144 @@ function parsePersonalMachines(value2) {
7395
8043
  };
7396
8044
  }).filter((machine) => machine.machineId !== "");
7397
8045
  }
8046
+ const orgSearchPaths = {
8047
+ /** Fetched by main only (secrets) — kept here so the path has one origin. */
8048
+ config: (companyId) => `/api/companies/${encodeURIComponent(companyId)}/search-config`,
8049
+ reissue: (companyId) => `/api/companies/${encodeURIComponent(companyId)}/search-config/reissue`,
8050
+ integration: (companyId) => `/api/admin/companies/${encodeURIComponent(companyId)}/search-integration`,
8051
+ acl: (companyId) => `/api/companies/${encodeURIComponent(companyId)}/search-acl`,
8052
+ leases: (companyId) => `/api/companies/${encodeURIComponent(companyId)}/search-index/leases`,
8053
+ cursors: (companyId, projectId) => `/api/companies/${encodeURIComponent(companyId)}/search-index/cursors${projectId === void 0 ? "" : `?projectId=${encodeURIComponent(projectId)}`}`,
8054
+ progress: (companyId) => `/api/companies/${encodeURIComponent(companyId)}/search-index/progress`
8055
+ };
8056
+ function record$3(value2) {
8057
+ return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
8058
+ }
8059
+ function str(value2, fallback = "") {
8060
+ return typeof value2 === "string" ? value2 : fallback;
8061
+ }
8062
+ function num(value2, fallback = 0) {
8063
+ return typeof value2 === "number" && Number.isFinite(value2) ? value2 : fallback;
8064
+ }
8065
+ const PHASES = ["idle", "waiting", "indexing", "done"];
8066
+ function parseOrgSearchCursors(raw) {
8067
+ const list = Array.isArray(record$3(raw).cursors) ? record$3(raw).cursors : [];
8068
+ return list.flatMap((item) => {
8069
+ const c = record$3(item);
8070
+ if (typeof c.sessionId !== "string" || typeof c.lastSeq !== "number") return [];
8071
+ return [{
8072
+ sessionId: c.sessionId,
8073
+ lastSeq: c.lastSeq,
8074
+ complete: c.complete === true,
8075
+ failureKind: typeof c.failureKind === "string" ? c.failureKind : null,
8076
+ updatedAt: num(c.updatedAt)
8077
+ }];
8078
+ });
8079
+ }
8080
+ function parseProjectProgress(raw) {
8081
+ const p = record$3(raw);
8082
+ return {
8083
+ projectId: str(p.projectId),
8084
+ sessionsTotal: num(p.sessionsTotal),
8085
+ sessionsIndexed: num(p.sessionsIndexed),
8086
+ sessionsFailed: num(p.sessionsFailed),
8087
+ messagesIndexed: num(p.messagesIndexed),
8088
+ phase: PHASES.includes(p.phase) ? p.phase : "idle",
8089
+ leaseHolderMasked: typeof p.leaseHolderMasked === "string" ? p.leaseHolderMasked : null,
8090
+ updatedAt: typeof p.updatedAt === "number" ? p.updatedAt : null
8091
+ };
8092
+ }
8093
+ function parseOrgSearchProgress(raw) {
8094
+ const r = record$3(raw);
8095
+ return {
8096
+ phase: PHASES.includes(r.phase) ? r.phase : "idle",
8097
+ sessionsTotal: num(r.sessionsTotal),
8098
+ sessionsIndexed: num(r.sessionsIndexed),
8099
+ sessionsFailed: num(r.sessionsFailed),
8100
+ messagesIndexed: num(r.messagesIndexed),
8101
+ activeLeases: num(r.activeLeases),
8102
+ updatedAt: typeof r.updatedAt === "number" ? r.updatedAt : null,
8103
+ projects: (Array.isArray(r.projects) ? r.projects : []).map(parseProjectProgress)
8104
+ };
8105
+ }
8106
+ function isApiError(error) {
8107
+ return error instanceof Error && error.name === "ApiError" && typeof error.status === "number" && typeof error.body === "string";
8108
+ }
8109
+ function errorCode(error) {
8110
+ try {
8111
+ const parsed = JSON.parse(error.body);
8112
+ return typeof parsed.error === "string" ? parsed.error : null;
8113
+ } catch {
8114
+ return null;
8115
+ }
8116
+ }
8117
+ function classify404(error) {
8118
+ const code = errorCode(error);
8119
+ if (code === "search-integration-not-configured") return { kind: "not-configured" };
8120
+ if (code === "project-not-found") return { kind: "not-found" };
8121
+ return { kind: "unsupported" };
8122
+ }
8123
+ async function attempt(run, onError) {
8124
+ try {
8125
+ return await run();
8126
+ } catch (error) {
8127
+ if (isApiError(error)) {
8128
+ const mapped = onError(error);
8129
+ if (mapped !== null) return mapped;
8130
+ }
8131
+ throw error;
8132
+ }
8133
+ }
8134
+ function createOrgSearchApi(transport) {
8135
+ const json = (body, method) => ({ method, body: JSON.stringify(body) });
8136
+ return {
8137
+ /** Member whose key returned 401 asks the reconciler for a new one (state → reissue). */
8138
+ requestKeyReissue: (companyId) => attempt(
8139
+ async () => {
8140
+ await transport.request(orgSearchPaths.reissue(companyId), { method: "POST" });
8141
+ return { kind: "ok" };
8142
+ },
8143
+ (error) => error.status === 404 ? errorCode(error) === "member-key-not-found" ? { kind: "not-found" } : { kind: "unsupported" } : null
8144
+ ),
8145
+ getIntegration: (companyId) => attempt(
8146
+ async () => ({ kind: "ok", integration: await transport.request(orgSearchPaths.integration(companyId)) }),
8147
+ (error) => error.status === 404 ? classify404(error) : null
8148
+ ),
8149
+ deleteIntegration: async (companyId) => {
8150
+ await transport.request(orgSearchPaths.integration(companyId), { method: "DELETE" });
8151
+ },
8152
+ getAcl: (companyId) => transport.request(orgSearchPaths.acl(companyId)),
8153
+ putAcl: (companyId, input) => attempt(
8154
+ async () => {
8155
+ await transport.request(orgSearchPaths.acl(companyId), json(input, "PUT"));
8156
+ return { kind: "ok" };
8157
+ },
8158
+ (error) => error.status === 409 ? { kind: "lease-not-held" } : error.status === 404 ? { kind: "unsupported" } : null
8159
+ ),
8160
+ lease: (companyId, projectId, action) => attempt(
8161
+ async () => {
8162
+ const lease = await transport.request(orgSearchPaths.leases(companyId), json({ projectId, action }, "POST"));
8163
+ return action === "release" ? { kind: "released" } : { kind: "ok", lease };
8164
+ },
8165
+ (error) => error.status === 404 ? classify404(error) : error.status === 403 ? { kind: "forbidden" } : null
8166
+ ),
8167
+ getCursors: (companyId, projectId) => attempt(
8168
+ async () => ({ kind: "ok", cursors: parseOrgSearchCursors(await transport.request(orgSearchPaths.cursors(companyId, projectId))) }),
8169
+ (error) => error.status === 404 ? classify404(error) : null
8170
+ ),
8171
+ putCursors: (companyId, projectId, cursors) => attempt(
8172
+ async () => {
8173
+ await transport.request(orgSearchPaths.cursors(companyId), json({ projectId, cursors }, "PUT"));
8174
+ return { kind: "ok" };
8175
+ },
8176
+ (error) => error.status === 409 ? { kind: "lease-not-held" } : error.status === 404 ? classify404(error) : null
8177
+ ),
8178
+ getProgress: (companyId) => attempt(
8179
+ async () => ({ kind: "ok", progress: parseOrgSearchProgress(await transport.request(orgSearchPaths.progress(companyId))) }),
8180
+ (error) => error.status === 404 ? { kind: "unsupported" } : null
8181
+ )
8182
+ };
8183
+ }
7398
8184
  const SERVER_MACHINE_CONCURRENCY = 4;
7399
8185
  const MAX_FANOUT_TIMEOUT_MS = 9e5;
7400
8186
  function machineFanoutTimeoutMs(perMachineMs, machineCount) {
@@ -7764,7 +8550,7 @@ const PUBLICATION_KEYS = /* @__PURE__ */ new Set([
7764
8550
  function fail(detail) {
7765
8551
  throw new Error(`artifact publication response is invalid: ${detail}`);
7766
8552
  }
7767
- function record$1(value2, label) {
8553
+ function record$2(value2, label) {
7768
8554
  if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) fail(`${label} must be an object`);
7769
8555
  return value2;
7770
8556
  }
@@ -7810,7 +8596,7 @@ function sourceIdentity(source) {
7810
8596
  ]) : JSON.stringify([source.chatId, source.path]);
7811
8597
  }
7812
8598
  function validatePublication(value2, apiBaseUrl, operation, expectedSource) {
7813
- const publication = record$1(value2, "publication");
8599
+ const publication = record$2(value2, "publication");
7814
8600
  if (Object.keys(publication).some((key) => !PUBLICATION_KEYS.has(key))) {
7815
8601
  fail("publication contains an unexpected field");
7816
8602
  }
@@ -7843,7 +8629,7 @@ function validatePublication(value2, apiBaseUrl, operation, expectedSource) {
7843
8629
  };
7844
8630
  }
7845
8631
  function parseStaticArtifactPublicationResult(value2, options) {
7846
- const response = record$1(value2, "response");
8632
+ const response = record$2(value2, "response");
7847
8633
  if (options.operation === "publish") {
7848
8634
  exactKeys(response, ["publication", "publicUrl", "reused"]);
7849
8635
  if (typeof response.reused !== "boolean") fail("reused is invalid");
@@ -11400,14 +12186,14 @@ function requireReceiver() {
11400
12186
  * @return {(Error|RangeError)} The error
11401
12187
  * @private
11402
12188
  */
11403
- createError(ErrorCtor, message, prefix, statusCode, errorCode) {
12189
+ createError(ErrorCtor, message, prefix, statusCode, errorCode2) {
11404
12190
  this._loop = false;
11405
12191
  this._errored = true;
11406
12192
  const err = new ErrorCtor(
11407
12193
  prefix ? `Invalid WebSocket frame: ${message}` : message
11408
12194
  );
11409
12195
  Error.captureStackTrace(err, this.createError);
11410
- err.code = errorCode;
12196
+ err.code = errorCode2;
11411
12197
  err[kStatusCode] = statusCode;
11412
12198
  return err;
11413
12199
  }
@@ -16178,10 +16964,10 @@ class Manager extends Emitter {
16178
16964
  * @private
16179
16965
  */
16180
16966
  onreconnect() {
16181
- const attempt = this.backoff.attempts;
16967
+ const attempt2 = this.backoff.attempts;
16182
16968
  this._reconnecting = false;
16183
16969
  this.backoff.reset();
16184
- this.emitReserved("reconnect", attempt);
16970
+ this.emitReserved("reconnect", attempt2);
16185
16971
  }
16186
16972
  }
16187
16973
  const debug = debugModule("socket.io-client");
@@ -16580,6 +17366,16 @@ function buildWorktreeMutationBody(input, options = {}) {
16580
17366
  Object.entries(input).filter(([key, value2]) => (key !== "machinePlatform" || options.includeMachinePlatform) && (typeof value2 === "string" && (value2.length > 0 || CLEARABLE_STRING_KEYS.has(key)) || value2 === null && CLEARABLE_STRING_KEYS.has(key)))
16581
17367
  ));
16582
17368
  }
17369
+ function readDaemonRunning(decoded) {
17370
+ if (!decoded || typeof decoded !== "object") return null;
17371
+ const { status, state } = decoded;
17372
+ return knownDaemonState(status) ?? knownDaemonState(state);
17373
+ }
17374
+ function knownDaemonState(value2) {
17375
+ if (value2 === "running") return true;
17376
+ if (value2 === "stopped" || value2 === "shutting-down") return false;
17377
+ return null;
17378
+ }
16583
17379
  const STANDALONE_USER_ID = "standalone-local";
16584
17380
  function isStandaloneAuthSession(session) {
16585
17381
  return session?.userId === STANDALONE_USER_ID;
@@ -16663,19 +17459,19 @@ function row(value2) {
16663
17459
  if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) throw new Error("automation-response-invalid");
16664
17460
  return value2;
16665
17461
  }
16666
- function stringValue(value2) {
17462
+ function stringValue$1(value2) {
16667
17463
  if (typeof value2 !== "string" || !value2) throw new Error("automation-response-invalid");
16668
17464
  return value2;
16669
17465
  }
16670
17466
  function nullableString(value2) {
16671
- return value2 === null ? null : stringValue(value2);
17467
+ return value2 === null ? null : stringValue$1(value2);
16672
17468
  }
16673
- function integer(value2, min = 0) {
17469
+ function integer$1(value2, min = 0) {
16674
17470
  if (!Number.isSafeInteger(value2) || value2 < min) throw new Error("automation-response-invalid");
16675
17471
  return value2;
16676
17472
  }
16677
17473
  function nullableInteger(value2) {
16678
- return value2 === null ? null : integer(value2);
17474
+ return value2 === null ? null : integer$1(value2);
16679
17475
  }
16680
17476
  function optionalNullableInteger(value2) {
16681
17477
  return value2 === void 0 ? null : nullableInteger(value2);
@@ -16683,40 +17479,40 @@ function optionalNullableInteger(value2) {
16683
17479
  function parseRuntimeState(value2) {
16684
17480
  const item = row(value2);
16685
17481
  return {
16686
- automationId: stringValue(item.automationId),
16687
- generation: integer(item.generation, 1),
16688
- stateVersion: integer(item.stateVersion, 1),
17482
+ automationId: stringValue$1(item.automationId),
17483
+ generation: integer$1(item.generation, 1),
17484
+ stateVersion: integer$1(item.stateVersion, 1),
16689
17485
  stateCiphertext: versionedAutomationBase64(item.stateCiphertext, 96 * 1024),
16690
- viewerKeyVersion: integer(item.viewerKeyVersion, 1),
17486
+ viewerKeyVersion: integer$1(item.viewerKeyVersion, 1),
16691
17487
  viewerKeyEnvelope: versionedAutomationBase64(item.viewerKeyEnvelope, 105, 105),
16692
- machineKeyVersion: integer(item.machineKeyVersion, 1),
16693
- updatedAt: integer(item.updatedAt)
17488
+ machineKeyVersion: integer$1(item.machineKeyVersion, 1),
17489
+ updatedAt: integer$1(item.updatedAt)
16694
17490
  };
16695
17491
  }
16696
17492
  function parseServerAutomationPublic(value2) {
16697
17493
  const item = row(value2);
16698
17494
  if (item.payloadVersion !== 1 && item.payloadVersion !== 2 || typeof item.paused !== "boolean") throw new Error("automation-response-invalid");
16699
17495
  const parsed = {
16700
- id: stringValue(item.id),
16701
- projectId: stringValue(item.projectId),
16702
- ownerAccountId: stringValue(item.ownerAccountId),
17496
+ id: stringValue$1(item.id),
17497
+ projectId: stringValue$1(item.projectId),
17498
+ ownerAccountId: stringValue$1(item.ownerAccountId),
16703
17499
  machineAccountId: nullableString(item.machineAccountId),
16704
17500
  machineId: nullableString(item.machineId),
16705
- revision: integer(item.revision, 1),
16706
- generation: integer(item.generation, 1),
17501
+ revision: integer$1(item.revision, 1),
17502
+ generation: integer$1(item.generation, 1),
16707
17503
  payloadVersion: item.payloadVersion,
16708
- payloadCiphertext: stringValue(item.payloadCiphertext),
16709
- viewerKeyId: stringValue(item.viewerKeyId),
16710
- viewerKeyVersion: integer(item.viewerKeyVersion, 1),
16711
- viewerKeyEnvelope: stringValue(item.viewerKeyEnvelope),
16712
- machineKeyVersion: integer(item.machineKeyVersion, 1),
17504
+ payloadCiphertext: stringValue$1(item.payloadCiphertext),
17505
+ viewerKeyId: stringValue$1(item.viewerKeyId),
17506
+ viewerKeyVersion: integer$1(item.viewerKeyVersion, 1),
17507
+ viewerKeyEnvelope: stringValue$1(item.viewerKeyEnvelope),
17508
+ machineKeyVersion: integer$1(item.machineKeyVersion, 1),
16713
17509
  paused: item.paused,
16714
- enabledAt: integer(item.enabledAt),
17510
+ enabledAt: integer$1(item.enabledAt),
16715
17511
  runRequestedAt: optionalNullableInteger(item.runRequestedAt),
16716
- appliedRevision: integer(item.appliedRevision),
17512
+ appliedRevision: integer$1(item.appliedRevision),
16717
17513
  appliedAt: nullableInteger(item.appliedAt),
16718
- createdAt: integer(item.createdAt),
16719
- updatedAt: integer(item.updatedAt),
17514
+ createdAt: integer$1(item.createdAt),
17515
+ updatedAt: integer$1(item.updatedAt),
16720
17516
  runtimeState: item.runtimeState === void 0 || item.runtimeState === null ? null : parseRuntimeState(item.runtimeState)
16721
17517
  };
16722
17518
  if (parsed.payloadVersion === 2 !== (parsed.runtimeState !== null) || parsed.runtimeState && (parsed.runtimeState.automationId !== parsed.id || parsed.runtimeState.generation !== parsed.generation || parsed.runtimeState.viewerKeyVersion !== parsed.viewerKeyVersion || parsed.runtimeState.machineKeyVersion !== parsed.machineKeyVersion)) {
@@ -16738,13 +17534,14 @@ function parseServerAutomationAdoption(value2) {
16738
17534
  function parseServerAutomationTarget(value2) {
16739
17535
  const item = row(value2);
16740
17536
  return {
16741
- machineAccountId: stringValue(item.machineAccountId),
16742
- machineId: stringValue(item.machineId),
16743
- machinePublicKey: stringValue(item.machinePublicKey),
16744
- machineKeyVersion: integer(item.machineKeyVersion, 1),
17537
+ machineAccountId: stringValue$1(item.machineAccountId),
17538
+ machineId: stringValue$1(item.machineId),
17539
+ machinePublicKey: stringValue$1(item.machinePublicKey),
17540
+ machineKeyVersion: integer$1(item.machineKeyVersion, 1),
16745
17541
  viewerPublicKey: nullableString(item.viewerPublicKey),
16746
- viewerKeyVersion: integer(item.viewerKeyVersion),
16747
- automationProtocolVersion: item.automationProtocolVersion === void 0 ? 1 : integer(item.automationProtocolVersion, 1)
17542
+ viewerKeyVersion: integer$1(item.viewerKeyVersion),
17543
+ automationProtocolVersion: item.automationProtocolVersion === void 0 ? 1 : integer$1(item.automationProtocolVersion, 1),
17544
+ sessionFollowupSupported: item.sessionFollowupSupported === true
16748
17545
  };
16749
17546
  }
16750
17547
  const RUN_STATUSES = /* @__PURE__ */ new Set(["CLAIMED", "RUNNING", "COMPLETED", "FAILED", "CANCELLED", "EXPIRED", "ABANDONED"]);
@@ -16753,16 +17550,16 @@ function parseServerAutomationRun(value2) {
16753
17550
  const item = row(value2);
16754
17551
  if (!RUN_STATUSES.has(item.status) || item.outcome !== null && !RUN_OUTCOMES.has(item.outcome) || typeof item.lateReport !== "boolean") throw new Error("automation-response-invalid");
16755
17552
  return {
16756
- id: stringValue(item.id),
16757
- automationId: stringValue(item.automationId),
16758
- generation: integer(item.generation, 1),
16759
- scheduledFor: integer(item.scheduledFor),
16760
- machineId: stringValue(item.machineId),
17553
+ id: stringValue$1(item.id),
17554
+ automationId: stringValue$1(item.automationId),
17555
+ generation: integer$1(item.generation, 1),
17556
+ scheduledFor: integer$1(item.scheduledFor),
17557
+ machineId: stringValue$1(item.machineId),
16761
17558
  status: item.status,
16762
17559
  sessionId: nullableString(item.sessionId),
16763
17560
  outcome: item.outcome,
16764
17561
  detailCiphertext: nullableString(item.detailCiphertext),
16765
- claimedAt: integer(item.claimedAt),
17562
+ claimedAt: integer$1(item.claimedAt),
16766
17563
  startedAt: nullableInteger(item.startedAt),
16767
17564
  completedAt: nullableInteger(item.completedAt),
16768
17565
  lateReport: item.lateReport,
@@ -16800,6 +17597,98 @@ function parseServerGithubWebhookStatus(value2) {
16800
17597
  lastDeliveryAt: nullableTimestamp(item.lastDeliveryAt)
16801
17598
  };
16802
17599
  }
17600
+ const SESSION_FOLLOWUP_WIRE_VERSION = 1;
17601
+ const STATUSES$1 = /* @__PURE__ */ new Set(["WAITING", "DELIVERY_PENDING", "PAUSED", "COMPLETED", "CANCELLED", "FAILED"]);
17602
+ const TERMINAL_CODES = /* @__PURE__ */ new Set([
17603
+ "CLEAN",
17604
+ "LOW_OR_NIT_ONLY",
17605
+ "UNSTRUCTURED",
17606
+ "ROUNDS_EXHAUSTED",
17607
+ "USER_INTERVENTION",
17608
+ "STOPPED",
17609
+ "SESSION_UNAVAILABLE",
17610
+ "TARGET_MISMATCH",
17611
+ "DECRYPT_FAILED",
17612
+ "PERMISSION_REVOKED"
17613
+ ]);
17614
+ function invalid() {
17615
+ throw new Error("session-followup-response-invalid");
17616
+ }
17617
+ function record$1(value2) {
17618
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) invalid();
17619
+ return value2;
17620
+ }
17621
+ function stringValue(value2) {
17622
+ return typeof value2 === "string" && value2 ? value2 : invalid();
17623
+ }
17624
+ function boundedString(value2, max) {
17625
+ const parsed = stringValue(value2);
17626
+ return parsed.length <= max ? parsed : invalid();
17627
+ }
17628
+ function integer(value2, min = 0) {
17629
+ return Number.isSafeInteger(value2) && value2 >= min ? value2 : invalid();
17630
+ }
17631
+ function validateSessionFollowupCreateInput(input) {
17632
+ try {
17633
+ if (input.wireVersion !== SESSION_FOLLOWUP_WIRE_VERSION || input.payloadVersion !== 1) invalid();
17634
+ if (!input.sessionId || input.sessionId.length > 200 || !input.viewerKeyId || input.viewerKeyId.length > 128) invalid();
17635
+ const totalRounds = integer(input.totalRounds, 2);
17636
+ const currentRound = integer(input.currentRound, 1);
17637
+ if (totalRounds > 7 || currentRound > totalRounds) invalid();
17638
+ integer(input.responseBoundarySeq);
17639
+ integer(input.viewerKeyVersion, 1);
17640
+ integer(input.machineKeyVersion, 1);
17641
+ versionedAutomationBase64(input.payloadCiphertext, 128 * 1024);
17642
+ versionedAutomationBase64(input.viewerKeyEnvelope, 105, 105);
17643
+ versionedAutomationBase64(input.machineKeyEnvelope, 105, 105);
17644
+ return input;
17645
+ } catch {
17646
+ throw new Error("session-followup-request-invalid");
17647
+ }
17648
+ }
17649
+ function parseSessionFollowupPublic(value2) {
17650
+ try {
17651
+ const item = record$1(value2);
17652
+ if (!STATUSES$1.has(item.status)) invalid();
17653
+ if (item.terminalCode !== null && !TERMINAL_CODES.has(item.terminalCode)) invalid();
17654
+ if (item.payloadVersion !== 1) invalid();
17655
+ const totalRounds = integer(item.totalRounds, 2);
17656
+ const currentRound = integer(item.currentRound, 1);
17657
+ if (totalRounds > 7 || currentRound > totalRounds) invalid();
17658
+ return {
17659
+ id: stringValue(item.id),
17660
+ projectId: stringValue(item.projectId),
17661
+ ownerAccountId: stringValue(item.ownerAccountId),
17662
+ machineAccountId: stringValue(item.machineAccountId),
17663
+ machineId: stringValue(item.machineId),
17664
+ sessionId: stringValue(item.sessionId),
17665
+ revision: integer(item.revision, 1),
17666
+ generation: integer(item.generation, 1),
17667
+ step: integer(item.step, 1),
17668
+ status: item.status,
17669
+ terminalCode: item.terminalCode,
17670
+ totalRounds,
17671
+ currentRound,
17672
+ responseBoundarySeq: integer(item.responseBoundarySeq),
17673
+ lastObservedSeq: integer(item.lastObservedSeq),
17674
+ payloadVersion: 1,
17675
+ payloadCiphertext: versionedAutomationBase64(item.payloadCiphertext, 128 * 1024),
17676
+ viewerKeyId: boundedString(item.viewerKeyId, 128),
17677
+ viewerKeyVersion: integer(item.viewerKeyVersion, 1),
17678
+ viewerKeyEnvelope: versionedAutomationBase64(item.viewerKeyEnvelope, 105, 105),
17679
+ machineKeyVersion: integer(item.machineKeyVersion, 1),
17680
+ completedAt: item.completedAt === null ? null : integer(item.completedAt),
17681
+ createdAt: integer(item.createdAt),
17682
+ updatedAt: integer(item.updatedAt)
17683
+ };
17684
+ } catch {
17685
+ throw new Error("session-followup-response-invalid");
17686
+ }
17687
+ }
17688
+ function parseSessionFollowupList(value2) {
17689
+ if (!Array.isArray(value2)) throw new Error("session-followup-response-invalid");
17690
+ return value2.map(parseSessionFollowupPublic);
17691
+ }
16803
17692
  const ERROR_CODES = /* @__PURE__ */ new Set([
16804
17693
  "PAIRING_INVALID",
16805
17694
  "PAIRING_EXPIRED",
@@ -16919,6 +17808,20 @@ class MachineDeleteConflictError extends Error {
16919
17808
  const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
16920
17809
  const LONG_REQUEST_TIMEOUT_MS = 18e4;
16921
17810
  const UPLOAD_REQUEST_TIMEOUT_MS = 12e4;
17811
+ const OWNER_SYNC_WARNING_MAX_COUNT = 16;
17812
+ const OWNER_SYNC_WARNING_MAX_LENGTH = 100;
17813
+ function parseOwnerSyncWarnings(value2) {
17814
+ if (!Array.isArray(value2)) return [];
17815
+ const warnings2 = [];
17816
+ for (const item of value2) {
17817
+ if (typeof item !== "string") continue;
17818
+ const warning = item.trim();
17819
+ if (!warning || warning.length > OWNER_SYNC_WARNING_MAX_LENGTH) continue;
17820
+ warnings2.push(warning);
17821
+ if (warnings2.length === OWNER_SYNC_WARNING_MAX_COUNT) break;
17822
+ }
17823
+ return warnings2;
17824
+ }
16922
17825
  function isRequestTimeoutError(error) {
16923
17826
  return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
16924
17827
  }
@@ -16928,7 +17831,9 @@ async function fetchWithTimeout(url2, init, timeoutMs) {
16928
17831
  return await fetch(url2, requestAbort.signal ? { ...init, signal: requestAbort.signal } : init);
16929
17832
  } catch (error) {
16930
17833
  if (requestAbort.didTimeout() && isRequestTimeoutError(error)) {
16931
- throw new Error(translateCurrent("api.serverTimeout", { seconds: Math.round(timeoutMs / 1e3) }));
17834
+ const timeout = new Error(translateCurrent("api.serverTimeout", { seconds: Math.round(timeoutMs / 1e3) }));
17835
+ timeout.name = "ServerTimeoutError";
17836
+ throw timeout;
16932
17837
  }
16933
17838
  throw error;
16934
17839
  }
@@ -17017,7 +17922,8 @@ function normalizeGithubPullRequest(raw) {
17017
17922
  head: typeof head === "string" ? head : null,
17018
17923
  base: typeof base === "string" ? base : null,
17019
17924
  draft: raw.draft === true,
17020
- merged
17925
+ merged,
17926
+ ...raw.autoMerge === true ? { autoMerge: true } : {}
17021
17927
  };
17022
17928
  }
17023
17929
  function extractPullRequestList(raw) {
@@ -17053,6 +17959,7 @@ function normalizeMachine(row2, candidateKeys) {
17053
17959
  const host = row2.host || registry?.host || (typeof decrypted.host === "string" ? decrypted.host : row2.id.slice(0, 8));
17054
17960
  const platform2 = row2.platform || registry?.platform || (typeof decrypted.platform === "string" ? decrypted.platform : "unknown");
17055
17961
  const automationSupport = normalizeAutomationSupport(decrypted.automationSupport) ?? normalizeAutomationSupport(registry?.automationSupport);
17962
+ const autonomousQualityGate = decrypted.autonomousQualityGateSupport ?? registry?.autonomousQualityGateSupport;
17056
17963
  return {
17057
17964
  id: row2.id,
17058
17965
  accountId: row2.accountId,
@@ -17065,8 +17972,10 @@ function normalizeMachine(row2, candidateKeys) {
17065
17972
  happyCliVersion: row2.happyCliVersion || registry?.happyCliVersion || (typeof decrypted.happyCliVersion === "string" ? decrypted.happyCliVersion : void 0),
17066
17973
  homeDir: row2.homeDir || registry?.homeDir || (typeof decrypted.homeDir === "string" ? decrypted.homeDir : void 0),
17067
17974
  resources,
17975
+ daemonRunning: readDaemonRunning(daemonState),
17068
17976
  mcpCallerGrantPublicKey: readMcpCallerGrantPublicKey(daemonState),
17069
17977
  automationSupport,
17978
+ autonomousQualityGate,
17070
17979
  filesystemScope: decrypted.filesystemScope,
17071
17980
  additionalDirectories: decrypted.additionalDirectories,
17072
17981
  registry
@@ -17096,8 +18005,57 @@ class ApiClient {
17096
18005
  headers: { "Content-Type": "application/json" },
17097
18006
  body: JSON.stringify(input)
17098
18007
  }, DEFAULT_REQUEST_TIMEOUT_MS);
18008
+ return parsePasswordLoginResponse(await this.handleResponse(response));
18009
+ }
18010
+ /**
18011
+ * MFA(TOTP) 로그인 2단계 — 서버 mfaLoginRoutes `/api/auth/mfa/login/totp`.
18012
+ * 성공 응답은 일반 로그인 결과의 상위집합이라 같은 strict parser 를 쓴다.
18013
+ * 실패는 handleResponse 의 ApiError(status + body 의 MFA_* code)로 온다.
18014
+ */
18015
+ async verifyMfaTotpLogin(input) {
18016
+ const response = await fetchWithTimeout(absoluteUrl(this.config.apiBaseUrl, "/api/auth/mfa/login/totp"), {
18017
+ method: "POST",
18018
+ headers: { "Content-Type": "application/json" },
18019
+ body: JSON.stringify(input)
18020
+ }, DEFAULT_REQUEST_TIMEOUT_MS);
17099
18021
  return parseCloudLoginResult(await this.handleResponse(response));
17100
18022
  }
18023
+ async startMfaPasskeyHandoff(input) {
18024
+ const response = await fetchWithTimeout(
18025
+ absoluteUrl(this.config.apiBaseUrl, "/api/auth/mfa/login/passkey-handoffs"),
18026
+ {
18027
+ method: "POST",
18028
+ headers: { "Content-Type": "application/json" },
18029
+ body: JSON.stringify({
18030
+ version: 1,
18031
+ loginToken: input.loginToken,
18032
+ clientKind: input.clientKind,
18033
+ ...input.deviceId ? { deviceId: input.deviceId } : {},
18034
+ e2eeDeviceApprovalVersion: 1
18035
+ }),
18036
+ signal: input.signal
18037
+ },
18038
+ DEFAULT_REQUEST_TIMEOUT_MS
18039
+ );
18040
+ return parseMfaPasskeyHandoff(await this.handleResponse(response), this.config.apiBaseUrl);
18041
+ }
18042
+ async exchangeMfaPasskeyHandoff(input) {
18043
+ const response = await fetchWithTimeout(
18044
+ absoluteUrl(
18045
+ this.config.apiBaseUrl,
18046
+ `/api/auth/mfa/login/passkey-handoffs/${encodeURIComponent(input.handoffId)}/exchange`
18047
+ ),
18048
+ {
18049
+ method: "POST",
18050
+ headers: { "Content-Type": "application/json" },
18051
+ body: JSON.stringify({ version: 1, exchangeToken: input.exchangeToken }),
18052
+ signal: input.signal
18053
+ },
18054
+ DEFAULT_REQUEST_TIMEOUT_MS
18055
+ );
18056
+ const data = await this.handleResponse(response);
18057
+ return response.status === 202 ? parsePendingMfaPasskeyExchange(data) : { kind: "complete", result: parseCloudLoginResult(data) };
18058
+ }
17101
18059
  async getMe() {
17102
18060
  return this.request("/api/auth/me", { boundary: "api" });
17103
18061
  }
@@ -17394,6 +18352,17 @@ class ApiClient {
17394
18352
  );
17395
18353
  return Array.isArray(data) ? data : data.members ?? [];
17396
18354
  }
18355
+ /**
18356
+ * 회사 공유 키 공개키 + 자기 grant 조회 (specs/e2ee-company-key-activation
18357
+ * P1). 응답 해석(myGrants null=미수립 vs []=권한없음)은 companyKeyChain 이
18358
+ * 담당하므로 여기서는 원본을 그대로 넘긴다.
18359
+ */
18360
+ async fetchCompanyDataKey(companyId) {
18361
+ return this.request(
18362
+ `/api/admin/companies/${encodeURIComponent(companyId)}/data-key`,
18363
+ { boundary: "api" }
18364
+ );
18365
+ }
17397
18366
  async updateCompanyMemberRole(companyId, accountId, role) {
17398
18367
  await this.request(`/api/admin/companies/${encodeURIComponent(companyId)}/members/${encodeURIComponent(accountId)}`, {
17399
18368
  method: "PATCH",
@@ -17532,6 +18501,10 @@ class ApiClient {
17532
18501
  }
17533
18502
  );
17534
18503
  }
18504
+ /** specs/org-elasticsearch-search — 경계 어댑터는 src/api/orgSearch.ts 가 소유한다. */
18505
+ orgSearch() {
18506
+ return createOrgSearchApi({ request: (path, init) => this.request(path, { boundary: "api", ...init ?? {} }) });
18507
+ }
17535
18508
  async getCompanyHappyCredentials(companyId) {
17536
18509
  return this.request(`/api/companies/${encodeURIComponent(companyId)}/happy-credentials`, { boundary: "api" });
17537
18510
  }
@@ -17668,6 +18641,45 @@ class ApiClient {
17668
18641
  const data = await this.request(`/v1/projects/${encodeURIComponent(projectId)}/automation-target`, { boundary: "sync" });
17669
18642
  return parseServerAutomationTarget(data.target);
17670
18643
  }
18644
+ sessionFollowupPath(projectId, followupId) {
18645
+ const root = `/v1/projects/${encodeURIComponent(projectId)}/session-followups`;
18646
+ return followupId ? `${root}/${encodeURIComponent(followupId)}` : root;
18647
+ }
18648
+ async listSessionFollowups(projectId, sessionId) {
18649
+ const query = sessionId ? `?session_id=${encodeURIComponent(sessionId)}&limit=100` : "?limit=100";
18650
+ return parseSessionFollowupList(await this.request(`${this.sessionFollowupPath(projectId)}${query}`, { boundary: "sync" }));
18651
+ }
18652
+ async createSessionFollowup(projectId, input) {
18653
+ return parseSessionFollowupPublic(await this.request(this.sessionFollowupPath(projectId), {
18654
+ method: "POST",
18655
+ boundary: "sync",
18656
+ body: JSON.stringify(validateSessionFollowupCreateInput(input))
18657
+ }));
18658
+ }
18659
+ async mutateSessionFollowupRevision(input) {
18660
+ const item = this.sessionFollowupPath(input.projectId, input.followupId);
18661
+ try {
18662
+ return parseSessionFollowupPublic(await this.request(input.action ? `${item}/${input.action}` : item, {
18663
+ method: input.method,
18664
+ boundary: "sync",
18665
+ body: JSON.stringify({ wireVersion: SESSION_FOLLOWUP_WIRE_VERSION, expectedRevision: input.expectedRevision })
18666
+ }));
18667
+ } catch (error) {
18668
+ if (!(error instanceof ApiError) || error.status !== 409) throw error;
18669
+ try {
18670
+ const body = JSON.parse(error.body);
18671
+ return parseSessionFollowupPublic(body.latest);
18672
+ } catch {
18673
+ throw error;
18674
+ }
18675
+ }
18676
+ }
18677
+ async stopSessionFollowup(projectId, followupId, expectedRevision) {
18678
+ return this.mutateSessionFollowupRevision({ projectId, followupId, expectedRevision, method: "POST", action: "stop" });
18679
+ }
18680
+ async deleteSessionFollowup(projectId, followupId, expectedRevision) {
18681
+ return this.mutateSessionFollowupRevision({ projectId, followupId, expectedRevision, method: "DELETE" });
18682
+ }
17671
18683
  async setServerAutomationViewerKey(projectId, input) {
17672
18684
  return this.request(`/v1/projects/${encodeURIComponent(projectId)}/automation-viewer-key`, {
17673
18685
  method: "PUT",
@@ -18336,23 +19348,25 @@ class ApiClient {
18336
19348
  });
18337
19349
  }
18338
19350
  async fetchSessions(candidateKeys = [], tokenOverride, accountPrivateKey = null) {
19351
+ const boundaryGeneration2 = sessionDekRegistryGeneration();
18339
19352
  const data = await this.request("/v1/sessions", {
18340
19353
  boundary: "sync",
18341
19354
  tokenOverride
18342
19355
  });
18343
19356
  const rows = Array.isArray(data) ? data : data.sessions ?? [];
18344
- await preDecryptSessionHeaders(rows, accountPrivateKey);
19357
+ await preDecryptSessionHeaders(rows, accountPrivateKey, boundaryGeneration2);
18345
19358
  return rows.map((row2) => apiSessionToSession(row2, candidateKeys));
18346
19359
  }
18347
19360
  async fetchSessionsPage(candidateKeys = [], cursor, limit = 200, tokenOverride, accountPrivateKey = null) {
18348
19361
  const params = new URLSearchParams({ limit: String(limit) });
18349
19362
  if (cursor) params.set("cursor", cursor);
19363
+ const boundaryGeneration2 = sessionDekRegistryGeneration();
18350
19364
  const data = await this.request(
18351
19365
  `/v2/sessions?${params.toString()}`,
18352
19366
  { boundary: "sync", tokenOverride }
18353
19367
  );
18354
19368
  const rows = data.sessions ?? [];
18355
- await preDecryptSessionHeaders(rows, accountPrivateKey);
19369
+ await preDecryptSessionHeaders(rows, accountPrivateKey, boundaryGeneration2);
18356
19370
  return {
18357
19371
  sessions: rows.map((row2) => apiSessionToSession(row2, candidateKeys)),
18358
19372
  nextCursor: data.nextCursor ?? null,
@@ -18370,8 +19384,9 @@ class ApiClient {
18370
19384
  return data.sessions ?? [];
18371
19385
  }
18372
19386
  async lookupSessions(candidateKeys, ids, tokenOverride, accountPrivateKey = null) {
19387
+ const boundaryGeneration2 = sessionDekRegistryGeneration();
18373
19388
  const rows = await this.lookupApiSessions(ids, tokenOverride);
18374
- await preDecryptSessionHeaders(rows, accountPrivateKey);
19389
+ await preDecryptSessionHeaders(rows, accountPrivateKey, boundaryGeneration2);
18375
19390
  return rows.map((row2) => apiSessionToSession(row2, candidateKeys));
18376
19391
  }
18377
19392
  async deleteSession(sessionId, tokenOverride) {
@@ -18495,6 +19510,22 @@ class ApiClient {
18495
19510
  message: typeof raw.message === "string" ? raw.message : null
18496
19511
  };
18497
19512
  }
19513
+ /** specs/desktop-pr-auto-merge-toggle — aplus-dev-studio#3315 server endpoint. */
19514
+ async setGithubPullRequestAutoMerge(input, tokenOverride) {
19515
+ const raw = await this.request(
19516
+ `/api/projects/${encodeURIComponent(input.projectId)}/pull-requests/${encodeURIComponent(String(input.number))}/auto-merge`,
19517
+ {
19518
+ method: "POST",
19519
+ boundary: "api",
19520
+ tokenOverride,
19521
+ body: JSON.stringify({ enabled: input.enabled, ...input.method ? { method: input.method } : {} })
19522
+ }
19523
+ );
19524
+ return {
19525
+ autoMerge: raw.autoMerge === true,
19526
+ prNumber: typeof raw.prNumber === "number" ? raw.prNumber : input.number
19527
+ };
19528
+ }
18498
19529
  async fetchMessages(sessionId, afterSeq, tokenOverride, signal) {
18499
19530
  const params = new URLSearchParams({ limit: "100" });
18500
19531
  if (afterSeq !== void 0) params.set("after_seq", String(afterSeq));
@@ -18667,15 +19698,19 @@ class ApiClient {
18667
19698
  });
18668
19699
  }
18669
19700
  async syncOwnerWorkspace(projectId, input = {}) {
18670
- return this.request(`/api/projects/${encodeURIComponent(projectId)}/sync-owner-workspace`, {
18671
- method: "POST",
18672
- boundary: "api",
18673
- timeoutMs: LONG_REQUEST_TIMEOUT_MS,
18674
- body: JSON.stringify({
18675
- trigger: input.trigger ?? "owner-manual",
18676
- ...input.vcsTarget ? { vcsTarget: input.vcsTarget } : {}
18677
- })
18678
- });
19701
+ const data = await this.request(
19702
+ `/api/projects/${encodeURIComponent(projectId)}/sync-owner-workspace`,
19703
+ {
19704
+ method: "POST",
19705
+ boundary: "api",
19706
+ timeoutMs: LONG_REQUEST_TIMEOUT_MS,
19707
+ body: JSON.stringify({
19708
+ trigger: input.trigger ?? "owner-manual",
19709
+ ...input.vcsTarget ? { vcsTarget: input.vcsTarget } : {}
19710
+ })
19711
+ }
19712
+ );
19713
+ return { result: data.result, warnings: parseOwnerSyncWarnings(data.warnings) };
18679
19714
  }
18680
19715
  async getProjectVcsRemote(projectId) {
18681
19716
  const data = await this.request(
@@ -19251,8 +20286,8 @@ function happyHomeOrder(input) {
19251
20286
  if (!runtime) return [...homes];
19252
20287
  return [runtime, ...homes.filter((source) => source !== runtime)];
19253
20288
  }
19254
- function selectFirstValidCredential(candidates, readFile2) {
19255
- for (const candidate of candidates) {
20289
+ function selectFirstValidCredential(candidates2, readFile2) {
20290
+ for (const candidate of candidates2) {
19256
20291
  const raw = readFile2(candidate.path);
19257
20292
  if (raw === null) continue;
19258
20293
  const credential = parseAccessKeyFile(raw);
@@ -19408,7 +20443,7 @@ class RealtimeSocket {
19408
20443
  }
19409
20444
  const schedule = connectSchedule(totalTimeoutMs);
19410
20445
  let lastError = null;
19411
- for (let attempt = 0; attempt < CONNECT_ATTEMPTS; attempt += 1) {
20446
+ for (let attempt2 = 0; attempt2 < CONNECT_ATTEMPTS; attempt2 += 1) {
19412
20447
  const result = await this.connectOnce(schedule.attemptTimeoutMs);
19413
20448
  if (result.type === "cancelled") throw this.cancelledError();
19414
20449
  if (result.type === "connected") {
@@ -19418,7 +20453,7 @@ class RealtimeSocket {
19418
20453
  }
19419
20454
  if (result.type === "error") lastError = result.error;
19420
20455
  this.socket.disconnect();
19421
- const retryDelay = schedule.retryDelaysMs[attempt];
20456
+ const retryDelay = schedule.retryDelaysMs[attempt2];
19422
20457
  if (retryDelay !== void 0 && retryDelay > 0 && !await this.waitForRetry(retryDelay)) {
19423
20458
  throw this.cancelledError();
19424
20459
  }
@@ -19927,8 +20962,19 @@ async function runLogin(out, env = process.env) {
19927
20962
  const client = new ApiClient({ apiBaseUrl: serverUrl, syncBaseUrl: serverUrl });
19928
20963
  let result;
19929
20964
  try {
19930
- result = await client.login({ email, password });
19931
- } catch {
20965
+ const outcome = await client.login({ email, password });
20966
+ if (outcome.kind === "mfa-required") {
20967
+ if (outcome.challenge.enrollmentRequired || !outcome.challenge.methods.includes("totp")) {
20968
+ throw new CliError("This account requires MFA enrollment or a security key. Sign in on the web first, then retry.");
20969
+ }
20970
+ const code = (await askVisible("MFA code (6 digits): ")).trim();
20971
+ if (!/^\d{6}$/.test(code)) throw new CliError("The MFA code must be 6 digits.");
20972
+ result = await client.verifyMfaTotpLogin({ loginToken: outcome.challenge.loginToken, code });
20973
+ } else {
20974
+ result = outcome.result;
20975
+ }
20976
+ } catch (error) {
20977
+ if (error instanceof CliError) throw error;
19932
20978
  throw new CliError("Login failed. Check the email and password, then try again.");
19933
20979
  }
19934
20980
  if (!result?.token || !result?.secret) throw new CliError("The server did not return usable credentials.");
@@ -20553,12 +21599,12 @@ const codexUltraEffortOptions = [
20553
21599
  function getEffortOptionsForAgent(agent, model) {
20554
21600
  if (model === "default") return [];
20555
21601
  if (agent === "codex") {
20556
- if (model === "gpt-5.6-sol" || model === "gpt-5.6-terra") return codexUltraEffortOptions;
21602
+ if (model === "gpt-6-astra" || model === "gpt-5.6-sol" || model === "gpt-5.6-terra") return codexUltraEffortOptions;
20557
21603
  if (model === "gpt-5.6-luna") return codexMaxEffortOptions;
20558
21604
  return codexEffortOptions;
20559
21605
  }
20560
21606
  if (agent === "gemini" || agent === "opencode" || agent === "grok") return [];
20561
- if (model === "claude-fable-5") return claudeFableEffortOptions;
21607
+ if (model === "claude-fable-5-1") return claudeFableEffortOptions;
20562
21608
  return claudeEffortOptions;
20563
21609
  }
20564
21610
  function defaultEffortForAgent(agent, model) {
@@ -20566,7 +21612,7 @@ function defaultEffortForAgent(agent, model) {
20566
21612
  if (options.length === 0) return null;
20567
21613
  if (agent === "codex") {
20568
21614
  if (model === "gpt-5.6-sol") return "low";
20569
- if (model === "gpt-5.6-terra" || model === "gpt-5.6-luna") return "medium";
21615
+ if (model === "gpt-6-astra" || model === "gpt-5.6-terra" || model === "gpt-5.6-luna") return "medium";
20570
21616
  }
20571
21617
  return options.some((option) => option.key === "high") ? "high" : options[options.length - 1].key;
20572
21618
  }
@@ -20578,13 +21624,14 @@ const newSessionAgents = [
20578
21624
  ];
20579
21625
  const claudeModelOptions = [
20580
21626
  { key: "default", label: "Default" },
20581
- { key: "claude-fable-5", label: "Fable 5" },
21627
+ { key: "claude-fable-5-1", label: "Fable 5.1" },
20582
21628
  { key: "claude-opus-5", label: "Opus 5" },
20583
21629
  { key: "claude-sonnet-5", label: "Sonnet 5" },
20584
21630
  { key: "claude-haiku-4-5", label: "Haiku 4.5" }
20585
21631
  ];
20586
21632
  const codexModelOptions = [
20587
21633
  { key: "default", label: "Default" },
21634
+ { key: "gpt-6-astra", label: "GPT-6 Astra" },
20588
21635
  { key: "gpt-5.6-sol", label: "GPT-5.6 Sol" },
20589
21636
  { key: "gpt-5.6-terra", label: "GPT-5.6 Terra" },
20590
21637
  { key: "gpt-5.6-luna", label: "GPT-5.6 Luna" },
@@ -20617,10 +21664,18 @@ function getModelsForAgent(agent) {
20617
21664
  if (agent === "grok") return grokModelOptions;
20618
21665
  return claudeModelOptions;
20619
21666
  }
21667
+ const RETIRED_MODEL_KEYS = {
21668
+ "claude-fable-5": "claude-fable-5-1"
21669
+ };
21670
+ function isKnownModelForAgent(agent, model) {
21671
+ const current = RETIRED_MODEL_KEYS[model] ?? model;
21672
+ return getModelsForAgent(agent).some((option) => option.key === current);
21673
+ }
20620
21674
  function normalizeModelForAgent(agent, model) {
20621
21675
  if (agent === "opencode") return model || "default";
21676
+ const current = RETIRED_MODEL_KEYS[model] ?? model;
20622
21677
  const models = getModelsForAgent(agent);
20623
- return models.some((option) => option.key === model) ? model : models[0]?.key ?? "default";
21678
+ return models.some((option) => option.key === current) ? current : models[0]?.key ?? "default";
20624
21679
  }
20625
21680
  function normalizeModelSelectionOptions(agent, current, patch) {
20626
21681
  const model = normalizeModelForAgent(agent, patch.model ?? current.model);
@@ -20675,7 +21730,7 @@ function updateLiveChatSettings(current, patch) {
20675
21730
  }
20676
21731
  let selection = { model: current.model, effort: current.effort };
20677
21732
  if (patch.model !== void 0) {
20678
- if (!liveChatModelChoices(current).some((option) => option.key === patch.model)) return current;
21733
+ if (!isKnownModelForAgent(current.agent, patch.model)) return current;
20679
21734
  selection = normalizeModelSelectionOptions(current.agent, selection, { model: patch.model });
20680
21735
  }
20681
21736
  if (patch.effort !== void 0) {
@@ -21573,14 +22628,12 @@ function hasNewConversationSettings(request) {
21573
22628
  return request.model !== void 0 || request.effort !== void 0 || request.useWorktree === true || request.worktreeName !== void 0 || request.worktreeBaseRef !== void 0 || request.files !== void 0 || request.voice === true || request.voiceFile !== void 0;
21574
22629
  }
21575
22630
  function validateExplicitSelection(request, agent) {
21576
- if (request.model !== void 0 && !modelChoices(agent).some((option) => option.key === request.model)) {
22631
+ if (request.model !== void 0 && !isKnownModelForAgent(agent, request.model)) {
21577
22632
  throw new CliError(`Unknown ${agent} model "${request.model}".`);
21578
22633
  }
21579
- if (request.effort !== void 0) {
21580
- const model = request.model ?? DEFAULT_CLI_NEW_CONVERSATION_OPTIONS.model;
21581
- if (!effortChoices(agent, model).some((option) => option.key === request.effort)) {
21582
- throw new CliError(`Effort "${request.effort}" is not available for ${agent} model "${model}".`);
21583
- }
22634
+ const model = request.model === void 0 ? DEFAULT_CLI_NEW_CONVERSATION_OPTIONS.model : normalizeModelForAgent(agent, request.model);
22635
+ if (request.effort !== void 0 && !effortChoices(agent, model).some((option) => option.key === request.effort)) {
22636
+ throw new CliError(`Effort "${request.effort}" is not available for ${agent} model "${model}".`);
21584
22637
  }
21585
22638
  }
21586
22639
  async function resolveCliNewConversationOptions(request, ask) {
@@ -22527,7 +23580,8 @@ function normalizeSessionRows(rows, candidateKeys) {
22527
23580
  directory: readString(metadata, "path") ?? "",
22528
23581
  summary: readSummary(metadata),
22529
23582
  active: row2.active === true,
22530
- updatedAt: row2.updatedAt ?? 0
23583
+ updatedAt: row2.updatedAt ?? 0,
23584
+ agentState: decryptJsonWithCandidates(row2.agentState, candidateKeys)
22531
23585
  });
22532
23586
  }
22533
23587
  return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
@@ -22556,9 +23610,9 @@ async function resolveMachineId(context, query) {
22556
23610
  const match = matchMachine(machines, query);
22557
23611
  if (match.type === "match") return match.machine.id;
22558
23612
  if (match.type === "not-found") throw new CliError(`No machine matches "${query}". Run \`saycode machines\` to list them.`);
22559
- const candidates = match.candidates.map((machine) => ` ${machine.host} (${machine.id.slice(0, 8)})`).join("\n");
23613
+ const candidates2 = match.candidates.map((machine) => ` ${machine.host} (${machine.id.slice(0, 8)})`).join("\n");
22560
23614
  throw new CliError(`"${query}" matches more than one machine:
22561
- ${candidates}
23615
+ ${candidates2}
22562
23616
 
22563
23617
  Use a longer prefix or the machine id.`);
22564
23618
  }
@@ -23895,6 +24949,7 @@ function readAgentEnv(env) {
23895
24949
  if (!root) return null;
23896
24950
  return {
23897
24951
  root,
24952
+ scope: normalizeDirectory(env.SAYCODE_AGENT_SCOPE ?? "") || root,
23898
24953
  depth: readNonNegativeInt(env.SAYCODE_AGENT_DEPTH, 0),
23899
24954
  maxSpawn: Math.min(
23900
24955
  readNonNegativeInt(env.SAYCODE_AGENT_MAX_SPAWN, DEFAULT_AGENT_MAX_SPAWN),
@@ -23913,8 +24968,9 @@ function childAgentEnv(parent, childAgentId) {
23913
24968
  return {
23914
24969
  [AGENT_ENV_FLAG]: "1",
23915
24970
  SAYCODE_AGENT_ROOT: parent.root,
24971
+ SAYCODE_AGENT_SCOPE: parent.scope,
23916
24972
  SAYCODE_AGENT_DEPTH: String(parent.depth + 1),
23917
- SAYCODE_AGENT_MAX_SPAWN: String(parent.maxSpawn),
24973
+ SAYCODE_AGENT_MAX_SPAWN: String(Math.min(parent.maxSpawn, DEFAULT_AGENT_MAX_SPAWN)),
23918
24974
  SAYCODE_AGENT_ID: childAgentId
23919
24975
  };
23920
24976
  }
@@ -23927,6 +24983,9 @@ function isInScope(session, scope) {
23927
24983
  if (directory === root) return true;
23928
24984
  return directory.startsWith(root === "/" ? "/" : `${root}/`);
23929
24985
  }
24986
+ function sessionsInScope(sessions, scope) {
24987
+ return sessions.filter((session) => isInScope(session, scope));
24988
+ }
23930
24989
  function machineSettingsPaths(input) {
23931
24990
  const override = input.env.SAYCODE_HAPPY_HOME?.trim();
23932
24991
  const runtimeHome = input.env.HAPPY_HOME_DIR?.trim();
@@ -24137,7 +25196,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
24137
25196
  runtime.recordSpawn({
24138
25197
  ts: runtime.now(),
24139
25198
  agentKey: key,
24140
- root: env.root,
25199
+ root: env.scope,
24141
25200
  depth: env.depth,
24142
25201
  childSessionId: sessionId,
24143
25202
  ...env.sessionId ? { parentSessionId: env.sessionId } : {},
@@ -24241,6 +25300,7 @@ function canControlSpawnedDescendant(records, parentSessionId, childSessionId) {
24241
25300
  return resolveSubagentControlPermission(lineage, parentSessionId, childSessionId);
24242
25301
  }
24243
25302
  function ownershipFailure(runtime, env, target) {
25303
+ if (env.depth === 0) return null;
24244
25304
  if (!env.sessionId) return agentFail("session_unknown", "This session did not receive APLUS_SESSION_ID.");
24245
25305
  if (!runtime.readLineage) return agentFail("not_owned", "spawn lineage is unavailable");
24246
25306
  try {
@@ -24281,6 +25341,143 @@ async function stopOwnedTarget(runtime, env, target) {
24281
25341
  return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
24282
25342
  }
24283
25343
  }
25344
+ const LAST_AGENT_TEXT_PREVIEW_LENGTH = 240;
25345
+ function deriveAgentSessionState(snapshot, active) {
25346
+ if (!active) return "ended";
25347
+ if (awaitingUserInput(snapshot)) return "waiting-input";
25348
+ if (snapshot.thinking) return "responding";
25349
+ return "idle";
25350
+ }
25351
+ function turnOpenInWindow(contents) {
25352
+ let lastEnd = -1;
25353
+ let lastActivity = -1;
25354
+ contents.forEach((content, index) => {
25355
+ if (turnBoundary(content) === "end") lastEnd = index;
25356
+ else if (isTurnActivity(content)) lastActivity = index;
25357
+ });
25358
+ return lastActivity > lastEnd;
25359
+ }
25360
+ const TURN_ACTIVITY_EVENTS = /* @__PURE__ */ new Set(["turn-start", "text", "tool-call-start", "tool-call-progress", "tool-call-end"]);
25361
+ function isTurnActivity(content) {
25362
+ if (content.role === "user") return true;
25363
+ if (content.role !== "session") return false;
25364
+ return TURN_ACTIVITY_EVENTS.has(content.content.ev.t);
25365
+ }
25366
+ function lastAgentTextPreview(message, max = LAST_AGENT_TEXT_PREVIEW_LENGTH) {
25367
+ if (!message) return null;
25368
+ const text2 = message.content;
25369
+ return text2.length <= max ? text2 : `${text2.slice(0, max)}…`;
25370
+ }
25371
+ function pendingOf(snapshot) {
25372
+ return {
25373
+ askUserQuestion: Boolean(snapshot.pendingAskUserQuestion),
25374
+ exitPlanMode: Boolean(snapshot.pendingExitPlanMode),
25375
+ permissionRequests: snapshot.pendingPermissionRequestCount ?? 0
25376
+ };
25377
+ }
25378
+ const STATUS_CONCURRENCY = 4;
25379
+ async function sessionStatus(runtime, session) {
25380
+ let sequenced = [];
25381
+ let error = null;
25382
+ try {
25383
+ sequenced = await runtime.readSequencedMessages(session.id, STATE_WINDOW_MESSAGES);
25384
+ } catch (cause) {
25385
+ error = cause instanceof Error ? cause.message : String(cause);
25386
+ }
25387
+ let snapshot = createWaitSnapshot();
25388
+ for (const { content } of sequenced) snapshot = foldMessageIntoSnapshot(snapshot, content);
25389
+ snapshot = foldAgentStateIntoSnapshot(snapshot, session.agentState ?? null);
25390
+ if (turnOpenInWindow(sequenced.map((entry) => entry.content))) snapshot = { ...snapshot, thinking: true };
25391
+ return {
25392
+ ...sessionSummary(session),
25393
+ state: deriveAgentSessionState(snapshot, session.active),
25394
+ turnEndedAt: snapshot.lastResponseCompletedAt ?? 0,
25395
+ completion: completionSignalOf(snapshot),
25396
+ pending: pendingOf(snapshot),
25397
+ lastAgentText: lastAgentTextPreview(snapshot.finalAgentMessage),
25398
+ lastSeq: sequenced.reduce((max, entry) => Math.max(max, entry.seq), 0),
25399
+ ...error !== null ? { error } : {}
25400
+ };
25401
+ }
25402
+ async function listSessionStatus(runtime, scope) {
25403
+ const sessions = sessionsInScope(await runtime.listSessions(), scope);
25404
+ const rows = [];
25405
+ for (let index = 0; index < sessions.length; index += STATUS_CONCURRENCY) {
25406
+ const batch = sessions.slice(index, index + STATUS_CONCURRENCY);
25407
+ rows.push(...await Promise.all(batch.map((session) => sessionStatus(runtime, session))));
25408
+ }
25409
+ return agentOk({ sessions: rows });
25410
+ }
25411
+ async function resolveAll(runtime, scope, sessionIds) {
25412
+ const sessions = await runtime.listSessions();
25413
+ const targets = [];
25414
+ for (const sessionId of sessionIds) {
25415
+ const session = sessions.find((candidate) => candidate.id === sessionId);
25416
+ if (!session) return agentFail("session_not_found", sessionId);
25417
+ if (!isInScope(session, scope)) return agentFail("out_of_scope", sessionId);
25418
+ targets.push({ session, scope });
25419
+ }
25420
+ return targets;
25421
+ }
25422
+ async function waitForAnyTarget(runtime, scope, sessionIds, until, timeoutMs) {
25423
+ const resolved = await resolveAll(runtime, scope, sessionIds);
25424
+ if (!Array.isArray(resolved)) return resolved;
25425
+ const since = runtime.now();
25426
+ const predicate = untilPredicate(until, since);
25427
+ const streams = [];
25428
+ for (const target of resolved) {
25429
+ try {
25430
+ streams.push(await runtime.openStream(target.session.id));
25431
+ } catch (error) {
25432
+ for (const stream2 of streams) stream2.close();
25433
+ throw error;
25434
+ }
25435
+ }
25436
+ const controllers = resolved.map(() => new AbortController());
25437
+ const pins = resolved.map((target, index) => startIdentityPin(runtime, target, controllers[index]));
25438
+ const outcomes = new Map(resolved.map((target) => [target.session.id, "pending"]));
25439
+ try {
25440
+ await new Promise((resolve) => {
25441
+ let settled = 0;
25442
+ resolved.forEach((target, index) => {
25443
+ void waitForSessionState({
25444
+ current: streams[index].current,
25445
+ subscribe: streams[index].subscribe,
25446
+ predicate,
25447
+ timeoutMs,
25448
+ signal: controllers[index].signal
25449
+ }).then((result) => {
25450
+ const id = target.session.id;
25451
+ if (result.outcome === "satisfied") outcomes.set(id, "satisfied");
25452
+ else if (result.outcome === "aborted") {
25453
+ if (pins[index].lost()) outcomes.set(id, "lost");
25454
+ } else outcomes.set(id, "timeout");
25455
+ settled += 1;
25456
+ if (result.outcome === "satisfied" || settled === resolved.length) resolve();
25457
+ });
25458
+ });
25459
+ });
25460
+ } finally {
25461
+ for (const controller of controllers) controller.abort();
25462
+ for (const pin of pins) pin.stop();
25463
+ for (const stream2 of streams) stream2.close();
25464
+ }
25465
+ const satisfied = resolved.filter((target, index) => outcomes.get(target.session.id) !== "lost" && predicate(streams[index].current())).map((target) => target.session.id);
25466
+ const lost = resolved.map((target) => target.session.id).filter((id) => outcomes.get(id) === "lost");
25467
+ const pending = resolved.map((target) => target.session.id).filter((id) => !satisfied.includes(id) && !lost.includes(id));
25468
+ if (satisfied.length > 0) {
25469
+ return agentOk({
25470
+ outcome: "satisfied",
25471
+ until,
25472
+ satisfied,
25473
+ pending,
25474
+ lost,
25475
+ sessions: resolved.filter((target) => satisfied.includes(target.session.id)).map((target) => sessionSummary(target.session))
25476
+ });
25477
+ }
25478
+ if (lost.length === resolved.length) return agentFail("session_not_running", lost.join(","));
25479
+ return agentFail("wait_timeout", `${until} not reached within ${timeoutMs}ms${lost.length ? ` (lost: ${lost.join(",")})` : ""}`);
25480
+ }
24284
25481
  const CLI_CONFIGURABLE_AGENTS = [.../* @__PURE__ */ new Set([
24285
25482
  ...newSessionAgents.map(({ id }) => id),
24286
25483
  "gemini"
@@ -24295,7 +25492,6 @@ function cliSpawnModelOptions() {
24295
25492
  }));
24296
25493
  }
24297
25494
  const DEFAULT_READ_LIMIT = 20;
24298
- const STATE_WINDOW_MESSAGES = 50;
24299
25495
  function scopeOf(runtime) {
24300
25496
  const env = readAgentEnv(runtime.env);
24301
25497
  if (!env) return agentFail("not_agent_env", "This command only runs inside a Saycode-spawned session.");
@@ -24306,7 +25502,7 @@ function scopeOf(runtime) {
24306
25502
  if (!machineId) {
24307
25503
  return agentFail("out_of_scope", "Could not determine this machine from any happy home settings.");
24308
25504
  }
24309
- return { scope: { root: env.root, machineId }, env };
25505
+ return { scope: { root: env.scope, machineId }, env };
24310
25506
  }
24311
25507
  function isFailure(value2) {
24312
25508
  return typeof value2 === "object" && value2 !== null && "stdout" in value2;
@@ -24318,13 +25514,13 @@ async function resolveTarget(runtime, scope, sessionId) {
24318
25514
  if (!isInScope(session, scope)) return agentFail("out_of_scope", sessionId);
24319
25515
  return { session, scope };
24320
25516
  }
24321
- async function readSession(runtime, target, last) {
24322
- const messages = await runtime.readMessages(target.session.id, Math.max(last, STATE_WINDOW_MESSAGES));
25517
+ async function readSession(runtime, target, last, since = 0) {
25518
+ const sequenced = await runtime.readSequencedMessages(target.session.id, Math.max(last, STATE_WINDOW_MESSAGES));
24323
25519
  let snapshot = createWaitSnapshot();
24324
25520
  const text2 = [];
24325
- for (const message of messages) {
25521
+ for (const { seq, content: message } of sequenced) {
24326
25522
  snapshot = foldMessageIntoSnapshot(snapshot, message);
24327
- if (message.role !== "session") continue;
25523
+ if (seq <= since || message.role !== "session") continue;
24328
25524
  const envelope = message.content;
24329
25525
  if (envelope.ev.t !== "text") continue;
24330
25526
  text2.push({
@@ -24340,6 +25536,8 @@ async function readSession(runtime, target, last) {
24340
25536
  session: sessionSummary(target.session),
24341
25537
  turnEndedAt: snapshot.lastResponseCompletedAt ?? 0,
24342
25538
  completion: completionSignalOf(snapshot),
25539
+ // 0 when the window is empty: "start from the beginning", never a stale cursor.
25540
+ lastSeq: sequenced.reduce((max, entry) => Math.max(max, entry.seq), 0),
24343
25541
  // `slice(-0)` is `slice(0)` — the whole array. `--last 0` is a reasonable
24344
25542
  // way to ask for state without the transcript, and it must not answer with
24345
25543
  // every message in the window.
@@ -24353,11 +25551,11 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = [], model
24353
25551
  }
24354
25552
  const context = resolveSpawnContextFiles({
24355
25553
  paths: contextFiles,
24356
- root: env.root,
25554
+ root: env.scope,
24357
25555
  isReadableFile: runtime.isReadableFile
24358
25556
  });
24359
25557
  if (!context.ok) {
24360
- return context.reason === "out_of_scope" ? agentFail("out_of_scope", `${context.path} is outside ${env.root}`) : agentFail("file_unreadable", `${context.path} could not be read`);
25558
+ return context.reason === "out_of_scope" ? agentFail("out_of_scope", `${context.path} is outside ${env.scope}`) : agentFail("file_unreadable", `${context.path} could not be read`);
24361
25559
  }
24362
25560
  const key = spawnBudgetKey(env);
24363
25561
  let spawned;
@@ -24386,7 +25584,7 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = [], model
24386
25584
  runtime.recordSpawn({
24387
25585
  ts: runtime.now(),
24388
25586
  agentKey: key,
24389
- root: env.root,
25587
+ root: env.scope,
24390
25588
  depth: env.depth,
24391
25589
  childSessionId: sessionId,
24392
25590
  ...env.sessionId ? { parentSessionId: env.sessionId } : {},
@@ -24416,7 +25614,8 @@ async function runAgentCommand(runtime, args) {
24416
25614
  spawned = null;
24417
25615
  }
24418
25616
  return agentOk({
24419
- root: scope.root,
25617
+ root: env.root,
25618
+ scope: env.scope,
24420
25619
  machineId: scope.machineId,
24421
25620
  // Which session this IS. Null when happy-cli did not supply it, and a
24422
25621
  // handoff must then be told its source explicitly rather than guess.
@@ -24430,8 +25629,8 @@ async function runAgentCommand(runtime, args) {
24430
25629
  });
24431
25630
  }
24432
25631
  if (args.verb === "ls") {
24433
- const sessions = await runtime.listSessions();
24434
- return agentOk({ sessions: sessions.filter((session) => isInScope(session, scope)).map(sessionSummary) });
25632
+ if (args.status) return listSessionStatus(runtime, scope);
25633
+ return agentOk({ sessions: sessionsInScope(await runtime.listSessions(), scope).map(sessionSummary) });
24435
25634
  }
24436
25635
  if (args.verb === "spawn") {
24437
25636
  const agent = args.agent ?? DEFAULT_SPAWN_AGENT;
@@ -24442,12 +25641,15 @@ async function runAgentCommand(runtime, args) {
24442
25641
  }
24443
25642
  return spawnSibling(runtime, env, args.text ?? "", agent, args.files ?? [], modelOptions);
24444
25643
  }
25644
+ if (args.verb === "wait" && args.sessionIds) {
25645
+ return waitForAnyTarget(runtime, scope, args.sessionIds, args.until ?? "turn-end", clampWaitTimeout(args.timeoutMs));
25646
+ }
24445
25647
  if (!args.sessionId) return agentFail("session_not_found", "no session id given");
24446
25648
  const target = await resolveTarget(runtime, scope, args.sessionId);
24447
25649
  if (isFailure(target)) return target;
24448
25650
  switch (args.verb) {
24449
25651
  case "read":
24450
- return readSession(runtime, target, args.last ?? DEFAULT_READ_LIMIT);
25652
+ return readSession(runtime, target, args.last ?? DEFAULT_READ_LIMIT, args.since);
24451
25653
  case "wait":
24452
25654
  return waitForTarget(runtime, target, args.until ?? "turn-end", clampWaitTimeout(args.timeoutMs));
24453
25655
  case "prompt":
@@ -24688,7 +25890,7 @@ async function recentContents(context, transport, sessionId, limit) {
24688
25890
  const ciphertext = unwrapEncryptedField(message.content);
24689
25891
  if (!ciphertext) continue;
24690
25892
  const content = decodeContent(transport, ciphertext);
24691
- if (content) contents.push(content);
25893
+ if (content) contents.push({ seq: message.seq, content });
24692
25894
  }
24693
25895
  return contents;
24694
25896
  }
@@ -24721,7 +25923,7 @@ async function openStream(context, transport, sessionId) {
24721
25923
  else apply(update);
24722
25924
  });
24723
25925
  try {
24724
- for (const content of await recentContents(context, transport, sessionId, STREAM_HISTORY_LIMIT)) {
25926
+ for (const { content } of await recentContents(context, transport, sessionId, STREAM_HISTORY_LIMIT)) {
24725
25927
  snapshot = foldMessageIntoSnapshot(snapshot, content);
24726
25928
  }
24727
25929
  } catch (error) {
@@ -24745,7 +25947,7 @@ async function openStream(context, transport, sessionId) {
24745
25947
  }
24746
25948
  function createAgentRuntime(context, transport, env = process.env) {
24747
25949
  const keys = [decodeBase64(context.syncCredential.secret)];
24748
- const homeDir = context.homeDir, lineageRoot = readAgentEnv(env)?.root ?? context.homeDir;
25950
+ const homeDir = context.homeDir, lineageRoot = readAgentEnv(env)?.scope ?? context.homeDir;
24749
25951
  const credentialHome = context.credential.source === "saycode" ? null : dirname(context.credential.path);
24750
25952
  return {
24751
25953
  env,
@@ -24755,7 +25957,8 @@ function createAgentRuntime(context, transport, env = process.env) {
24755
25957
  readFile: readFileOrNull,
24756
25958
  isReadableFile: isReadableFileSync,
24757
25959
  listSessions: async () => normalizeSessionRows(await context.rest.listSessions(), keys),
24758
- readMessages: (sessionId, limit) => recentContents(context, transport, sessionId, limit),
25960
+ readMessages: async (sessionId, limit) => (await recentContents(context, transport, sessionId, limit)).map((entry) => entry.content),
25961
+ readSequencedMessages: (sessionId, limit) => recentContents(context, transport, sessionId, limit),
24759
25962
  openStream: (sessionId) => openStream(context, transport, sessionId),
24760
25963
  sendPrompt: async (sessionId, text2) => createCliSessionConnection({
24761
25964
  rest: context.rest,