Compare commits

...

334 Commits

Author SHA1 Message Date
Variet Worker
32cf69469c docs: session end — known-issues 2건 추가 + observer-dev-guide 3.3 업데이트 + devlog #019
[2026-04-19] Accept all span 렌더링 감지 실패 (v0.5.101)
[2026-04-19] auto-approve _from_ws 마커 누락 (v0.5.103)
observer-dev-guide 3.3: Accept all Observer 접근 가능으로 변경
Vikunja: #638 done, #639 done
2026-04-20 04:58:13 +09:00
Variet Worker
7c8891b99c fix(bridge): v38 auto-approve response에 _from_ws 마커 추가 — Observer polling 실패 수정 (v0.5.103)
근본 원인: auto-approve response 파일에 _from_ws 마커 없음.
processResponseFile(response watcher)이 Observer보다 먼저 파일을 읽고 삭제.
Observer의 GET /response/{rid} polling이 항상 {waiting:true} 반환.
known-issues [2026-04-18] WS response 파일 삭제 버그와 동일 패턴.
2026-04-20 04:43:56 +09:00
Variet Worker
3cc3442fda fix(bridge): v37 openReviewChanges 선호출 — agentAcceptAllInFile 실효성 보장 (v0.5.102)
agentAcceptAllInFile은 diff review 패널이 포커스되어야 동작.
v34에서 직접 호출만 했더니 SUCCESS 반환하지만 실제 효과 없음.
openReviewChanges → 500ms 대기 → agentAcceptAllInFile 순서로 수정.
2026-04-20 04:34:44 +09:00
Variet Worker
e95e7791f9 fix(observer): v36 Accept all 감지 — span.cursor-pointer 선택자 추가 (v0.5.101)
근본 원인: AG Native에서 Accept all 버튼이 <button>이 아닌 <span> 태그.
Observer의 allBtns 선택자가 button만 스캔하여 Accept all 미감지.
ACCEPT-SCAN 로그에서 tag=SPAN cls=cursor-pointer 확인.
span.cursor-pointer 추가로 diff review 버튼 감지 복구.
2026-04-20 04:26:58 +09:00
Variet Worker
2bf1eb41d1 feat(probe): v35 code_edit 자동 Accept — agentAcceptAllInFile 직접 호출 (v0.5.100)
Accept all 버튼이 Observer DOM에 없음 확인 (에디터 레이어).
step_probe에서 WAITING code_edit 감지 시 500ms 후 agentAcceptAllInFile 직접 실행.
Observer relay 필터에 ACCEPT 추가.
2026-04-20 04:05:47 +09:00
Variet Worker
cf1352eefa fix(bridge): v34 Accept all 이중 보장 — agentAcceptAllInFile 직접 호출 + ACCEPT 로그 relay (v0.5.99)
v34: Accept all/diff_review 감지 시 Observer DOM 클릭 + extension host agentAcceptAllInFile 이중 실행.
Observer relay 필터에 ACCEPT 키워드 추가로 ACCEPT-SCAN 진단 로그 활성화.
2026-04-20 00:11:07 +09:00
Variet Worker
6aea48e2e9 feat(bridge): v33 Accept all 자동승인 — diff review 버튼도 auto-approve (v0.5.98)
Always run과 동일하게 Accept all / Accept 버튼도 즉시 자동 승인.
Observer가 step_type=diff_review로 전송 → bridge에서 AUTO_APPROVE_RE 매칭 → 즉시 응답.
2026-04-19 21:27:18 +09:00
Variet Worker
bd5a7ca8b9 fix(observer): v32 터미널 프롬프트 조기감지 — JUNK/PROMPT 필터 전에 명령어 추출 (v0.5.97)
근본원인: code 요소의 textContent '❯ project > command'가 PowerShell 키워드
(return, function, const)를 포함할 수 있어 JUNK_CODE_RE에 잘못 걸림.
v32: ❯ 마커로 시작하는 code 텍스트는 JUNK/PROMPT 전에 명령어 직접 추출.
14/14 E2E 테스트 통과.
2026-04-19 19:55:44 +09:00
Variet Worker
8ada5f7daf fix(observer): v31 — content_copy 아이콘 필터 + 후보 길이순 정렬 + trailing icon strip (v0.5.96) 2026-04-19 15:18:11 +09:00
Variet Worker
4f2be831a1 diag(observer): v30 SCAN 진단 로그 — div textContent 덤프 (v0.5.95) 2026-04-19 15:08:56 +09:00
Variet Worker
cbfd137dcb fix(observer): v30 Running command 추출 — includes 매칭 + raw fallback + 디버그 로그 (v0.5.94) 2026-04-19 15:03:09 +09:00
Variet Worker
a99a1e3f54 docs: 배포 전 체크리스트에 log relay 필터/regex E2E/가정 검증 항목 추가 2026-04-19 14:56:40 +09:00
Variet Worker
ad4ed623bd docs: observer-dev-guide DOM 구조 BTN-DOM-DUMP 기반 갱신 + log relay 필터 규칙 추가 2026-04-19 14:48:42 +09:00
Variet Worker
64800d3c20 fix(observer): 'Running command' div에서 명령어 추출 — pre/code 대신 plain div 탐색 (v0.5.93) 2026-04-19 14:40:05 +09:00
Variet Worker
70c83b4226 fix(observer): log 필터에 CONTEXT/DEFERRED/DETECTED/BTN-DOM 추가 — 진단 로그 relay 누락 수정 (v0.5.92) 2026-04-19 14:10:59 +09:00
Variet Worker
bb54802c06 feat(enrichment): Discord 알림 지연 + Step Probe 폴링 — generic Always run 커맨드 100% 보강 (v0.5.91) 2026-04-19 10:25:55 +09:00
Variet Worker
bf53072f3c feat(enrichment): Step Probe API 메모리 기반 명령어 보강 — Always run 표시 개선 (v0.5.90) 2026-04-19 09:59:20 +09:00
Variet Worker
02b4b03699 diag(observer): Always run 버튼 주변 DOM 구조 원샷 덤프 추가 (v0.5.89) 2026-04-19 09:19:31 +09:00
Variet Worker
db805c6fde fix(observer): matchedType 대소문자 무시 — Always run이 permission으로 잘못 분류되는 문제 수정 (v0.5.88) 2026-04-19 08:01:47 +09:00
Variet Worker
7f33a20e43 docs: 배포 전 자기검증 체크리스트 추가 — 재시작 요구 최소화 정책 2026-04-19 07:58:14 +09:00
Variet Worker
ef788e6ecc docs: Observer 개발 가이드 SSOT 문서 생성 — 제약사항/배포/DOM구조/교훈 종합 2026-04-19 07:52:53 +09:00
Variet Worker
cd00986274 fix(observer): 깨진 문자열 리터럴 2건 수정 — walkNode 크래시 해결 (v0.5.87) 2026-04-19 07:51:29 +09:00
Variet Worker
12095f36a4 fix(observer): function declaration → var expression — strict mode 크래시 수정 (v0.5.86) 2026-04-19 07:41:04 +09:00
Variet Worker
498683c977 fix(approval): _from_ws 파일 60초 TTL 자동 삭제 — stale SKIP 스팸 방지 (v0.5.85) 2026-04-19 07:32:49 +09:00
Variet Worker
1662ac4f6b fix(observer): regex → 문자열 비교로 isGenericDesc 수정 — template literal escaping 회피 (v0.5.84) 2026-04-19 07:02:04 +09:00
Variet Worker
d027562f17 fix(observer): 500ms 딜레이드 컨텍스트 추출 + 버튼 셀렉터 확장 (v0.5.83)
- Always run 감지 시 desc가 generic이면 500ms 딜레이 후 재추출
- 버튼 셀렉터에 role=button, monaco-button, vscode-button 추가
- ACCEPT-SCAN 디버그 로그 (30초 간격)
2026-04-19 06:53:54 +09:00
Variet Worker
cc261011d6 fix(observer): 구 visibility 체크 제거 — Accept all 버튼 감지 차단 수정 (v0.5.81) 2026-04-19 04:18:35 +09:00
Variet Worker
37fbb9657e fix(observer): diff_review Accept all 버튼 감지 — offsetParent 체크 완화 (v0.5.80) 2026-04-19 03:53:03 +09:00
Variet Worker
965f619664 docs: relay-architecture + known-issues 업데이트 (v0.5.78-79 변경사항 반영) 2026-04-19 03:48:31 +09:00
Variet Worker
139ad3ee93 fix(extension): Retry auto-approve 흐름 복구 + Observer 형제 탐색 + thinking 필터링 (v0.5.79)
- WS response 파일에 _from_ws 마커 추가하여 processResponseFile 삭제 방지
- extractContextFromNearby에 sibling 탐색 추가 (AG Native DOM 구조 대응)
- thinking 블록 (max-h-[200px]) 필터링으로 내부 사고 릴레이 차단
- DOM 탐색 depth 5→10 확대 + pre.font-mono 우선 탐색
- 사용자 메시지 셀렉터 (.select-text.rounded-lg) 추가
2026-04-19 03:46:39 +09:00
Variet Worker
08fd08b9a6 feat(observer): diagnostic log relay via HTTP + auto-approve enrichment fallback (v0.5.63) #task-634 2026-04-18 08:18:35 +09:00
Variet Worker
326454be40 fix(bridge): move Always run auto-approve BEFORE filter chain — no more silent drops (v0.5.60) #task-634 2026-04-18 06:54:15 +09:00
Variet Worker
98b7585e3c fix(observer): text-level markdown table wrapping for Discord — AG Native uses divs not HTML tables (v0.5.59) #task-634 2026-04-18 06:46:21 +09:00
Variet Worker
c7f939ce85 fix(bridge): Always run auto-approve now checks buttons array, not just rawCmd (v0.5.58) #task-634 2026-04-18 06:35:01 +09:00
Variet Worker
7a1675fd5d feat(observer): table-to-codeblock conversion for Discord compatibility (v0.5.57) #task-634 2026-04-18 06:25:55 +09:00
Variet Worker
6b9f1188c3 feat(bridge): DOM Markdown parser restoration (v0.5.56) + code noise filter fix + user msg relay #task-634 2026-04-17 08:06:53 +09:00
Variet Worker
13e569f426 chore(bridge): update known-issues and prep for DOM Observer MD restoration (#634) 2026-04-17 06:25:22 +09:00
Variet Worker
b2f17a086b docs: devlog 20260416-005 v0.5.53 Always run auto-approve + index update 2026-04-16 22:08:16 +09:00
Variet Worker
7dbf73aa89 feat(bridge): v17 Always run auto-approve + Retry button relay (v0.5.53) #task-632
- Observer v17: ACTION_WORDS에 'Retry' 추가 — Agent terminated 대화상자의 Retry 버튼 감지/릴레이
- http-bridge: 'Always run' 버튼 자동승인 — response 파일 즉시 생성하여 observer가 바로 클릭
- bot.py: auto_approved 상태 처리 — Discord에 비대화형 '자동 승인' 알림 표시
- Observer matchedType에 'retry' step_type 매핑
2026-04-16 22:03:09 +09:00
Variet Worker
62ee081ffe fix(observer): v16 style/script strip in extractCleanStepText — CSS가 AI 응답으로 Discord 전달되는 버그 수정 (v0.5.52) #task-632 2026-04-16 21:08:52 +09:00
Variet Worker
60a2a97868 docs: E2E 코드 검증 결과 기록 — known-issues #632 업데이트 + devlog 003 2026-04-16 17:17:18 +09:00
Variet Worker
7ae43088e6 docs: devlog 20260416-002 AG Native chat relay + index update 2026-04-16 05:30:20 +09:00
Variet Worker
729875f3a6 feat(observer): v15 AG Native chat relay — scanChatBodies dual strategy (#632)
- Add AG Native DOM path: #conversation + .leading-relaxed.select-text
- Keep Cascade path: [data-testid=conversation-view] + [data-step-index]
- Register #632 in known-issues.md (SDK+DOM both blocked for AG Native)
- Bump version 0.5.50 → 0.5.51
- Add DOM analysis helper scripts
2026-04-16 05:28:44 +09:00
Variet Worker
a00d561e28 docs: devlog 2026-04-16 + known-issues v0.5.50 terminal output filter 2026-04-16 04:59:27 +09:00
Variet Worker
7ade31e4cf fix(bridge): v16 terminal output filter + v15 stale LS auto-fix + heartbeat probe (v0.5.50) #task-619
- http-bridge v16: Block terminal OUTPUT as enriched cmd — if description has no prompt marker (> » $ #), it's stdout from code block, not an actual command. Prevents 'No extension.log found' etc. from reaching Discord.
- step-probe v15: Stale LS auto-detection — if all sessions are >5min old, periodically retry fixLSConnection(). Heartbeat probe every 10 polls to detect step changes when summary API returns frozen stepCount.
- extension.ts v15: fixLSConnection() fallback — match LS processes without --workspace_id (common after AG restart)
2026-04-16 04:58:05 +09:00
Variet Worker
66233bd9cb fix(bridge): strengthen JUNK_CONTENT_RE — add CSS block, .code-block/.code-line, integration.build patterns
Unit tested: 12 legit commands pass, all junk patterns now filtered.
HTML manually patched to v14 (bypasses extension host cache).
2026-04-15 15:37:09 +09:00
Variet Worker
ed90cbf874 fix(observer/bridge): v14 — strict 5-level DOM scope, CSS/code/icon junk filter, auto-version sync (v0.5.47) #task-619
Root causes fixed:
1. extractContextFromNearby depth 20→5 — stops grabbing unrelated UI/editor code
2. JUNK_CODE_RE — rejects CSS rules, JS source code, extension internals
3. ICON_GLUE_RE — rejects Material icon text glued with content
4. Fallback span/div/p collection REMOVED entirely (always grabbed chat text)
5. html-patcher strips old observer from integration.build() cache
6. http-bridge server-side JUNK_CONTENT_RE as last line of defense
2026-04-15 14:55:58 +09:00
Variet Worker
2e32be96fe fix(observer/bridge): v13 _promptOnlySkipped fallback guard + generic button no-context filter (v0.5.46) #task-619 2026-04-15 13:27:09 +09:00
Variet Worker
87c99c7243 docs: devlog 2026-04-15 + scratch cleanup
- devlog entry: PROMPT_ONLY_RE 근본원인 분석 및 수정 (v0.5.45)
- scratch files: 이전 세션 디버깅 스크립트 전체 정리
2026-04-15 09:59:22 +09:00
Variet Worker
01539e9bfb fix(observer/bridge): PROMPT_ONLY_RE — regex escaping + ellipsis prefix handling (v0.5.45) #task-619
Observer: \\\\s (4-backslash) → \\s (2-backslash) in template literal regex
  - Per known-issues rule: regex literals need 2-backslash for \s, \d etc.
  - Old: literal \\s matched, new: whitespace class matched correctly

http-bridge: /^[\s\\\/]*[\w_.-]+\s*[>»$#]\s*$/ → /^.*[>»$#]\s*$/
  - Old pattern failed on ellipsis '…' prefix (U+2026 not in charset)
  - Terminal prompt '…\\gravity_control >' was passing through as cmd
  - New pattern: any text ending with prompt marker is skipped
2026-04-15 09:53:32 +09:00
Variet Worker
4684376c78 fix(http-bridge): remove incorrect extracted===rawDesc skip in enrichment (v0.5.44) #task-619 2026-04-15 07:47:56 +09:00
Variet Worker
7ee5947b32 fix(observer): v12 — skip prompt-only code text + enrichment validation (v0.5.44) #task-619
- extractContextFromNearby: PROMPT_ONLY_RE skips code elements containing only terminal prompts (e.g. '\\gravity_control >')
- Multi-codeEl traversal: tries all code elements at each depth, picks longest non-prompt text
- http-bridge enrichment: validates extracted command is not just a prompt fragment before enriching
- Fixes: cmd='\\gravity_control >' (prompt-only) no longer sent to Discord as the command text
2026-04-15 07:43:15 +09:00
Variet Worker
59ddcbb612 fix(http-bridge): move command enrichment before Run filter — fixes context loss (v0.5.43) #task-619 2026-04-15 06:54:06 +09:00
Variet Worker
ed63f65975 feat(observer): v11 — enhanced context extraction + 5-level sibling search + command enrichment (v0.5.42) #task-619
- extractContextFromNearby: span/div/p text fallback when pre/code not found (depth 0-8)
- collectSiblingButtons: extended to 5 parent levels, stop only when both action+reject found
- http-bridge: command enrichment — swap generic button text with actual command from description
- Fixes: cmd='Always run' now shows actual command text, Cancel button detection improved
2026-04-15 06:20:47 +09:00
Variet Worker
02d9799f20 diag(observer): v10-diag — extractContextFromNearby trail logging for context extraction failure analysis #task-619 2026-04-14 07:38:27 +09:00
Variet Worker
a9c64e6716 docs: known-issues — template literal regex escaping bug (v0.5.41) 2026-04-14 06:06:23 +09:00
Variet Worker
8e89209a27 fix(observer): template literal regex escaping — \s/\d had wrong backslash count (v0.5.41)
Root cause: regex literals inside template literal (\...\) need exactly 2
backslashes (\\s, \\d) to produce \s, \d in the output. The v9 code had 4
backslashes (\\\\s) which produced literal \\s in the browser (matching
backslash+s instead of whitespace class). This broke:
- 'Running N commands' skip pattern (never matched)
- NOISE_CODE_RE (declare/import line filters)
- cleanLines() pre-strip word boundary \\b and split/join \\n
- cleanButtonText() whitespace strip \\s

The fix restores all regex-literal patterns to exactly 2 backslashes.
new RegExp() string patterns (like NOISE_RE) already used 4 backslashes
correctly (string→regex has one extra escaping layer).

Verified: node eval of generated code confirms /^Running\s*\d+\s*commands?$/i
correctly matches 'Running2 commands' and 'Running 4 commands'.

#task-619 #task-620
2026-04-14 06:05:43 +09:00
Variet Worker
a8d875167d fix(observer): v9 - stop treating Running N commands as approval button, add DOM-climbing context extraction 2026-04-13 19:37:18 +09:00
Variet Worker
2a1ebf1020 fix(extension): UTF-8 encoding + noise filter enhancement (v0.5.39)
- http-bridge.ts: add req.setEncoding('utf8') to all POST handlers
  to fix Korean text corruption in pending/chat/dump payloads
- observer-script.ts: add inline pre-strip in cleanLines() for
  Material icon names concatenated by textContent without newlines
- observer-script.ts: apply cleanLines() to codeText extraction
- known-issues: document UTF-8 encoding and noise filter issues
2026-04-13 12:56:25 +09:00
Variet Worker
5a76e30993 docs: known-issues — html-patcher String.replace dollar-pattern corruption bug 2026-04-13 12:22:41 +09:00
Variet Worker
d6ed8764b8 fix(html-patcher): escape $ in inline script replacement to prevent String.replace() special pattern corruption — root cause of observer SyntaxError #task-619 2026-04-13 12:22:02 +09:00
Variet Worker
a214ab029f docs: devlog — observer v8 verification session, V8 cache re-cleared 2026-04-13 11:02:26 +09:00
Variet Worker
f45d2d970d fix(observer): ensure inline script injection before </body> for Electron execution + add BEACON diagnostic ping
- html-patcher: relocate inline script from after </html> to before </body>
- html-patcher: clean up duplicate </html> tags from previous bad insertions
- observer-script: add immediate BEACON fetch to /ping on script load
- http-bridge: add diagnostic request logging for non-polling endpoints
- devlog 003: crash recovery session notes
2026-04-12 21:30:58 +09:00
Variet Worker
6dc0854c47 docs: devlog 003 — observer v8 full-DOM dump 2026-04-12 07:37:52 +09:00
Variet Worker
0e03b3a300 feat(observer): v8 full-DOM unconditional dump — body tree capture at 5s/15s/60s with depth 15, indexed dump files, deep-inspect overhaul #task-619 2026-04-12 07:37:25 +09:00
Variet Worker
353265bed8 docs: AG Native bundle reverse engineering analysis — plannerResponse/Whi renderer structure, V8 cache fix, known-issues update 2026-04-12 07:05:57 +09:00
Variet Worker
eef59e6bb2 docs: devlog entry for AG Native DOM parser v7 rewrite session 2026-04-12 06:15:47 +09:00
Variet Worker
a4d7286bce refactor(observer): v7 step-aware AG Native DOM parser with data-testid/data-step-index based content extraction
- Replace CSS class-based scanning with [data-testid='conversation-view'] + [data-step-index] traversal
- New extractCleanStepText(): clone-and-strip buttons/SVG/icons before text extraction
- New extractStepContext(): step-container-aware context with header + code block
- NOISE_RE: block Material icon names, button labels, UI artifacts
- Auto DOM structure dump on first conversation-view detection
- Enhanced deep-inspect with step element + button inventory
- known-issues: document AG Native SDK API incompatibility
2026-04-12 06:14:46 +09:00
Variet Worker
70dc301dca fix(bridge): isolate DOM observation scope and strip UI noise (TypeScript declarations, metrics) from Discord pending approval embeds 2026-04-11 17:25:33 +09:00
Variet Worker
7630bf1f8c chore: Add jump_url logging and plaintext fallback to approval messages to trace silent Discord drops 2026-04-11 15:45:21 +09:00
Variet Worker
ec7883755a fix(bot): remove lingering bridge dependency triggering AttributeError on pending relay 2026-04-11 13:21:02 +09:00
Variet Worker
072f83bf25 refactor(bridge): migrate gravity bridge to pure websocket gateway architecture, deleting legacy local file scanners and dependencies 2026-04-11 13:06:38 +09:00
Variet Worker
5e697cd919 Fix DOM observer regex/container bugs and add continuous AI Chat Body scraper via HTTP Bridge 2026-04-11 00:08:55 +09:00
Variet Worker
b3825e1c8a fix(extension): skip Antigravity ghost sessions in Fallback 2 to prevent trajectory not found infinite loop 2026-04-10 22:19:09 +09:00
Variet Worker
a99c283656 fix(extension): restore AI Response Content capture by patching DOM extraction, CSP connect-src, and TS regex literal serialization 2026-04-10 21:10:33 +09:00
Variet Worker
58887f6933 chore: bump version to 0.5.23 for vsix 2026-04-10 17:39:07 +09:00
Variet Worker
488b36f192 fix(step-probe): ensure fast AI responses and tool calls are captured by real-time block 2026-04-10 17:12:21 +09:00
Variet Worker
300338d5d3 fix(extension): bypass 10-item limit of GetAllCascadeTrajectories by utilizing GetDiagnostics 2026-04-10 16:52:12 +09:00
Variet Worker
e745744636 fix(Backend): Gravity Bridge response extraction & bot exception crash loop
* Restore step.content.parts traversal missing in prior bugfix
* Catch wide exceptions in bot.py chat_snapshot_scanner and move broken files to .json.failed to prevent loop aborts blocking the pending queue
2026-04-10 16:33:02 +09:00
Variet Worker
b88e75b075 fix(bridge): eliminate discord empty embed spam by disabling DOM observer proactive pending
- removed PATS in observer-script.ts
- step-probe.ts now handles 100% of pending detection with full RPC payload context
- DOM observer restricted to trigger-click polling only
2026-04-10 16:12:15 +09:00
Variet Worker
2ece05fc6f fix(extension): resolve AI response dropping for sub-5s executions by relaxing IDLE capture condition #task-607 2026-04-10 16:05:30 +09:00
Variet Worker
6bbc9ddd00 fix(extension): resolve AI response dropping by adding nested payload extraction in step-utils 2026-04-10 15:52:25 +09:00
Variet Worker
89c95de18c fix(bridge): resolve missing Discord embed bodies by extracting detailed RPC payload from step_probe 2026-04-10 08:02:41 +09:00
Variet Worker
fadd39424b fix(bridge): fix Discord signal relay false-positives and empty body logic 2026-04-10 00:12:01 +09:00
Variet Worker
22e1799d66 fix(extension): resolve Native UI icon text gluing causing DOM observer signal drop #task-603 2026-04-09 23:13:49 +09:00
Variet Worker
e4f674ec9f docs: restore unintentionally deleted known issues (mend destructive commit) 2026-04-09 22:36:50 +09:00
Variet Worker
47c0602427 fix(extension): pin point CodeLens exclusion filter to prevent native Agent UI freezing (v0.5.22) #task-602 2026-04-09 22:32:40 +09:00
Variet Worker
75762964e3 fix(extension): adapt DOM observer to Native Agent panel and Tailwind migration (v0.5.21) 2026-04-09 21:56:29 +09:00
Variet Worker
d2023321bd fix(extension): remove redundant SafeToAutoRun chat snapshot for Discord relay
* Removed writeChatSnapshot calls in step-probe.ts to prevent duplicate ' 자동 실행됨' notifications since bot.py already broadcasts '🤖 자동 승인됨'.
* Docs: Update devlog and known-issues with Discord Bot cache deletion bugs and multi-workspace LS connection conflicts.
* Fixes Vikunja #593
2026-04-08 17:59:40 +09:00
Variet Worker
2eb1fbb6b7 fix(pipeline): resolve SafeToAutoRun deadlock and sync freezing (v0.5.20) (#589) 2026-04-08 07:30:33 +09:00
Variet Worker
13f13ee243 fix(extension): resolve 10-item limit truncation & WS zombie disconnection (v0.5.14) 2026-04-01 18:21:51 +09:00
Variet Worker
2d5059d2d5 chore(ext): version bump 0.5.11 2026-03-28 09:21:10 +09:00
Variet Worker
7bbd8749d7 fix(extension): guitar_score step-probe UTF-8 loop + approval stepIndex guard (v0.5.11) 2026-03-28 09:15:11 +09:00
Variet Worker
d5fdc41f35 fix(extension): Discord signal drop and UI freeze (async IO, regex filters, WS rate-limits) (v0.5.10) 2026-03-25 07:14:34 +09:00
Variet Worker
3ec45ac6b7 docs(devlog): record hash and Vikunja ID for session 001 and 003 2026-03-24 18:19:30 +09:00
Variet Worker
101ec20b21 fix(extension): restructure DOM observer to prevent false positive triggers (v0.5.10) 2026-03-24 18:15:05 +09:00
Variet Worker
86e5a24a75 docs(devlog): record hash and Vikunja ID for session 002 2026-03-24 14:04:04 +09:00
Variet Worker
7b6cd59801 fix(extension): support vscode native notification UI and Always Allow buttons for DOM observer (#514) 2026-03-24 13:58:21 +09:00
Variet Worker
f13bcc871c fix(ext): v0.5.8 false positive zombie socket disconnect bug resolve (timestamp replace setTimeout) 2026-03-24 07:00:43 +09:00
Variet Worker
ecebec3906 fix(bridge): resolve websocket zombie connection and bounding memory leaks 2026-03-23 21:11:52 +09:00
Variet Worker
e21f71baf8 docs: devlog 2026-03-22 VSIX v0.5.5 빌드 2026-03-22 09:15:27 +09:00
Variet Worker
b81135d855 chore(ext): version bump 0.5.5 2026-03-22 01:23:09 +09:00
Variet Worker
a6aa643be9 docs: devlog #2 wrong-LS auto-recovery (v0.5.5) 2026-03-21 21:16:49 +09:00
Variet Worker
6234301a47 fix(ext): v0.5.5 wrong-LS 자동 복구 — fixLSConnection export + 'input not registered' 감지 시 LS 재연결 + 1회 retry 2026-03-21 21:15:18 +09:00
Variet Worker
a72c522ab5 fix(extension): v0.5.4 신호 감지 3중 버그 수정 — 세션 전환 즉시 probe, reviewAbsoluteUris 필드, stepIndex uint32 clamp + permission 매핑 2026-03-21 17:51:10 +09:00
Variet Worker
f4ded343c7 docs: devlog commit hash update + known-issues idle-resume 3-bug (v0.5.2) 2026-03-21 10:58:09 +09:00
Variet Worker
5aad82c727 fix(ext+hub): v0.5.2 Idle→Resume 신호 소실 3중 버그 수정 — auth_fail 재연결 + pending_owners 보존 + step-probe 리셋 2026-03-21 10:51:02 +09:00
Variet Worker
94cbda6f3d docs: devlog #1 2026-03-19 + known-issues browser_subagent Allow 추가 + rule #12 2026-03-21 10:16:35 +09:00
Variet Worker
549af6dae2 fix(ext): browser_subagent Allow 버튼 RPC 매핑 수정 — runExtensionCode payload 적용 (v0.5.1) 2026-03-20 18:13:07 +09:00
Variet Worker
e306fae130 docs: devlog #4 — codebase health + communication audit (no changes needed) 2026-03-18 15:52:19 +09:00
Variet Worker
bc9d0f2fbb docs: devlog #3 — step-probe module split + Vikunja #414 done 2026-03-18 14:35:22 +09:00
Variet Worker
17978a750c refactor(ext): split step-probe.ts → approval-handler.ts (1597→1017+411 lines) #task-414 2026-03-18 14:34:32 +09:00
Variet Worker
0f057c0c95 docs: devlog #2 — bot unit tests + Vikunja #410 done 2026-03-18 14:06:25 +09:00
Variet Worker
a41062b6ff test(bot): bot.py unit tests — 27 cases for _write_command, _hub_on_pending, ApprovalView #task-410 2026-03-18 14:05:39 +09:00
Variet Worker
029a246658 docs: devlog 2026-03-18 커밋 해시 업데이트 2026-03-18 13:24:21 +09:00
Variet Worker
e7631177f8 refactor(cleanup): v0.5.0 Collector 제거 + dead code 정리 + HttpBridgeContext 버그 수정
- DELETE collector.py (523줄)
- main.py: BOT_MODE=remote 분기 제거
- gateway.py: Collector REST 6개 endpoint 제거 (311→168줄)
- bridge.py: RemoteTransport 제거 (480→270줄)
- config.py: REMOTE_BRIDGE_URL 제거
- extension.ts: dead code 4개 + stale module vars 제거
- step-probe.ts: getStepProbeContext() 추가, autoApproveEnabled 제거
- FIX: HttpBridgeContext stale primitive (getter 패턴으로 수정)
- ADD: extension.log rotation (10MB→2MB tail)
- docs: architecture.md, tech-stack.md, known-issues.md 업데이트
2026-03-18 11:08:59 +09:00
Variet Worker
4a5521dcc3 docs: devlog #006 + known-issues !stop stale primitive update #task-410 2026-03-18 09:20:07 +09:00
Variet Worker
ab0c116c9e fix(ext): !stop getActiveSessionId stale primitive — use step-probe getter #task-410 2026-03-18 08:34:58 +09:00
Variet Worker
07bbb626a6 docs: devlog #005 + known-issues !stop root cause update + Vikunja #411 done 2026-03-18 08:22:10 +09:00
Variet Worker
d55b6b97ad fix: stop command uses activeSessionId instead of renderer-only getActiveCascadeId #task-411 2026-03-18 08:09:29 +09:00
Variet Worker
d8eac80b2f fix(ext): !stop CancelCascadeInvocation RPC — AG 빨간■ 동일 메커니즘 적용 #task-411 2026-03-18 07:16:57 +09:00
Variet Worker
759dab55b6 fix(ext): !stop 핸들러 SDK cancelCurrentTask() 교체 — rejectAgentStep 미등록 이슈 해결 #task-411 2026-03-18 06:49:17 +09:00
Variet Worker
bbfafdc5e4 docs: rejectAgentStep 조사 결과 — CancelCascadeInvocation RPC 대안 발견 #task-411 2026-03-18 06:46:26 +09:00
Variet Worker
ac803d436f test(hub): 45개 단위 테스트 추가 — 연결 관리, pending_owners, 라우팅, 인증 #task-412 2026-03-18 06:42:51 +09:00
Variet Worker
ebf2228aa8 docs: known-issues 정리 + Vikunja #410~#414 태스크 등록 반영 2026-03-18 06:38:05 +09:00
Variet Worker
881a424b23 docs: known-issues 아카이빙 + Collector 폐기 마킹 + 레퍼런스 문서 보강 #task-409 2026-03-18 06:28:40 +09:00
Variet Worker
d06b1ea0db docs: usage-guide WS Hub 아키텍처 업데이트 + start_bot.bat Collector 경고 추가 2026-03-17 22:06:52 +09:00
Variet Worker
48ae19b3e1 docs: known-issues pending_owners lifecycle + devlog #017 2026-03-17 21:56:43 +09:00
Variet Worker
9ccfa83439 fix(hub): reassign pending_owners on WS reconnect — prevents approval response loss 2026-03-17 21:52:50 +09:00
Variet Worker
0fae7e32aa fix(ext,bot): 통신 아키텍처 감사 — writeRegistration 이중쓰기 + ApprovalView fallback + scanner 최적화
- step-probe.ts: writeRegistration WS 후 return 추가 (파일 이중쓰기 방지)
- bot.py: ApprovalView approve/reject/choice — send_response_to_pending_owner 반환값 확인 + file bridge fallback (5곳)
- bot.py: scanner 주기 3s/5s → 30s (Hub 모드 불필요 I/O 감소)
2026-03-17 21:30:05 +09:00
Variet Worker
47cc838d9d fix(ext,bot): Accept All WS regression + auto_approve dual-write — VSIX v0.4.5 2026-03-17 21:01:24 +09:00
Variet Worker
4e8ac8d6b7 docs: known-issues dual-delivery + devlog #013-014 2026-03-17 20:39:21 +09:00
Variet Worker
0da6291d98 chore(extension): bump to v0.4.4 - dual delivery fix + echo dedup 2026-03-17 20:36:27 +09:00
Variet Worker
4bb400820c fix(command-handler): add echo-dedup for WS commands to prevent Discord relay 2026-03-17 20:34:55 +09:00
Variet Worker
302d21d35c fix(bot,extension): prevent dual delivery of commands and responses via WS+file 2026-03-17 20:30:37 +09:00
Variet Worker
6640d42449 refactor(extension): split extension.ts into 3 modules - http-bridge, html-patcher, command-handler (#398) 2026-03-17 18:50:12 +09:00
Variet Worker
1ce8b7c707 docs: devlog #012 + known-issues WS 응답 라우팅 2026-03-17 17:47:21 +09:00
Variet Worker
2eea5fa638 fix(ext): WS response → tryApprovalStrategies 직접 호출 (파일 경유 제거) 2026-03-17 17:43:45 +09:00
Variet Worker
adbed69237 docs: devlog #012 final + known-issues ApprovalView WS 2026-03-17 17:08:15 +09:00
Variet Worker
442221e6a3 fix(bot): ApprovalView Hub WS 응답 라우팅 — Discord 승인이 Extension에 전달 안 되는 근본 원인 2026-03-17 14:53:22 +09:00
Variet Worker
50efd52f41 docs: devlog #012 update + known-issues ApprovalRequest 누락 필드 2026-03-17 14:30:35 +09:00
Variet Worker
f6181e552d fix(bot): ApprovalRequest missing conversation_id + timestamp in Hub path 2026-03-17 13:20:00 +09:00
Variet Worker
1bb54eb820 docs: devlog #012 + known-issues 3건 + VSIX v0.4.3 빌드 아티팩트 2026-03-17 10:48:09 +09:00
Variet Worker
9523d1328e fix(ext): workspaceUri 누락 + WS-only 전송 + user msg dedup 2026-03-17 10:38:45 +09:00
Variet Worker
96e9b8adce fix(bot): Hub WS auto-approve Discord 알림 누락 + !auto 이중발송 dedup 2026-03-17 10:37:55 +09:00
Variet Worker
edd4943e2e chore(extension): ws 모듈 번들 + E2E 사전 검증 #task-396
- extension/package.json: ws dependency 추가
- extension/.vscodeignore: !node_modules/ws/** 추가 (VSIX 번들)
- known-issues: NPM WS 프록시 + ws 모듈 미번들 이슈 추가
- devlog: #010 완료, #011 E2E 사전 검증 (미완료)
2026-03-17 08:21:43 +09:00
Variet Worker
6ea3211a58 docs: devlog #010 - 문서 재작성 + 서버 배포 + WS 호환 2026-03-17 07:42:55 +09:00
Variet Worker
b9b240de0b fix(extension): ws-client browser WebSocket API compat (.onopen/.onmessage) 2026-03-17 07:41:56 +09:00
Variet Worker
36b70505d7 docs: .env.example Hub 인증 변수 추가 2026-03-17 07:20:19 +09:00
Variet Worker
5bdaba01bd fix(infra): docker-compose.yml 서버 실제 구성 반영 + Caddyfile 제거 2026-03-17 07:18:57 +09:00
Variet Worker
28d399ba91 infra: Caddyfile ag.variet.net + docker-compose Hub env vars + Extension hubUrl 설정 2026-03-17 07:09:46 +09:00
Variet Worker
fadfd88f51 docs: architecture/tech-stack/conventions 전면 재작성 + Wiki 동기화 2026-03-17 06:48:46 +09:00
Variet Worker
61bd4b1ffb docs: devlog 009 hash update 2026-03-17 06:42:45 +09:00
Variet Worker
5f795b9a91 refactor(extension): 모듈 분리 + Hub 통합 테스트 #task-395
- extension.ts 3,446→1,289줄 (-63%)
- step-probe.ts (1,435줄): setupMonitor, processResponseFile, tryApprovalStrategies
- observer-script.ts (687줄): DOM observer script
- ws-client.ts (390줄): WSBridgeClient
- step-utils.ts (114줄): step 파싱 유틸
- auth.py (115줄): JWT + registration code
- hub.py (581줄): WSHub + per-client queue
- Hub WS 연동 테스트 통과 (auth, chat, register)
- VSIX v0.4.0 빌드
2026-03-17 06:41:42 +09:00
Variet Worker
a372bd8b2d docs: session end — known-issues 3건 (cross-project flooding, pending 누적, diff_review brain/) + devlog #008 2026-03-16 23:08:31 +09:00
Variet Worker
e3f8fb93f7 fix: cross-project event flooding + pending accumulation + diff_review brain exclusion
Phase 1: Collector auto-cleanup of auto_resolved/expired pending files after Gateway forwarding
Phase 2: Watcher project filter (only MY sessions emit events) + Collector event forward filter
Phase 3: Extension diff_review excludes brain/ artifact files (task.md, implementation_plan.md)
2026-03-16 23:05:27 +09:00
Variet Worker
7ca0bc0f1f docs: session end — known-issues 2건 (병렬 step 누락, snapshot 로깅) + devlog #007 2026-03-16 20:42:04 +09:00
Variet Worker
7f079a56a0 fix: process ALL parallel WAITING steps instead of only first one
step_probe break statement caused only one WAITING step to get
a pending file when AG runs multiple parallel tool calls.
Now iterates all WAITING steps and creates pending for each.
2026-03-16 20:36:41 +09:00
Variet Worker
fdc0084813 fix: add chat snapshot delivery success/failure logging 2026-03-16 20:22:49 +09:00
Variet Worker
f309518e78 fix: add channel failure logging to diagnose Discord notification delivery issue 2026-03-16 19:47:06 +09:00
Variet Worker
412c212c6e fix(extension): v0.3.16 — diff_review duplicate approval filter + IDLE notification + !auto echo removal 2026-03-16 19:14:43 +09:00
Variet Worker
0035394b9c docs: session end — known-issues update + devlog #005 (v0.3.15 diff_review fix) 2026-03-16 18:46:07 +09:00
Variet Worker
0fdf668abc fix(extension): diff_review use agentAcceptAllInFile instead of dead RPC strategies (v0.3.15) 2026-03-16 18:43:04 +09:00
Variet Worker
5a1d4f0b0c fix(extension): acknowledgeCodeActionStep RPC discovery + v0.3.14 3-tier strategy 2026-03-16 18:11:20 +09:00
Variet Worker
82461bc3fc fix(extension): diff_review RPC parameter experiment — 4 format variants (A/B/C/D) + known-issues update 2026-03-16 16:59:58 +09:00
Variet Worker
9ef2c3f07c fix(extension): diff_review steps=[] race condition — in-memory metadata cache (v0.3.13)
Root cause: Collector deletes pending file before Extension reads edit_step_indices.
Fix: diffReviewMetadata Map caches step indices in Extension memory.
Known issue added. Devlog entry 003.
2026-03-16 16:09:42 +09:00
Variet Worker
12a1cf8692 docs: update devlog hash 2026-03-16 14:23:38 +09:00
Variet Worker
f302984721 fix(extension): diff_review 2-strategy deploy + 8s pending delay
- Deploy AcknowledgeCascadeCodeEdit RPC strategy (was in source but never compiled)
- Add 8s setTimeout delay for diff_review pending (AI response arrives on Discord first)
- Capture closure variables for delayed pending creation safety
- known-issues: diff_review pending ordering fix
2026-03-16 14:22:41 +09:00
Variet Worker
15f6a743a4 docs: update devlog hash 2026-03-16 13:53:23 +09:00
Variet Worker
d521dd5fa3 fix(extension): step_type mapping bug + diff_review handler refactor
- Separate read tools (file_permission) from write tools (code_edit)
- write_to_file/replace_file_content now use AcknowledgeCascadeCodeEdit RPC
- diff_review: 2-strategy approach (RPC first, openReviewChanges fallback)
- Track modified_files and edit_step_indices in diff_review pending
- known-issues: 3 new entries (pending accumulation, step_type bug, isDirty failure)
2026-03-16 13:52:02 +09:00
Variet Worker
078f721187 docs(devlog): update 2026-03-16 commit hash 2026-03-16 11:12:44 +09:00
Variet Worker
2d9fe964f6 fix(bridge): v0.3.12 approval state management — sawRunningAfterPending gate + approval-flow.md system doc
- processResponseFile: set sawRunningAfterPending=true instead of removing resets
  (prevents infinite pending loop AND known-issues L479 auto_resolve regression)
- Hoist sawRunningAfterPending to module level for cross-function access
- Add recentPendingSteps memory dedup Map (60s TTL) for file-deletion resilience
- Create docs/approval-flow.md: complete system flow guide with state diagram
- Update known-issues.md: 2 new entries (state reset fix, memory dedup)
2026-03-16 11:11:50 +09:00
Variet Worker
37c0aae41c fix(bot): multi-project signal freeze — cache-only _get_channel + per-tick scanner cap
Root cause: When 3+ projects generated pending simultaneously, Bot's
pending_approval_scanner made 20-40 Discord API calls in one tick
(sequential await), triggering Discord 429 rate limits which blocked
the entire scanner for 10-30s, freezing ALL signal delivery.

Two fixes:
1. _get_channel(): Replace guild.fetch_channels() (API call) with
   discord.utils.get(guild.channels) (in-memory cache). Eliminates
   redundant API calls + Lock contention when multiple projects arrive.
2. pending_approval_scanner: Per-tick caps (5 new + 5 status) prevent
   one tick from monopolizing Discord API quota. Excess items are
   naturally processed in subsequent 3-second ticks.
2026-03-16 07:06:51 +09:00
CD
64f80212c3 docs(devlog): add entries 007-008 for system audit and agent rules 2026-03-15 23:28:28 +09:00
CD
9b93ee9776 docs(agent): add anti-local-thinking rules — NEVER #10 strengthen (disprove before report), NEVER #11 (no mechanical apply), ALWAYS #9 (project history cross-ref), Bug Report Protocol 2026-03-15 23:26:54 +09:00
CD
c9f44afcf1 fix(bridge): system audit + 5-file bug fix — PATS Deny trigger removal, auto_resolved chat dedup, UUID filenames, IP rate limit leak, bot.py deque 2026-03-15 23:01:20 +09:00
CD
429cae47b7 docs: session end — known-issues + devlog + VSIX v0.3.11 deployed 2026-03-15 18:53:00 +09:00
CD
5e5f515db4 fix(bridge): auto-approve crash — DOM observer Deny filter + bot reject-word guard + AGENT rule 2026-03-15 18:49:58 +09:00
CD
6739f8f30c fix(bridge): v0.3.11 approval flow architecture fix — eliminate double-fire auto-approve, strip 30+ failed RPC strategies, add project_name DEDUP guard
- Remove Extension-side auto-approve (was double-firing with Bot auto-approve)
- Strip failed strategies 0A-1 from tryApprovalStrategies (~150 lines)
- Keep only Strategy 0-PROTO (proto RPC) + Strategy 2 (clickTrigger)
- Add bot.py AUTO-RESOLVED logging for diagnostics
- Update known-issues with 3 new entries
- Clean deployment: v0.3.8→v0.3.10→v0.3.11
2026-03-15 17:11:38 +09:00
CD
75289b3ec5 docs: update devlog with perf optimization entry + compiled output 2026-03-15 10:54:24 +09:00
CD
ae0509fbb5 perf(bridge): 3 optimizations — pollResponseGroup 1500ms, renderer adaptive idle, Bot single-pass scanner 2026-03-15 10:51:22 +09:00
CD
f96203646e fix(bridge): 4 race condition fixes for approval lifecycle 2026-03-15 10:44:31 +09:00
CD
c910c7c386 docs: add v0.3.10 entry to devlog 2026-03-15 08:31:47 +09:00
CD
10caae1506 chore(extension): bump version to 0.3.10 2026-03-15 08:27:38 +09:00
CD
28975f9c4b docs: update devlog commit hash 2026-03-15 08:19:26 +09:00
CD
40e3cd550f fix(bridge): 5 bug fixes for approval signal drop and Discord relay
- DEDUP: add conversation_id guard to prevent cross-session step_index collision
- step_probe: suppress pending when projectName=default (empty window)
- watchCommandsDir: add 3s polling fallback (fs.watch silent fail on Windows)
- auto toggle: write chat_snapshot confirmation back to Discord
- bot on_message: add message ID dedup for Gateway event replay
2026-03-15 08:18:26 +09:00
Variet Worker
1f96997831 docs: devlog 2026-03-13 — Collector 성능 최적화 2026-03-13 23:21:58 +09:00
Variet Worker
d4a2016d06 perf(collector/watcher): 로컬 데이터 전송 성능 최적화 — mtime 프리체크, 프로젝트 캐시, re-forward 수정, 폴링 간격 조정 2026-03-13 23:13:18 +09:00
Variet Worker
1835166c5a docs: devlog 2026-03-13 — Discord 아티팩트 알림 개선 2026-03-13 09:47:50 +09:00
Variet Worker
e5a05e3ac4 feat(bot/extension/watcher): Discord 아티팩트 알림 개선 — 파일 첨부 전송, truncation 확대, 동적 .md 감시 2026-03-13 09:46:56 +09:00
Variet Worker
9036f1cefc docs: devlog #005 + known-issues — rate limit 구조적 수정 기록 2026-03-12 23:06:15 +09:00
Variet Worker
56de71470d fix(collector/bridge/gateway): rate limit 구조적 수정 — 점진적 백오프 + adaptive 폴링 + burst-friendly 윈도우 2026-03-12 22:33:49 +09:00
Variet Worker
5cdf7777a5 docs: devlog #004 업데이트 — throttle fix 커밋 추가 2026-03-12 21:15:01 +09:00
Variet Worker
bcc29f9331 fix(collector): add 0.3s throttle between multi-project command polls to prevent rate limit bursts 2026-03-12 21:11:05 +09:00
Variet Worker
5a4ac1bf9b docs: devlog #004 + known-issues — Collector multi-project polling bug 2026-03-12 20:54:23 +09:00
Variet Worker
ae51d28857 fix(collector): multi-project command polling via bridge/register/ discovery 2026-03-12 20:37:23 +09:00
Variet Worker
71f2a269f0 docs: devlog #003 추가 — workbench.html 크로스 복원 CSS 수정 (직전 세션 기록 복구) 2026-03-12 18:09:52 +09:00
Variet Worker
6d8c6f182c fix(extension): HTML 패치 안전성 강화 — pre-patch backup + 구조 검증 + 자동 복원 2026-03-12 17:55:42 +09:00
Variet Worker
bb6c03e957 docs: devlog 002 커밋 해시 업데이트 2026-03-12 17:08:00 +09:00
Variet Worker
a9feee6faa fix(extension): workbench.html 0-byte 파괴 방지 — pre-read/pre-write 안전 가드 추가 2026-03-12 17:05:29 +09:00
Variet Worker
52c9526fdb fix(bridge): 429 Rate Limit 무한 루프 방지 — 지수 백오프 + Collector 폴링 보호 + rate limit 완화 2026-03-12 00:50:29 +09:00
Variet Worker
feb8c05a73 chore: 테스트 파일 삭제 2026-03-12 00:02:13 +09:00
Variet Worker
3fcf4f7037 docs: 세션 종료 — AGENT.md 검증 규칙 + known-issues 3건 + devlog
AGENT.md:
- NEVER #8-9: 실제 E2E 테스트 없이 '구현 완료' 금지
- ALWAYS #7-8: 실행 검증 필수, '구현'과 '검증' 구분 보고

known-issues 3건:
- Collector 동기→aiohttp 전환 기록
- Extension fs.watch response 감지 누락 (미해결)
- rejectAgentStep 미등록 (미해결)
2026-03-12 00:00:30 +09:00
Variet Worker
d7ed454332 fix: 나노 검증 — health URL/response polling/startup 상태변경 3건 수정
1. health_check: /../health → /health (URL 해석 오류)
2. response polling: startup pending 제외 (불필요한 HTTP 요청 방지)
3. startup pending 상태변경: pending→skip, auto_resolved/expired→forward
2026-03-11 23:01:24 +09:00
Variet Worker
1bf41ceee3 refactor: 아키텍처 수정 — 동기HTTP→aiohttp + 연결 모니터링 + 재시도큐
#1 동기 HTTP → async aiohttp (Critical)
  - RemoteTransport: urllib.request → aiohttp.ClientSession
  - 모든 HTTP 요청이 non-blocking으로 전환
  - 이벤트 루프 블로킹 문제 해결

#2 연결 상태 모니터링
  - RemoteTransport: connected 플래그 + 연속 실패 카운터
  - Collector: 30초마다 health check → 실패 시 경고 로그
  - 연결 복구 시 ' Gateway connected' 메시지

#3 실패 재시도 큐
  - RemoteTransport: _retry_queue (최대 100건)
  - POST 실패 시 큐에 저장, 연결 복구 후 자동 재전송
  - Collector: 10초마다 retry flush
2026-03-11 22:55:54 +09:00
Variet Worker
d2a477e12e fix(collector): MERGE + auto_resolved/expired 상태 변경 감지
- pending 파일 콘텐츠 해시 추적 (_pending_hashes)
- 내용 변경 시 Gateway에 재전달 (MERGE: command 업데이트, status 변경)
- _startup_pending으로 시작 시 기존 파일과 신규 파일 분리
2026-03-11 22:48:47 +09:00
Variet Worker
58a421f5a6 fix: 전체 시스템 감사 — 6건 수정 (보안 + 안정성)
Bug 1 (만료됨 스팸): Collector 시작 시 기존 pending skip
Bug 2 (pending 미삭제): Gateway에서 response 소비 시 pending도 삭제
Bug 3 (재시작 중복): Bug 1로 해결

Security 1: API 요청 1MB 크기 제한 (client_max_size)
Security 2: IP별 rate limiting (10 req/s)
Security 3: _commands 메모리 누수 방지 (TTL 30분)
2026-03-11 22:42:05 +09:00
Variet Worker
7eca0763c9 fix(collector): 기능 누락 3건 수정 — Discord 명령어/채팅/등록 중계
Gap 1: Discord→Extension 명령어 깨짐
  - bot.py: _write_command() 래퍼 — gateway.push_command()도 호출
  - main.py: bot.gateway 연결
  - 슬래시 명령어 + on_message 모두 _write_command 사용

Gap 2: Chat snapshot 미전달
  - collector.py: _forward_chat_snapshots_loop 추가

Gap 3: Session registration 미전달
  - collector.py: _forward_registrations_loop 추가
2026-03-11 22:31:08 +09:00
Variet Worker
3d75825bba feat(collector): brain event 중계 추가 — Watcher 이벤트를 Gateway로 전달
- collector.py: _forward_events_loop — BrainEvent를 JSON으로 serialize하여 /api/event POST
- gateway.py: /api/event 엔드포인트 — 수신한 이벤트를 bot event_queue에 주입
- main.py: event_queue를 CollectorBridge에 전달

이제 task.md, implementation_plan, walkthrough 변경사항이 Collector→Gateway→Discord 경로로 전달됨
2026-03-11 22:24:48 +09:00
Variet Worker
7e36db5191 fix(docker): Dockerfile에 parser.py 등 누락 — COPY *.py로 수정 2026-03-11 21:24:09 +09:00
Variet Worker
c60d14e408 docs: devlog 2026-03-11 항목 006~009 커밋 해시 및 추가 기록 2026-03-11 20:18:36 +09:00
Variet Worker
95c2905e14 feat(collector): RemoteTransport + CollectorBridge 구현 — Collector↔Gateway HTTP 통신 완성
- bridge.py RemoteTransport: HTTP 클라이언트, API Key auth, Gateway API 매핑
- collector.py CollectorBridge: 3개 async loop (pending 전달, response 폴링, commands 폴링)
- main.py: BOT_MODE=remote → CollectorBridge 실행 (Discord bot 없이)
- config.py: GATEWAY_API_KEY 설정
- .env.example: 모든 설정 항목 업데이트
2026-03-11 20:10:45 +09:00
Variet Worker
95da3e9307 feat(gateway): API Key 인증 + HTTPS (Caddy) 보안 강화
- gateway.py: auth middleware — /api/* 엔드포인트에 Bearer token 필수
- Caddyfile: Let's Encrypt 자동 HTTPS 리버스 프록시
- docker-compose.yml: Caddy 추가, Gateway 포트 내부 전용
- config.py: GATEWAY_API_KEY 설정 추가
- .env: 키 생성 명령어 가이드 포함
2026-03-11 19:49:24 +09:00
Variet Worker
6dbbb57fa7 feat(gateway): Docker Gateway 봇 + HTTP API 구현 #task-311
- gateway.py: Collector↔Gateway HTTP API (pending, response, chat, register, commands)
- Dockerfile + docker-compose.yml: BOT_MODE=gateway, port 8585
- main.py: gateway 모드 (watcher 비활성, GatewayAPI 시작)
- config.py: gateway 모드 BRAIN_PATH 검증 스킵
- requirements.txt: aiohttp 추가
- docs/usage-guide.md: Docker 배포 섹션 추가
- Extension VSIX v0.3.9 빌드 (auto-approve 포함)
2026-03-11 19:38:26 +09:00
Variet Worker
c1303999cf feat(bot,bridge): P1 !auto 토글 자동승인 + P2 BridgeTransport 추상화 #task-304 #task-305
P1: !auto 토글 (bot.py + extension.ts)
- auto_approve_projects set으로 프로젝트별 상태 관리
- !auto → on/off 토글, pending 자동 승인 + 🤖 자동 승인됨 embed
- Extension step_probe에서 autoApproveEnabled 시 직접 tryApprovalStrategies

P2: BridgeTransport 추상화 (bridge.py)
- BridgeTransport ABC + LocalTransport (기존 동작 100% 호환)
- RemoteTransport 스켈레톤 (multi-PC 대비)
- config.py BOT_MODE/REMOTE_BRIDGE_URL, main.py transport 주입

docs: usage-guide.md + tech-stack.md Python 경로 기록
2026-03-11 19:25:40 +09:00
Variet Worker
1696a2976b fix(config,extension): BRAIN_PATH 빈문자열 버그 + 크로스프로젝트 DEDUP MERGE 수정
- config.py: os.getenv BRAIN_PATH 빈값 시 CWD 해석 → or 패턴으로 수정
- extension.ts: writePendingApproval DEDUP에 project_name 가드 3곳 추가
- extension.ts: HTTP /pending file_permission dedup에도 project_name 가드
- known-issues: 2건 추가 (BRAIN_PATH, DEDUP MERGE)
- devlog: 2026-03-11 생성
2026-03-11 09:36:55 +09:00
Variet Worker
71aa80d144 fix(extension): v0.3.9 — SDK JS 파일 VSIX 포함 수정 + start_bot.bat conda Python 우선 2026-03-11 00:01:26 +09:00
CD
ff559bc6ee chore: .agents 워크플로우/레퍼런스/가이드 전체 추가 (.gitignore 규칙 제거) 2026-03-10 23:29:28 +09:00
CD
a0d46f1ff3 fix(extension): SDK LS 대소문자 매칭 버그 수정 — fixLSConnection() 추가 (멀티프로젝트 신호 누락 해결) 2026-03-10 22:51:02 +09:00
CD
4d780ec5e7 docs: devlog 013 + known-issues (Reload Window stale session, RUNNING 우선 선택, IDLE 채널) 2026-03-10 22:21:32 +09:00
CD
6179c4d242 fix(bridge): RUNNING 세션 우선 선택 + IDLE 채널 자동 생성 제거
- extension: bestSession 선택에 2단계 비교 (RUNNING > IDLE, then modTime)
- extension: [SESSION-FILTER] 진단 로그 + [projectName] 로그 접두사
- bot: pending_approval_scanner의 IDLE 프로젝트 자동 채널 생성 제거
- known-issues: 2개 항목 추가 (IDLE 고착, 채널 증식)
2026-03-10 21:56:46 +09:00
CD
5a3217d31a fix(extension): 크로스 프로젝트 response watcher 우회 수정 + file_permission write 도구 3-button 매핑
- response watcher: pending 삭제 후 response data의 project_name으로 fallback 필터
- processResponseFile: sessionId를 pending에서 우선 사용 (activeSessionId 대신)
- logToFile: [projectName] 접두사 추가 (공유 로그 구분)
- file_permission 리스트에 replace_file_content, write_to_file, multi_replace_file_content 추가
- UserResponse에 project_name 필드 추가 + bot.py 4곳 전파
- known-issues: 2건 추가, devlog 012
2026-03-10 21:02:06 +09:00
CD
08c5cb461b docs: devlog 011 + known-issues (workspace URI 세션 격리) 2026-03-10 19:33:39 +09:00
CD
ae91134ff2 fix(extension): v0.3.8 — workspace URI 기반 세션 필터링 (멀티프로젝트 격리) 2026-03-10 19:28:32 +09:00
CD
c9524fc8a8 fix(extension): v0.3.7 — file_permission 3-button 주입 + active_project.lock 제거
- writePendingApproval()에서 step_type=file_permission일 때 자동 3-button 주입
- active_project.lock 메커니즘 제거 (멀티 프로젝트 동시 사용 지원)
- step_probe auto-resolve에 project_name 필터 추가
- known-issues 2건 추가
2026-03-10 18:48:51 +09:00
CD
11a4730873 docs: devlog 008-009 (project lock, stale reject, v0.3.6 release) 2026-03-10 17:50:00 +09:00
CD
bd46beabb1 release: v0.3.6 — deployment package (VSIX + bot launcher + stale response filter + project lock) 2026-03-10 17:44:24 +09:00
CD
95d4f854f5 fix: skip stale timeout responses (>2min old reject) to prevent phantom REJECT duplicates 2026-03-10 17:23:47 +09:00
CD
186875ad0b feat: single active project lock — warns if another project already connected to Discord 2026-03-10 17:13:20 +09:00
CD
99f3f264ed docs: devlog entries 006-007 (diff review relay, step_type passthrough, file_permission auto-detect) 2026-03-10 15:56:14 +09:00
CD
d1586c5e97 fix: auto-detect file_permission for file-related tools in step_probe + always check cmd for allow 2026-03-10 15:50:01 +09:00
CD
4dcb78c1ce fix: focus dirty files before executing agentAcceptAllInFile command 2026-03-10 15:34:37 +09:00
CD
0470c03ab3 fix: add step_type to default approve/reject/timeout callbacks (not just multi-choice) 2026-03-10 15:29:55 +09:00
CD
26c19fb6be fix: add step_type to ApprovalRequest (was being filtered out by known-fields logic) 2026-03-10 15:16:18 +09:00
CD
c4dfbcad67 fix: increase pending timeout to 30min, pass step_type through response 2026-03-10 15:07:36 +09:00
CD
7982263fcd fix: pass step_type through response file for diff_review routing 2026-03-10 15:02:24 +09:00
CD
8fbf6bf6b7 fix: diff review uses cumulative file tracking instead of IDLE-time step scan 2026-03-10 14:44:16 +09:00
CD
f8f9ce8f5f fix: init lastUserInputStepIdx + lastResponseCaptureStep on session change (prevents stale replay) 2026-03-10 14:35:56 +09:00
CD
82b727a1e6 fix: skip echo relay for Discord-origin user messages 2026-03-10 14:31:47 +09:00
CD
c15b0f676f feat: diff review Discord relay — Accept/Reject all via VS Code commands 2026-03-10 14:28:01 +09:00
CD
8a6428efa8 docs: devlog 004-005 entries (auto_resolved sync + #253 relay) 2026-03-10 14:10:23 +09:00
CD
b50012075e feat: full conversation relay #253 — user messages + error notifications to Discord 2026-03-10 14:08:14 +09:00
CD
514c0f2738 fix: extract user message from userInput.userResponse field (discovered via step dump) 2026-03-10 14:05:22 +09:00
CD
17dd6654f1 feat: relay AG-side user messages to Discord via chat_snapshots 2026-03-10 13:58:19 +09:00
CD
048ffd90a3 feat: auto_resolved sync + expired card update + DOM step_index 2026-03-10 13:52:27 +09:00
CD
93439d2f1c docs: devlog index 002+003, known-issues update (verbosity + file_permission), Vikunja #276 #277 done 2026-03-10 13:46:43 +09:00
CD
a440868101 docs: devlog 20260310-003 — approval flow improvements summary 2026-03-10 13:42:52 +09:00
CD
47dbd38c7c fix: show actual arg values (paths, queries) instead of parameter names in approval 2026-03-10 13:30:01 +09:00
CD
e107b70510 fix: dedup file_permission pendings (10s window) + clean description text 2026-03-10 13:21:18 +09:00
CD
bec38f9a6a fix: filter DOM Observer Run-only pendings when step_probe already has pending 2026-03-10 13:08:50 +09:00
CD
14d2acf6c4 feat: 3-button file permission UX (Allow Once / Allow This Conversation / Deny) 2026-03-10 12:45:12 +09:00
CD
c9b4fd4722 fix: route file_permission scope by cmd (once=1, conversation=2) 2026-03-10 11:20:55 +09:00
CD
c612c37105 fix: module-scope stallProbed + reset after approval for consecutive detection 2026-03-10 11:16:23 +09:00
CD
857e10126d fix: add verbosity=DEBUG to all step_probe calls for full command text 2026-03-10 11:11:10 +09:00
CD
75a3482a9c fix: command length 150->1500, filter EPHEMERAL_MESSAGE, widen approval gate 2026-03-10 11:01:45 +09:00
CD
df592723b7 feat: file_permission interaction + DOM Observer RPC passthrough 2026-03-10 10:54:28 +09:00
CD
563fbadd5a docs: devlog 20260310-002 session summary 2026-03-10 10:42:44 +09:00
CD
2958bdc950 feat: real-time PLANNER_RESPONSE capture on every delta>0 during RUNNING 2026-03-10 09:54:30 +09:00
CD
9b047c0c7d fix: extract text from plannerResponse.modifiedResponse field 2026-03-10 09:38:24 +09:00
CD
7ed2db90df fix: add verbosity=DEBUG to GetCascadeTrajectorySteps for response text 2026-03-10 09:13:13 +09:00
CD
1089c6ce61 fix: extract text from plannerResponse field for Discord relay 2026-03-10 09:02:16 +09:00
CD
e586bb6d41 feat: capture AI text responses on RUNNING->IDLE for Discord relay 2026-03-10 08:43:57 +09:00
CD
8c6d25c6d4 fix: add snapshot diagnostics + lower content filter for Discord messages 2026-03-10 08:18:36 +09:00
CD
628b5ae2fa fix: use stepOffset to bypass 775-step API limit with full details 2026-03-10 08:08:36 +09:00
CD
2361aa7558 fix: disable ResolveOutstandingSteps + add 775-limit stall fallback 2026-03-10 08:03:57 +09:00
CD
0e3a896c86 feat: step_type routing for all approval interaction types 2026-03-10 07:56:36 +09:00
CD
1f63f60280 feat: proto-based RPC approval for Run commands via Discord
Decoded HandleCascadeUserInteractionRequest protobuf schema from AG's
extension.js (message #162, base64 FileDescriptor 78KB).

Working payload (variant PROTO-0):
  cascadeId + interaction.{trajectoryId, stepIndex, runCommand.confirm}

Changes:
- extension.ts: Added Strategy 0-PROTO with decoded proto RPC call
- extension.ts: Fixed processResponseFile to call tryApprovalStrategies()
  instead of direct clickTrigger (was bypassing all strategies)
- extension.ts: Fixed false positive Run detection (sessionStalled reset
  when step_probe confirms no WAITING)
- extension.ts: Moved lastPendingStepIndex to module scope
- extension.ts: Added activeTrajectoryId tracking from session init
- bot.py: Added MERGE detection + Discord message edit for command updates
- bot.py: Added _sent_commands tracking for merge detection

Proto RE methodology:
1. Found schema exports in AG extension.js
2. Located fileDesc() with base64 protobuf descriptor
3. Decoded 58KB raw proto, found message names
4. Extracted CascadeRunCommandInteraction.confirm field
5. Tested camelCase JSON via ConnectRPC = SUCCESS
2026-03-10 07:45:10 +09:00
CD
98646fed27 docs: update devlog index with commit hash aab1cfb 2026-03-10 06:34:38 +09:00
CD
aab1cfba27 fix(bridge): approval ENOENT race condition + multi-choice button grouping #task-276 #task-277 2026-03-10 06:32:20 +09:00
CD
373c0f7ddc fix(bridge): approval flow robustness — pending cleanup, MERGE dedup, false positive filter, auto_resolve, 30min timeout 2026-03-10 00:41:39 +09:00
CD
7fdefb0c63 docs: update devlog index with commit hash 4ba65f9 2026-03-09 23:26:39 +09:00
CD
4ba65f9fc7 feat(bridge): Retry/Dismiss/Reject-all button detection + agent_guide workflow integration #task-274 2026-03-09 23:26:04 +09:00
CD
7a387630dc docs: update devlog index with commit hash 18b3734 2026-03-09 22:37:36 +09:00
CD
18b3734c02 fix(bridge): approval flow tuning — dedup + text cleanup + stall fallback removal + safe reject #task-256 2026-03-09 22:31:44 +09:00
CD
520d36ea43 docs: E2E approval flow success verification #task-264 #task-255 2026-03-09 21:44:54 +09:00
CD
bf0e046cbb docs: update devlog index with commit hash 08077e8 2026-03-09 20:59:47 +09:00
CD
08077e8afa fix(bridge): CSP script-src 'unsafe-inline' patch for renderer v3 execution #task-264 2026-03-09 20:35:38 +09:00
CD
da31740cc2 docs: V8 CachedData diagnosis + cache clearing for renderer v3 #task-264 2026-03-09 20:03:35 +09:00
CD
5971a524ea fix(bridge): workbench.html inline v3 script injection + both-HTML loop patch #task-264 2026-03-09 19:38:06 +09:00
CD
23bd8f4613 docs: add approval strategy decision chain to known-issues (handoff clarity) 2026-03-09 18:28:45 +09:00
CD
62306d3cf1 docs: update devlog index with commit hash a07d9d3 2026-03-09 18:25:04 +09:00
CD
a07d9d3803 feat(bridge): deep-inspect HTTP endpoint + recursive DOM inspector #task-264 2026-03-09 18:24:41 +09:00
CD
dddbd2b96f docs: update devlog index with commit hash 32bf5ae 2026-03-09 18:07:01 +09:00
CD
32bf5ae416 feat(bridge): renderer v3 deep DOM traversal (iframe/webview/shadow) #task-255
- deepFindButtons(): traverse iframe contentDocument, webview.executeJavaScript, shadow DOMs
- dumpDOMStructure(): startup diagnostic dump of all iframes/webviews/buttons
- 3-phase trigger-click: deep DOM → webview execJS → iframe direct
- known-issues: webview iframe isolation confirmed, v3 solution documented
2026-03-09 18:06:01 +09:00
CD
5e64860c3f docs: update devlog index + known-issues with renderer DOM click status 2026-03-09 15:12:12 +09:00
CD
4497e966b9 feat(bridge): renderer DOM click approval + command discovery diagnostic
- CMD-DISCOVERY: enumerate all antigravity.* commands at activation (72) and during WAITING state (119)
- APPROVAL-CMD-CHECK: re-check commands inside tryApprovalStrategies for dynamic registration
- Confirmed: ALL 7 SDK approval commands NOT REGISTERED in current AG build
- Confirmed: sendChatActionMessage, executeCascadeAction also NOT REGISTERED
- Replaced failed keyboard simulation (Strategy 2) with renderer DOM click approach:
  - Added clickTrigger variable + GET /trigger-click HTTP endpoint
  - Renderer polls /trigger-click every 1s, clicks Run/Accept button via DOM
- Updated known-issues.md with comprehensive findings
- Added devlog entry 20260309-002
2026-03-09 15:09:13 +09:00
CD
3b1bb9246e feat(bridge): step-type-specific approval commands + SDK research
- tryApprovalStrategies: terminalCommand.run > terminalCommand.accept > command.accept > acceptAgentStep
- Step probe: immediate on first stall (5s), 775-limit detection with dynamic fallback
- NOTIFY filter: skip <50 chars, TASK dedup by taskName+taskStatus
- BTN-DUMP diagnostic removed from renderer
- Focus: agentPanel.focus + agentSidePanel.focus (verified SDK commands)
- known-issues: add step-type command mismatch finding
2026-03-09 09:19:36 +09:00
CD
027135e2b5 fix(bridge): response file race condition + Run button regex + known issues
- Fix: processResponseFile no longer deletes response files for DOM observer
  approvals, allowing renderer pollResponse to find and serve them via HTTP
- Fix: Run button regex ^Run$ → ^Run to match 'Run Alt+⏎' button text
- Fix: BTN-DUMP diagnostic added to generateApprovalObserverScript (source)
- Doc: 2 new known issues (race condition, renderer script 3-location confusion)
- Doc: devlog entry #19
2026-03-08 22:58:17 +09:00
CD
32726d4d3a docs(devlog): 접근 과정 + 실패 사례 상세 기록 (entry #018 보강) 2026-03-08 20:25:48 +09:00
CD
810fbcc114 feat(bridge): 승인 감지 최적화 — latestToolCallStep 즉시 감지 + DOM scan 확장
- latestToolCallStep RPC 기반 즉시 감지 (30초 stall → 5초 poll)
- DOM scan 범위: findPanel() → document.body 확장
- Accept all/Reject all 리뷰 바 패턴 추가
- Stall detection을 100초 fallback으로 약화
- extractToolCommand/extractToolDescription 헬퍼 추가
- known-issues 5건 신규 추가
- start/services workflow: Python 전체 경로 + services.md 로딩

#task-258 #task-262
2026-03-08 20:21:11 +09:00
CD
8ed1ece87a fix(bridge): renderer script debugging — async fetch, install path fix, product.json checksums
- Replace sync XHR tryPing() with async fetch tryPingAsync() for port discovery
- Add ag-sdk JS file to product.json checksums in updateProductChecksums()
- Revert to inline script approach for jetski HTML (vscode-file:// blocks custom .js)
- Remove old external script tag cleanup, add inline markers
- Update known-issues with 3 new findings
- Add devlog entry #16
2026-03-08 19:51:27 +09:00
CD
43f023c87e fix(bridge): v0.3.5 — inline script + deterministic port + auto-checksum
- vscode-file:// refuses custom .js files → inline script into HTML
- Random port → deterministic port from project name hash (gravity_control=34332)
- Hardcoded port in renderer script for immediate discovery
- Auto-update product.json SHA256 checksums after HTML modification
- Bump version 0.2.0 → 0.3.5
2026-03-08 18:37:09 +09:00
CD
afb1a1d6e6 docs(bridge): product.json 체크섬 불일치 근본 원인 기록 #task-258 2026-03-08 17:43:48 +09:00
CD
b92c3c072f fix(bridge): multi-window isolation v0.3.4 2026-03-08 16:56:23 +09:00
CD
c97414cd37 fix(bridge): stall-based approval detection + known issues from deep debugging
- IDLE→stall detection: RUNNING+delta=0 for 6 polls (30s)
- lastModifiedTime-based thinking filter (partial)
- ResolveOutstandingSteps confirmed CANCELS steps (removed)
- HandleCascadeUserInteraction always socket hang up (removed)
- VS Code accept commands: silent success, no effect
- Hybrid approval: focus+all commands sequential, no break
- logToFile: console.log backup added
- Known issues: 4 critical findings documented
- better-antigravity reference added for future research
2026-03-08 14:38:41 +09:00
CD
2574ce6f08 feat: immediate pending detection for all step types 2026-03-08 10:19:27 +09:00
CD
7a38e7ecc9 feat: auto-WAITING detection via stall + step query 2026-03-08 09:56:01 +09:00
CD
0bf3217ae1 fix: panel focus before approval 2026-03-08 09:43:55 +09:00
CD
e7bc4046a4 fix(bridge): hybrid approval — SDK rawRPC + VS Code commands
Root cause: VS Code commands (acceptAgentStep, terminalCommand.run etc)
return undefined silently but don't actually accept WAITING steps.
LS requires HTTPS + CSRF token for RPC calls.

New approach: Phase 1 tries SDK rawRPC (has CSRF auth) with
HandleCascadeUserInteraction + ResolveOutstandingSteps.
Phase 2 tries all VS Code commands as fallback.
All results logged to bridge/extension.log for debugging.

Also removed stall detection (fundamentally broken — stepCount
keeps incrementing from other tool calls during WAITING).
2026-03-08 09:29:40 +09:00
CD
c98b6432f8 docs: devlog 08 entries 8-11 (polling overhaul, stall detection, approval handler) 2026-03-08 08:20:32 +09:00
CD
f1f9a0b40b fix(bridge): safer stall detection + VS Code command-based approval
Stall detection fixes:
- Threshold 2→6 polls (30s minimum stall before triggering)
- Added lastModifiedTime tracking (both stepCount AND modTime must freeze)
- Cooldown 30s→60s between pending writes
- Track lastPendingStepCount to prevent retrigger for same stall

Approval handler fixes:
- Replace HandleCascadeUserInteraction RPC with VS Code commands
- Sequential fallback: acceptAgentStep → command.accept → terminalCommand.run
- Same pattern for reject: rejectAgentStep → command.reject → terminalCommand.reject
- Removed SDK dependency check (VS Code commands work without SDK)
2026-03-08 08:14:35 +09:00
CD
9b9c9c71fe fix(bridge): stall-based WAITING detection, remove GetCascadeTrajectorySteps
GetCascadeTrajectorySteps has 775-step hard limit and cannot see
WAITING steps beyond that. No RPC exists for direct WAITING detection.

New approach: if stepCount frozen for 2+ polls (~10s) while status is
RUNNING, treat as WAITING and write pending approval to Discord.
30s cooldown prevents duplicate pending messages.

Also removes the last GetCascadeTrajectorySteps call from Extension -
now only a single GetAllCascadeTrajectories RPC per 5s poll cycle.
2026-03-08 08:05:41 +09:00
CD
f6ae9c87a5 fix(bridge): remove SDK EventMonitor to stop ERR_CONNECTION_REFUSED spam
EventMonitor was dual-polling GetCascadeTrajectorySteps every 2s via rawRPC,
which has a 775-step hard limit and generates connection errors on port change.

Changes:
- Remove entire SDK EventMonitor (onStepCountChanged, onNewConversation, etc.)
- Keep only GetAllCascadeTrajectories POLL at 5s interval
- Remove all sdk.monitor.stop() calls
- Unleash ERR_CONNECTION_REFUSED (127.0.0.1:1080) is Antigravity's own issue
2026-03-08 07:51:50 +09:00
CD
854f33b816 fix(bridge): use GetAllCascadeTrajectories for real-time relay
Root cause: GetCascadeTrajectorySteps has 775-step hard limit,
startStepIndex parameter is completely ignored (verified via direct RPC).

Solution: GetAllCascadeTrajectories returns:
- stepCount: real-time (verified 1413->1457 live)
- latestNotifyUserStep: full notificationContent
- latestTaskBoundaryStep: full taskName/Status/Summary
- stepIndex on each for dedup

E2E verified: Python script -> RPC -> snapshot -> Bot -> Discord
2026-03-08 07:37:39 +09:00
CD
c3964f8e7a fix(bridge): rawRPC direct polling + SDK analysis docs + trial-and-error log
- Root cause: getDiagnostics.lastStepIndex is stale, SDK EventMonitor cannot detect real-time step changes
- Fix: Direct rawRPC('GetCascadeTrajectorySteps') polling every 5s
- Relay: PLANNER_RESPONSE, NOTIFY_USER, TASK_BOUNDARY, WAITING steps
- Added: docs/discord-bridge-analysis.md (full SDK architecture analysis)
- Added: docs/devlog/entries/20260308-003.md (trial-and-error history)
- Added: antigravity-sdk-main/ source reference
- Vikunja: #252 done, #253 created, #251 commented
2026-03-08 07:08:25 +09:00
CD
731dad35bf docs: update devlog with commit hash 0c3d6cd 2026-03-08 05:56:47 +09:00
CD
0c3d6cdb6d fix(bridge): step structure discovery + approval watcher + AI text relay
- plannerResponse.response = user-facing text field (confirmed)
- step.runCommand.commandLine = command (not toolCall.argumentsJson)
- Add response watcher: bridge/response/ → ResolveOutstandingSteps RPC
- Fix AI text: use modifiedResponse/response, last-wins, dedup
- Fix flooding: slice(-delta) to skip old steps on reload
- Bot: 404 cache invalidation for deleted Discord channels
2026-03-08 02:29:17 +09:00
CD
876143d397 fix(bridge): align Extension protocol with Bot — 3 mismatches fixed
- Snapshot: response/chat_snapshot.txt → chat_snapshots/*.json
- Command field: cmd.message → cmd.text (matches Bot.write_command)
- RPC: GetConversation (404) → GetCascadeTrajectorySteps
- Bundle sql-wasm.js + sql-wasm.wasm into VSIX (45KB→379KB)
- Handle consumed flag, clean 38 stale commands
- Add extractAIText helper with fallback chain
2026-03-08 01:14:20 +09:00
CD
4bb72921ae feat: embed antigravity-sdk source — zero npm dependencies (45KB VSIX) 2026-03-08 00:45:07 +09:00
CD
bc2fca0da4 feat: complete SDK rewrite — antigravity-sdk v1.6.0 integration (1155→280 lines) 2026-03-08 00:38:45 +09:00
CD
d9e20301c5 probe: Trial E2 complete — Electron access limited, hybrid strategy analysis 2026-03-08 00:01:41 +09:00
CD
8c0736fe2b probe: Trial E2 — Electron webContents + undocumented VS Code commands 2026-03-07 23:38:25 +09:00
CD
87094e00b0 feat: subscribeToStream on new cascade — real-time StreamCascadeReactiveUpdates subscription 2026-03-07 23:23:16 +09:00
CD
12131b9103 probe: Trial D3 — streaming RPC with protocol_version=1 + cascadeId combined 2026-03-07 23:05:54 +09:00
CD
4d1bb7a443 probe: Trial D2 — fixed protobuf encoding (protocol_version=1, cascadeId, ConnectRPC framing) 2026-03-07 22:58:56 +09:00
CD
2794a26c77 probe: Trial D — streaming RPCs + JSON retry with live auth context 2026-03-07 22:52:30 +09:00
CD
d213c2f0f5 probe: Trial B2 — getChromeDevtoolsMcpUrl, getWsTargets, remote-debugging-port scan 2026-03-07 21:22:23 +09:00
CD
026e7d5e33 docs: add SDK analysis results, revise trial plan with sendChatActionMessage discovery 2026-03-07 21:15:19 +09:00
CD
7ba1e1b977 probe: Trial A (extension exports) + Trial B (chat commands) — systematic approach 2026-03-07 20:52:33 +09:00
CD
804aa19b35 docs: confirm #14 failed, add systematic trial plan (A→E) 2026-03-07 20:48:14 +09:00
CD
3fab31b465 docs: comprehensive approach history (14 attempts, failures, unexplored alternatives) 2026-03-07 20:43:41 +09:00
CD
0d90b257c3 feat: extractFromLogs for AI response text, remove failed RPCs, summary fallback 2026-03-07 20:31:58 +09:00
CD
b0c2f865c8 feat: try LoadTrajectory + multiple RPCs for actual AI response text, summary as fallback 2026-03-07 20:15:27 +09:00
CD
7415ab7890 feat: immediately relay summary when new conversation detected with AI response 2026-03-07 20:06:19 +09:00
CD
41f90b3b15 fix: track ALL trajectories, detect new conversations, summary fallback per-traj 2026-03-07 19:55:34 +09:00
CD
0c9664542e fix: use getDiagnostics for cascade IDs + summary fallback + stop poll spam 2026-03-07 19:43:31 +09:00
CD
dfc76a9b4b fix: fallback chain for step retrieval (5 method+field combos) 2026-03-07 19:32:32 +09:00
CD
b6adeff402 fix: use real API response (trajectoryId, current:true, step count diffing) 2026-03-07 19:25:48 +09:00
CD
e4b98af308 docs: LS ConnectRPC reference (100+ RPC methods) + devlog + commands update 2026-03-07 19:16:33 +09:00
CD
be6fae71de fix: correct RPC method names from LS binary (Heartbeat, GetUserTrajectoryDescriptions, GetCascadeTrajectorySteps) 2026-03-07 19:00:11 +09:00
CD
f2ed431aa5 fix: LS ConnectRPC use HTTPS when detected, not port heuristic 2026-03-07 18:31:13 +09:00
CD
91b3a7ef20 feat: LS ConnectRPC bridge for AI response relay to Discord 2026-03-07 18:15:01 +09:00
CD
150967deee probe: getManagerTrace + getWorkbenchTrace + LS port discovery 2026-03-07 18:04:18 +09:00
CD
952883d3d2 probe: getDiagnostics structure discovery for AI response relay 2026-03-07 17:54:45 +09:00
244 changed files with 38225 additions and 1918 deletions

70
.agents/AGENT.md Normal file
View File

@@ -0,0 +1,70 @@
---
description: 모든 작업에 자동 적용되는 에이전트 행동 규칙. 새 대화 시작 시 반드시 이 파일을 먼저 읽습니다.
---
# Agent Rules
## Identity
당신은 이 프로젝트의 시니어 개발자입니다. 지시를 정확히 따르고, 추측보다 근거를 우선합니다.
## NEVER (절대 금지)
1. NEVER start coding without reading relevant reference documents in `.agents/references/`
2. NEVER guess when documentation exists — always check `.agents/references/` first
3. NEVER repeat a failed approach — check `.agents/references/known-issues.md` first
4. NEVER call APIs directly when helper scripts exist in `.agents/workflows/helpers/`
5. NEVER skip the pre-task checklist defined in `.agents/workflows/pre-task.md`
6. NEVER attempt the same failed approach more than 2 times
7. NEVER truncate error messages — always show the full error output
8. NEVER say "구현 완료" or "동작 확인" without ACTUAL end-to-end test — import/문법 통과는 검증이 아님
9. NEVER confuse "코드가 논리적으로 맞음" with "실제로 동작함" — 실행 로그가 없으면 미검증
10. NEVER fix or audit code by looking at only the immediate file:
(a) Open the PRODUCER (who creates the data?) and CONSUMER (who reads/deletes?)
(b) Search for defense mechanisms (try-catch, dedup, idempotency guards)
(c) DISPROVE the bug before reporting — if a defense exists, it may be a false positive
(d) Report only bugs with a proven end-to-end triggering path
"I traced the flow" without opening actual files = violation.
11. NEVER apply changes mechanically across files — every import, variable, function must have at least one callsite in the SAME file
## ALWAYS (필수)
1. ALWAYS run `.agents/workflows/pre-task.md` before any implementation task
2. ALWAYS check `.agents/references/known-issues.md` before debugging
3. ALWAYS cite which reference document you consulted and what you learned
4. ALWAYS stop and ask the user if 2 consecutive attempts on the same approach fail
5. ALWAYS use existing helper scripts instead of raw API calls
6. ALWAYS read related existing code (minimum 3 files) before writing new code
7. ALWAYS verify with real execution after implementation — trigger the actual flow, check logs (e.g. extension.log), confirm the expected result appeared
8. ALWAYS distinguish "구현했다" vs "검증했다" when reporting to user — 테스트 안 했으면 명시
9. ALWAYS cross-reference with project history (devlog, git log -5, Vikunja) when evaluating system state — code absence may mean "intentionally removed" or "deployed externally", not "unimplemented"
## Failure Protocol
```
1st failure → Re-read reference docs → Try DIFFERENT approach
2nd failure (same issue) → STOP → Report diagnosis to user with:
- What was tried
- What failed
- Root cause hypothesis
- Suggested next steps
3rd attempt on same approach → FORBIDDEN
```
## Reference Loading Order
1. `.agents/AGENT.md` (this file — behavior rules)
2. `.agents/references/known-issues.md` (past failure patterns)
3. `.agents/references/` (project-specific knowledge)
4. `.agents/workflows/services.md` (service credentials & protocols)
5. `.agents/workflows/` (action procedures)
## Bug Report Protocol
→ See `.agents/references/bug-report-protocol.md`
## PowerShell Notes
- `curl` → PowerShell에서 `Invoke-WebRequest` 별칭. **반드시 `curl.exe`** 사용
- `npm` → 실행 정책 문제 시 `cmd /c npm` 사용
- JSON 처리 시 `.py` 스크립트 권장 (PowerShell 이스케이핑 이슈 방지)

163
.agents/GUIDE.md Normal file
View File

@@ -0,0 +1,163 @@
# AI 에이전트 워크플로우 시스템 가이드
> 이 가이드는 AI 코딩 에이전트가 더 똑똑하게 동작하도록 설계된 범용 워크플로우 시스템의 사용법을 설명합니다.
---
## 왜 이 시스템이 필요한가?
AI 에이전트는 다음과 같은 문제를 자주 일으킵니다:
| 문제 | 원인 |
|------|------|
| 📋 워크플로우를 무시함 | 규칙이 강제가 아닌 권고 사항으로만 작성됨 |
| 🔄 같은 실수를 반복함 | 과거 실패 기록을 저장/참조하는 메커니즘 없음 |
| 📖 레퍼런스 문서를 안 읽음 | "읽어라"는 강제 지시가 없고, 어떤 문서를 확인할지 불명확 |
| 🎲 추측으로 시행착오 | 작업 전 체크리스트(Pre-flight Checklist) 부재 |
이 시스템은 **13회 웹 검색**, **80+ 소스 분석**, **7개 주요 AI 플랫폼**(Claude, GPT, Gemini, Cursor, Cline, Roo, Windsurf) 연구를 기반으로 설계되었습니다.
---
## 파일 구조 개요
```
.agents/
├── AGENT.md ← 🧠 에이전트 헌법 (NEVER/ALWAYS 규칙)
├── GUIDE.md ← 📖 이 가이드
├── references/ ← 📚 프로젝트 지식 베이스
│ ├── architecture.md ← 아키텍처 설명
│ ├── tech-stack.md ← 기술 스택 & 버전
│ ├── conventions.md ← 코딩 컨벤션
│ └── known-issues.md ← 🔴 과거 실패 기록 (핵심!)
└── workflows/ ← ⚙️ 행동 절차
├── start.md ← 세션 시작 (룰 로딩 + devlog 복구)
├── end.md ← 세션 종료 (devlog + known-issues + Vikunja + Git)
├── pre-task.md ← 작업 전 필수 체크리스트
├── debug.md ← 디버깅 전용 절차
├── services.md ← 서비스 연동 정보 + AI 작업 프로토콜
├── check-gitea.md ← Gitea 현황 조회
├── check-vikunja.md ← Vikunja 태스크 조회
└── helpers/
├── vikunja_helper.py ← Vikunja API 안전 래퍼
└── wiki_helper.py ← Gitea Wiki 래퍼
```
**프로젝트 루트에 자동 생성되는 디렉토리:**
```
docs/devlog/ ← 📓 세션별 작업 기록
├── YYYY-MM-DD.md ← Index (매일 1줄씩 누적)
└── entries/
└── YYYYMMDD-NNN.md ← Entry (설계 결정/미완료 시만)
```
---
## 각 파일의 역할
### 🧠 `AGENT.md` — 에이전트 헌법
에이전트가 **모든 대화에서 따라야 하는 글로벌 규칙**입니다.
**핵심 메커니즘:**
- **NEVER 규칙**: `"절대 ~하지 마라"` — 연구에 따르면 금지 규칙이 더 잘 지켜집니다
- **Failure Protocol**: 동일 접근 2회 실패 시 자동 중단 → 유저에게 보고
- **Reference Loading Order**: 어떤 문서를 먼저 읽을지 우선순위 명시
### 📋 `pre-task.md` — 사전 점검 체크리스트
모든 구현 작업 전에 실행하는 **4단계 체크리스트**:
1. 요구사항 정리
2. 레퍼런스 확인 (추측 금지)
3. 계획 수립
4. 유저 확인
### 🔴 `known-issues.md` — 과거 실패 기록
**가장 중요한 파일.** 에이전트가 같은 실수를 반복하는 근본 원인은 **실패를 기억하지 못하기 때문**입니다. 이 파일은:
- 세션 종료 시 에이전트가 자동으로 새 이슈를 추가
- 디버깅/구현 전에 에이전트가 반드시 확인
- 시간이 지날수록 **축적 학습** 효과
### 🔧 `debug.md` — 디버깅 전용 워크플로우
**추측 기반 디버깅을 금지**하는 5단계 절차:
1. 정보 수집 (에러 전문 확인)
2. known-issues 확인
3. 근본 원인 분석 (가설 → 검증)
4. 수정 및 검증
5. 기록 (known-issues에 추가)
### 📓 Devlog — 세션별 작업 기록 (start.md / end.md에서 관리)
known-issues가 **실패만** 기록한다면, devlog는 **전체 세션 이력**을 기록합니다:
- **Index** (`docs/devlog/YYYY-MM-DD.md`): 매 작업마다 1줄 (필수)
- **Entry** (`docs/devlog/entries/YYYYMMDD-NNN.md`): 설계 결정/미완료/삽질 시만 (선택)
- **start.md**에서 자동으로 오늘/어제 devlog를 읽어 맥락 복구
### ▶️ `start.md` / ⏹️ `end.md` — 세션 관리
- **start**: 에이전트 룰 로딩 + devlog 맥락 복구 + Git 상태 + Vikunja TODO
- **end**: known-issues 업데이트 + devlog 기록 + Vikunja 동기화 + Git commit/push
---
## 사용법
### 새 프로젝트에 적용하기
1. `.agents/` 디렉토리를 프로젝트에 복사
2. `references/` 파일들을 프로젝트에 맞게 채우기:
- `architecture.md` — 프로젝트 구조 설명
- `tech-stack.md` — 사용 기술 및 버전
- `conventions.md` — 코딩 스타일 규칙
3. 프로젝트별 워크플로우가 있다면 `workflows/`에 추가
### 프로젝트별 워크플로우와 함께 사용하기
이 범용 워크플로우와 프로젝트별 워크플로우(예: Vikunja 동기화, Gitea 연동)는 **함께 사용**합니다:
```
.agents/
├── AGENT.md ← 범용 (공통)
├── references/ ← 범용 + 프로젝트 특화
│ ├── known-issues.md ← 범용 (공통)
│ └── ... ← 프로젝트에 맞게 작성
└── workflows/
├── pre-task.md ← 범용 (공통)
├── debug.md ← 범용 (공통)
├── start.md ← 범용 기반 + 프로젝트 단계 추가
├── end.md ← 범용 기반 + 프로젝트 단계 추가
├── services.md ← ⭐ 프로젝트별
├── check-vikunja.md ← ⭐ 프로젝트별
├── check-gitea.md ← ⭐ 프로젝트별
└── helpers/
├── vikunja_helper.py ← ⭐ 프로젝트별
└── wiki_helper.py ← ⭐ 프로젝트별
```
### 다른 AI IDE에서도 사용하기
| 대상 플랫폼 | 방법 |
|------------|------|
| **Cursor** | `AGENT.md``.cursor/rules/agent.mdc` (alwaysApply) |
| **Claude Code** | `AGENT.md``CLAUDE.md`, references를 `@import` |
| **Windsurf** | `AGENT.md``.windsurfrules` 또는 `.windsurf/rules/agent.md` |
| **Cline/Roo** | 루트에 `AGENTS.md`로 복사 |
| **Gemini** | `AGENT.md``.gemini/GEMINI.md` |
---
## 연구 근거 요약
이 시스템의 각 설계 결정은 학술 연구와 실무 사례에 근거합니다:
| 설계 결정 | 근거 |
|----------|------|
| NEVER > ALWAYS (금지 규칙 우선) | Community 검증 — "NEVER use X" ≫ "always prefer Y" |
| 2회 실패 시 자동 중단 | Streak Breaker / Sentinel Check 연구 |
| 실패 기록 누적 | Reflexion Framework (텍스트 피드백 기반 자기 교정) |
| 사전 체크리스트 강제 | Claude Skills 체크리스트 + GPT Chain-of-Thought |
| Progressive Disclosure | Anthropic Context Engineering (2025) |
| 300줄 이하 규칙 | Claude `CLAUDE.md` 공식 권장 (토큰 효율성) |
| 코드 예시 > 설명 | GitHub Copilot Agents, AGENTS.md 공통 Best Practice |

View File

@@ -0,0 +1,322 @@
# Architecture
> AI 에이전트는 구현 전 이 문서를 반드시 확인합니다.
## 1. 프로젝트 개요
**Gravity Control**은 Antigravity AI 코딩 에이전트와 Discord를 실시간으로 연결하는 브릿지 시스템이다.
### 핵심 목적
- AI 에이전트의 **승인 요청**(코드 실행, 파일 수정 등)을 Discord로 전달하고 사용자 응답을 반환
- AI 에이전트의 **작업 스냅샷**(대화 요약, 진행 상황)을 Discord에 실시간 표시
- **코드 리뷰**(diff review) accept/reject을 Discord에서 처리
- 사용자의 Discord **명령어**(!approve, !reject, !auto 등)를 AG Extension으로 전달
- **Auto-approve 모드**로 무인 작업 지원
### 시스템 구성
```
┌────────────────┐ WebSocket ┌──────────────┐ Discord API ┌─────────┐
│ VS Code │◄──────────────────►│ Hub Server │◄───────────────────►│ Discord │
│ AG Extension │ type:auth/chat │ (hub.py + │ discord.py bot │ 서버 │
│ (TypeScript) │ /pending/resp │ gateway.py)│ │ │
└────────────────┘ └──────────────┘ └─────────┘
↕ AG SDK (RPC) ↕
┌────────────────┐ ┌──────────────┐
│ Antigravity │ │ 파일 bridge │ ← 레거시 fallback
│ AI Engine │ │ (bridge.py) │ (WS 미사용 시)
└────────────────┘ └──────────────┘
```
---
## 2. 디렉토리 구조
```
gravity_control/
├── main.py # 진입점: Bot + Hub + Watcher 통합 시작
├── config.py # 환경변수 + .env 로드 (66줄)
├── ── 서버 측 (Python) ──
├── bot.py # Discord 봇: 승인 UI, 채널 관리, Hub 핸들러 (1,286줄)
├── hub.py # WebSocket Hub: 연결 관리, 메시지 라우팅 (580줄)
├── auth.py # JWT 토큰 + registration code 인증 (127줄)
├── gateway.py # HTTP REST API + /ws endpoint (168줄)
├── bridge.py # 파일 기반 IPC (레거시 fallback) (270줄)
├── watcher.py # Brain 디렉토리 변경 감시 (290줄)
├── parser.py # Markdown → Discord 변환 (245줄)
├── ── Extension 측 (TypeScript) ──
├── extension/src/
│ ├── extension.ts # 메인: SDK init, activate, 오케스트레이션 (650줄)
│ ├── step-probe.ts # 폴링 + 응답 처리 + 승인 전략 (1,479줄)
│ │ # setupMonitor(), processResponseFile(),
│ │ # writePendingApproval(), tryApprovalStrategies()
│ ├── http-bridge.ts # HTTP 서버 (Renderer↔Extension Host 통신) (280줄)
│ │ # startHttpBridge(), getDeterministicPort()
│ ├── html-patcher.ts # AG HTML 패치 + product.json 체크섬 (280줄)
│ │ # setupApprovalObserver(), updateProductChecksums()
│ ├── command-handler.ts # Discord→AG 명령어 처리 (175줄)
│ │ # watchCommandsDir(), handleWSCommand()
│ ├── observer-script.ts # DOM Observer 스크립트 생성 (698줄)
│ │ # generateApprovalObserverScript()
│ ├── ws-client.ts # WSBridgeClient: Hub 연결, 재연결, 큐 (505줄)
│ ├── step-utils.ts # Step 파싱 순수 함수 4개 (114줄)
│ │ # extractPlannerText, filterEphemeral,
│ │ # extractToolCommand, extractToolDescription
│ └── sdk/ # Antigravity SDK 로컬 임베드
│ ├── index.js # SDK 런타임 (4,014줄)
│ └── index.d.ts # SDK 타입 정의 (2,297줄)
├── ── 테스트 ──
├── tests/
│ ├── test_ws_hub.py # Hub WS 연결 테스트
│ └── test_syntax.py # Python 구문 검증
├── ── 문서 / 설정 ──
├── .env # 환경변수 (git 제외)
├── .agents/references/ # AI 에이전트 레퍼런스
├── docs/devlog/ # 작업 로그
└── start_bot.bat # 윈도우용 봇 시작 스크립트
```
---
## 3. 핵심 모듈 상세
### 3.1 Hub (hub.py) — WebSocket 메시지 허브
**역할**: Extension ↔ Bot 간 실시간 양방향 통신 중계
| 기능 | 설명 |
|------|------|
| 연결 관리 | 프로젝트별 다중 인스턴스, 인스턴스 번호 자동 부여 |
| JWT 인증 | registration_code → JWT 발급 → 이후 토큰 재인증 |
| 메시지 라우팅 | pending, chat, register, auto_resolve, brain_event |
| 응답 역라우팅 | request_id → pending_owners → 원본 Extension으로 전달 |
| Rate limiting | per-connection 100msg/10s |
| Dedup | msg_id 기반 60s TTL 중복 제거 |
| Heartbeat | 30s 간격 ping/pong |
**프로토콜**:
```
1. Client → Server: {type:"auth", registration_code/token, project, pc}
2. Server → Client: {type:"auth_ok", conn_id, instance_number, session_token}
3. 양방향 메시지 교환:
- Extension→Hub: pending, chat, register, auto_resolve, brain_event
- Hub→Extension: response, command, instance_update, error
```
### 3.2 Auth (auth.py) — 인증 관리
| 기능 | 설명 |
|------|------|
| Registration Code | 사전 공유 코드로 최초 인증 |
| JWT 발급 | HMAC-SHA256, 24시간 유효 |
| 토큰 검증 | 만료/위조 감지, 프로젝트+PC 메타데이터 포함 |
### 3.3 Bot (bot.py) — Discord 인터페이스
| 기능 | 설명 |
|------|------|
| 승인 UI | Approve/Reject 버튼, diff_review Accept/Reject |
| Auto-approve | `!auto` 토글, 세션 간 초기화 |
| 채널 관리 | `#ag-{project}` 자동 채널 매칭 |
| 스냅샷 전달 | 2000자 초과 시 파일 첨부 |
| 명령어 | !approve, !reject, !auto, !status, !send |
| Hub 연동 | on_hub_pending, on_hub_chat, on_hub_register 핸들러 |
| IDLE 알림 | AI step 종료 시 Discord 알림 |
### 3.4 Extension (extension.ts) — VS Code 확장 (오케스트레이터)
| 기능 | 설명 |
|------|------|
| AG SDK 연동 | getCascadeStatus, getAllTrajectories RPC |
| 세션 감지 | activeSessionId 자동 추적 |
| 프로젝트 자동 감지 | git remote URL 기반 |
| 모듈 초기화 | HTTP bridge, observer, command handler 시작 |
| WS bridge | WSBridgeClient 통한 Hub 연결 (우선) |
| Status bar | SDK 상태 + 연결 상태 표시 |
### 3.4a HTTP Bridge (http-bridge.ts)
`HttpBridgeContext` 인터페이스로 extension.ts의 공유 상태 참조:
| 기능 | 설명 |
|------|------|
| POST /pending | Renderer가 발견한 승인 버튼 보고 |
| GET /response/:rid | Renderer가 Discord 응답 폴링 |
| GET /trigger-click | Extension→Renderer 클릭 트리거 |
| GET/POST /deep-inspect* | DOM 심층 검사 |
| getDeterministicPort | 프로젝트명 기반 결정적 포트 |
### 3.4b HTML Patcher (html-patcher.ts)
| 기능 | 설명 |
|------|------|
| setupApprovalObserver | AG Workbench HTML 파일에 observer 스크립트 인라인 삽입 |
| updateProductChecksums | product.json SHA256 체크섬 업데이트 (vscode-file:// 프로토콜용) |
| CSP 패치 | script-src에 'unsafe-inline' 추가 |
| .orig 백업 | 최초 패치 전 원본 백업, 손상 시 자동 복구 |
### 3.4c Command Handler (command-handler.ts)
`CommandHandlerContext` 인터페이스로 extension.ts 상태 참조:
| 기능 | 설명 |
|------|------|
| watchCommandsDir | commands/ 디렉토리 fs.watch + 3s 폴링 |
| handleWSCommand | WS Hub 경유 명령어 처리 |
| !stop, !auto | AG 에이전트 제어 명령어 |
| 텍스트 전달 | Discord → AG `sendPromptToAgentPanel` |
### 3.5 Step Probe (step-probe.ts) — 상태 폴링
`BridgeContext` 인터페이스로 extension.ts와 상태 공유:
| 기능 | 설명 |
|------|------|
| setupMonitor | 3초 간격 SDK 폴링, 세션/step 변화 감지 |
| processResponseFile | Discord 응답 → AG RPC 실행 |
| writePendingApproval | 승인 요청 파일/WS 전송 |
| tryApprovalStrategies | 다단계 승인: DOM click → VS Code command → RPC |
| setupResponseWatcher | response/ 디렉토리 파일 감시 |
**BridgeContext 필드** (14개):
`bridgePath`, `projectName`, `sdk`, `wsBridge`, `logToFile`, `autoApproveEnabled`, `activeSessionId`, `setClickTrigger`, `recentDiscordSentTexts`, `writeChatSnapshot`, `writeChatSnapshotWithFiles`, `workspaceUri`, `diffReviewMetadata`, `sessionStalled`, `lastPendingStepIndex`, `stallProbed`, `sawRunningAfterPending`
### 3.6 WS Client (ws-client.ts) — Hub 클라이언트
| 기능 | 설명 |
|------|------|
| 연결 관리 | WebSocket + 자동 재연결 |
| Backoff | 1s→60s 지수 백오프 + ±30% jitter |
| 메시지 큐 | 200개 버퍼, 재연결 시 자동 flush |
| Heartbeat | 25s 간격 ping |
| 인증 | registration_code 또는 session_token |
| API | sendPending, sendChat, sendRegister, sendAutoResolve |
### 3.7 Observer Script (observer-script.ts)
AG Webview의 DOM을 관찰하여 승인 버튼을 자동 감지/클릭:
- MutationObserver로 `.actions-container` 감시
- 버튼 텍스트 매칭으로 Approve/Reject 자동 실행
- `postMessage`로 Extension과 통신
### 3.8 Step Utils (step-utils.ts)
순수 함수 4개:
- `extractPlannerText(content)` — AI 응답 텍스트 추출
- `filterEphemeral(text)` — 시스템 메시지 필터링
- `extractToolCommand(content)` — 도구 명령어 추출
- `extractToolDescription(content)` — 도구 설명 추출
---
## 4. 데이터 흐름
### 4.1 승인 요청 플로우
```
AG Engine → SDK RPC → Extension(step-probe.ts)
→ setupMonitor: WAITING step 감지
→ writePendingApproval: pending 데이터 생성
→ [WS] wsBridge.sendPending() → Hub → Bot → Discord (버튼 UI)
→ [파일] bridge/pending/{id}.json (fallback)
```
### 4.2 승인 응답 플로우
```
Discord (사용자 버튼 클릭) → Bot
→ [Hub connected] Hub.route_response() → WS → Extension
→ [File fallback] bridge/response/{id}.json → setupResponseWatcher
→ processResponseFile → tryApprovalStrategies
1차: DOM observer script (webview inject)
2차: VS Code command (cascade.approveCurrentStep)
3차: Direct RPC (acknowledgeCodeActionStep)
```
### 4.3 채팅 스냅샷 플로우
```
Extension(step-probe.ts) → 새 step 텍스트 감지
→ writeChatSnapshot(text) → truncation + dedup
→ [WS] wsBridge.sendChat() → Hub → Bot → Discord (#ag-{project})
→ [파일] bridge/pending/ snapshot 파일 (fallback)
```
### 4.4 Diff Review 플로우
```
AG Engine → 파일 수정 → Extension(step-probe.ts)
→ edit_step_indices + modified_files 메타데이터 수집
→ writePendingApproval (step_type="diff_review", 8초 지연)
→ Discord (Accept all / Reject all 버튼)
→ 응답 → handleDiffReviewResponse()
→ openReviewChanges → 파일별 포커스 → agentAcceptAllInFile VS Code 커맨드
```
> [!IMPORTANT]
> diff_review는 **VS Code 커맨드만 유효**합니다. RPC 방식(AcknowledgeCascadeCodeEdit 등)은 모두 실패 확정.
> 상세 경위는 known-issues-archive.md의 "Diff Review 관련" 섹션을 참조하세요.
---
## 5. 통신 프로토콜
### 5.1 WebSocket 메시지 타입
**Extension → Hub (upstream)**:
| type | data 필드 | 설명 |
|------|-----------|------|
| `auth` | registration_code/token, project, pc | 최초 인증 |
| `pending` | request_id, command, description, buttons | 승인 요청 |
| `chat` | content, attached_files, conversation_id | 채팅 스냅샷 |
| `register` | conversation_id, project_name | 세션 등록 |
| `auto_resolve` | request_id | 자동 해결 알림 |
| `brain_event` | (payload) | 브레인 이벤트 |
| `heartbeat` | - | 연결 유지 |
**Hub → Extension (downstream)**:
| type | data 필드 | 설명 |
|------|-----------|------|
| `auth_ok` | conn_id, instance_number, session_token | 인증 성공 |
| `auth_fail` | reason | 인증 실패 |
| `response` | request_id, approved, button_index | 승인 응답 |
| `command` | text, action | Discord 명령어 |
| `instance_update` | active_count, instances[] | 인스턴스 변경 |
| `error` | error | 에러 |
### 5.2 BOT_MODE 동작 차이
| 모드 | Watcher | Hub/Gateway | 용도 |
|------|---------|-------------|------|
| `local` | ✅ (brain 감시) | ❌ | 로컬 개발 (Extension과 같은 PC) |
| `gateway` | ❌ | ✅ (port 8585) | 서버 배포 (WS Hub + Gateway) |
| `remote` | ✅ | ❌ | ⚠️ **DEPRECATED** — 레거시 Collector (WS Hub 사용 권장) |
---
## 6. 보안
| 항목 | 구현 |
|------|------|
| WS 인증 | Registration Code → JWT (HMAC-SHA256, 24h) |
| Gateway API | API Key 헤더 (`X-API-Key`) |
| Rate limit | per-connection 100msg/10s |
| 메시지 dedup | msg_id 기반 60s TTL |
| Discord | Bot 토큰 + Guild ID 제한 |
---
## 7. Extension 설정 (VS Code)
| 설정 키 | 설명 | 기본값 |
|---------|------|--------|
| `gravityBridge.bridgePath` | Bridge 디렉토리 경로 | `~/.gemini/antigravity/bridge` |
| `gravityBridge.projectName` | 프로젝트 이름 | git remote 자동 감지 |
| `gravityBridge.hubUrl` | WebSocket Hub URL | (비어있으면 WS 비활성) |
| `gravityBridge.registrationCode` | Hub 등록 코드 | (서버에서 발급) |

View File

@@ -0,0 +1,41 @@
# Bug Report Protocol
> 이 프로토콜은 코드 감사 또는 디버깅 시 false positive를 방지하기 위한 6단계 검증 절차입니다.
> AGENT.md 규칙 #10의 세부 구현입니다.
## 절차
```
1. Identify: 로컬 코드에서 잠재적 이슈 식별
2. Trace: 해당 데이터의 전체 생명주기 추적
- Producer (생성자) 파일을 열어 확인
- Transport (전달 경로: file, HTTP, RPC) 확인
- Consumer (소비자) 파일을 열어 확인
3. Defend: 기존 방어 메커니즘 검색
- try-catch, idempotency guard, dedup logic
- upstream validation, downstream tolerance
4. Disprove: 버그가 아닌 이유를 적극적으로 찾기
- "이 코드가 안전한 이유는?"
- 방어 메커니즘이 존재하면 → false positive 가능성 높음
5. Prove: 여전히 버그라면 트리거 경로를 증명
- 구체적 입력 → 구체적 경로 → 구체적 실패
- "A가 B를 호출하면 C에서 D가 발생" 형태
6. Report: 증명된 버그만 보고
- 트리거 경로 + 심각도 + 영향 범위 포함
- 증명 못 한 것은 보고하지 않음
```
## 결정 기준
| 상황 | 판정 |
|------|------|
| 방어 메커니즘 존재 + 트리거 경로 없음 | ❌ 보고 안 함 |
| 방어 메커니즘 없음 + 트리거 경로 증명됨 | ✅ 보고 |
| 방어 메커니즘 존재 + 우회 경로 증명됨 | ✅ 보고 (우회 경로 명시) |
| 잘 모르겠음 | 🔍 추가 조사 후 판단 (추측으로 보고 금지) |
## 근거
- Anthropic Code Review: "verification step attempts to disprove each finding"
- LLM Self-Verification: 자기 결과를 검증하지 않으면 noise와 과신 리포트 양산
- Systems Thinking: 개별 컴포넌트가 아닌 관계와 상호의존성에 집중

View File

@@ -0,0 +1,125 @@
# Coding Conventions
> AI 에이전트는 코드를 작성하기 전 이 컨벤션을 확인합니다.
## 네이밍
### Python (서버)
| 대상 | 규칙 | 예시 |
|------|------|------|
| 변수/함수 | snake_case | `write_pending_approval()` |
| 클래스 | PascalCase | `GravityBot`, `WSHub`, `TokenManager` |
| 상수 | UPPER_SNAKE_CASE | `MAX_MSG_SIZE`, `HEARTBEAT_INTERVAL` |
| 파일명 | snake_case | `hub.py`, `ws_client.py` |
| 로거명 | 모듈명 | `logging.getLogger("hub")` |
### TypeScript (Extension)
| 대상 | 규칙 | 예시 |
|------|------|------|
| 변수/함수 | camelCase | `writePendingApproval()`, `setupMonitor()` |
| 클래스 | PascalCase | `WSBridgeClient` |
| 인터페이스 | PascalCase | `BridgeContext`, `WSPendingData` |
| 상수 | UPPER_SNAKE_CASE | `MAX_QUEUE_SIZE`, `AUTH_TIMEOUT` |
| 파일명 | kebab-case | `ws-client.ts`, `step-probe.ts` |
| export 함수 | camelCase | `initStepProbe()`, `generateApprovalObserverScript()` |
## 코드 스타일
| 항목 | Python | TypeScript |
|------|--------|-----------|
| 들여쓰기 | 4 spaces | 4 spaces |
| 따옴표 | 쌍따옴표 `"` (f-string 포함) | 작은따옴표 `'` |
| 세미콜론 | N/A | 사용 |
| 줄바꿈 | LF (Unix) | CRLF (Windows, git 자동 변환) |
| 최대 줄 길이 | 120자 권장 | 120자 권장 |
| 타입 힌트 | 적극 사용 (`-> list[str]`) | strict (`BridgeContext` 인터페이스) |
## 커밋 메시지
```
<type>(<scope>): <description>
type: feat|fix|refactor|test|docs|chore|ci|infra
scope: server|extension|hub|bot|gateway|bridge (선택)
```
**예시:**
- `feat(hub): WebSocket Hub 구현 + JWT 인증`
- `refactor(extension): 모듈 분리 (step-probe, observer-script)`
- `fix(bot): auto-approve 세션 간 초기화`
- `docs: architecture.md 전면 재작성`
관련 Vikunja 태스크가 있으면: `feat(hub): WS Hub 구현 #task-395`
## 주석
- 한국어/영어 혼용 가능
- TODO 주석: `// TODO: 설명` 형식
- 섹션 구분: `// ─── Section Name ───` (TypeScript), `# ─── Section ───` (Python)
- 복잡한 로직에는 반드시 WHY(왜) 주석 추가
- 함수 docstring: Python은 `"""..."""`, TypeScript는 `/** ... */`
## 모듈 분리 패턴
Extension 모듈 분리 시 사용하는 패턴:
| 패턴 | 용도 | 예시 |
|------|------|------|
| **순수 함수 추출** | 외부 상태 참조 없는 함수 | `step-utils.ts` |
| **독립 스크립트** | 문자열 반환 함수 | `observer-script.ts` |
| **Context 패턴** | 공유 상태가 많은 함수 그룹 | `step-probe.ts` (BridgeContext) |
| **클래스 추출** | 자체 상태 + 메서드 | `ws-client.ts` (WSBridgeClient) |
## 테스트
| 항목 | 위치 | 도구 |
|------|------|------|
| Python 구문 검사 | `tests/test_syntax.py` | `ast.parse` |
| WS Hub 연결 | `tests/test_ws_hub.py` | `websockets` |
| TypeScript 컴파일 | `npx tsc --noEmit` | TypeScript compiler |
| E2E | 수동 (Discord 버튼 클릭) | — |
## 로깅
| 측 | 방식 | 포맷 |
|----|------|------|
| Python | `logging.getLogger(name)` | `YYYY-MM-DD HH:MM:SS [name] LEVEL: message` |
| Extension | `logToFile(msg)` → bridge/log/ | `[HH:MM:SS] message` + `[WS]` prefix |
| Hub | `[HUB]` prefix | `[HUB] Auth OK: {conn_id} project={project}` |
| Gateway | `[GATEWAY]` prefix | `[GATEWAY] HTTP API started on {host}:{port}` |
## WS / File Bridge 상호 배타 패턴
> [!IMPORTANT]
> WS Hub과 파일 bridge는 **항상 상호 배타적**이어야 합니다.
> 양쪽에 동시에 쓰면 이중 전달 버그가 발생합니다. (known-issues-archive 참조)
**Extension (TypeScript):**
```typescript
// ✅ 올바른 패턴
if (ctx.wsBridge?.isConnected()) {
ctx.wsBridge.sendPending(data);
return; // ← 반드시 return으로 파일 쓰기 건너뛰기
}
// File fallback
fs.writeFileSync(pendingPath, JSON.stringify(data));
```
**Bot (Python):**
```python
# ✅ 올바른 패턴
if self.hub:
await self.hub.send_response(...)
else:
bridge.write_response(...)
```
**금지 패턴:**
```python
# ❌ 이중 쓰기 — 절대 금지
if self.hub:
await self.hub.send_response(...)
bridge.write_response(...) # ← Hub 성공해도 파일에도 씀 → 이중 처리
```

View File

@@ -0,0 +1,581 @@
# Known Issues — Archive
> **해결 완료된 이슈 아카이브입니다.**
> 현재 활성 이슈는 [`known-issues.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues.md)를 참조하세요.
> 비슷한 문제가 재발할 때 이 문서에서 과거 해결 방법을 검색하세요.
---
## 포맷
```markdown
### [날짜] [키워드] — 한줄 요약
- **증상**: 무엇이 잘못되었는가
- **원인**: 근본 원인
- **해결**: 올바른 해결 방법
- **주의**: 재발 방지를 위한 교훈
```
---
## 승인 전략 결정 체인 (2026-03-08~09 전체 요약)
> **이 섹션은 2026-03-08~09 전체 세션의 시행착오를 요약합니다.**
> **이미 거부된 접근을 다시 시도하지 마세요.**
### ❌ 시도 후 거부된 접근 (재시도 금지)
| # | 접근 | 결과 | 거부 사유 |
|---|------|------|-----------|
| 1 | LS RPC `HandleCascadeUserInteraction` | `socket hang up` | AG 빌드에서 핸들러 미구현 |
| 2 | LS RPC `ResolveOutstandingSteps` | CANCEL 동작 (승인이 아님) | 명칭과 달리 step을 취소함 |
| 3 | VS Code 7개 승인 명령 (`terminalCommand.run` 등) | `command not found` | AG 런타임에 미등록 |
| 4 | 키보드 시뮬레이션 (`type {Enter}`) | 빈 메시지 전송 | Chat input에 캡처됨 |
| 5 | `sendChatActionMessage` / `executeCascadeAction` | 미등록 | 119개 명령에 없음 |
| 6 | pywinauto (OS 레벨) | 사용자 결정 폐기 | 크로스플랫폼 불가, 창 겹침 문제 |
| 7 | CDP (Chrome DevTools Protocol) | **사용자 명시적 거부** | 비표준, `--remote-debugging-port` 필요 |
| 8 | Renderer v1 DOM Click (flat scan) | Run 버튼 미발견 | webview iframe 격리 |
### 🔑 핵심 전제
- Run/Accept 버튼은 **`vscode-webview://` origin의 격리된 iframe** 안에 있음
- 외부 workbench DOM에서 `querySelector`로 접근 **불가** (cross-origin)
- `<webview>.executeJavaScript()`**유일한 표준 관통 경로** (Electron API)
- AG 패치 후 **반드시 풀 프로세스 재시작** 필요 (Reload Window 불충분)
- **양쪽 HTML (workbench.html + workbench-jetski-agent.html) 모두 inline 패치** 필수
- HTML 패치 변경 시 **`%APPDATA%\Antigravity\CachedData` 삭제 필수** (V8 바이트코드 캐시 무효화)
- **CSP `script-src``'unsafe-inline'` 패치 필수** (없으면 인라인 스크립트 무조건 차단)
---
## Renderer / HTML 패치 관련 (2026-03-08~09)
### [2026-03-08] Antigravity Renderer Injection — Electron 캐시 차단
- **증상**: workbench.html, workbench-jetski-agent.html에 `<script>` 태그 추가 후 리로드해도 실행되지 않음
- **원인**: Electron의 V8 코드 캐시가 수정된 HTML을 무시하고 캐시된 버전을 서빙
- **해결**: 렌더러 인젝션 방식 **포기**. Extension Host에서 RPC 폴링 방식으로 전환
- **주의**: Antigravity는 `workbench-jetski-agent.html`을 사용 (Jetski = 내부 코드네임)
### [2026-03-08] Antigravity 승인 대기 = RUNNING (NOT IDLE)
- **증상**: IDLE 기반 승인 감지가 실제 승인 대기를 놓침
- **원인**: 승인 대기 시 세션 상태가 `CASCADE_RUN_STATUS_RUNNING` (IDLE 아님), `IDLE`은 대화 대기(notify_user 후)
- **해결**: `RUNNING + delta=0` (stall) 기반 감지로 전환. 6 polls (30초) 이상 FROZEN 시 pending 생성
- **주의**: Thinking/생성 중에도 `RUNNING + delta=0`이 발생 → `lastModifiedTime`으로 구분 시도했으나 불완전
### [2026-03-08] ResolveOutstandingSteps RPC — 승인이 아닌 취소!
- **증상**: Discord 승인 → `ResolveOutstandingSteps` 호출 → step이 취소됨
- **원인**: `ResolveOutstandingSteps`는 blocking steps를 "resolve" = **REJECT/CANCEL**, approve가 아님
- **해결**: `ResolveOutstandingSteps` 제거. `HandleCascadeUserInteraction``socket hang up`
- **주의**: KI에 "more reliable"로 기록되어 있으나 실제 동작은 cancel임. KI 업데이트 필요
### [2026-03-08] VS Code Accept Commands — Silent Success 문제
- **증상**: 4개 accept command 모두 OK(undefined) 반환하나 실제 승인 안 됨
- **원인**: webview에 활성 포커스가 필요. `panel.focus()`로는 충분하지 않음
- **해결**: **미해결**. Windows UI Automation 등 OS 레벨 접근 필요
- **주의**: reject commands는 동작함. accept만 focus 의존성 있음
### [2026-03-08] Multi-Window 세션 등록 경쟁 조건
- **증상**: 이 창(gravity_control)의 대화가 `#ag-variet_agent` 채널로 메시지 전달
- **원인**: `writeRegistration()`이 폴링 루프에서 호출 → 먼저 폴링한 확장이 세션을 자기 프로젝트로 등록
- **해결**: `writeRegistration`을 폴링에서 제거, `writeChatSnapshot`/`writePendingApproval`에서만 지연 호출
- **주의**: `GetAllCascadeTrajectories`는 모든 창의 세션을 반환하므로 세션→창 매핑은 불가능. 활동 기반 등록만 신뢰 가능
### [2026-03-08] 공유 렌더러 스크립트 파일 덮어쓰기 문제
- **증상**: DOM Observer 렌더러 스크립트가 잘못된 HTTP bridge 포트에 연결
- **원인**: 두 확장이 동일한 `ag-sdk-variet-gravity-bridge.js` 파일에 각자 포트를 씀 → 마지막 확장 것만 남음
- **해결**: `ag-bridge-ports.json`에 모든 확장의 port를 JSON으로 기록, 렌더러가 all ports를 순회하며 ping
- **주의**: 렌더러 스크립트 파일 경로는 SDK patcher namespace에 의해 고정 — 변경 불가
### [2026-03-08] workbench.html vs workbench-jetski-agent.html
- **증상**: 렌더러에서 `[GB Observer]` 로그가 전혀 안 나옴
- **원인**: DevTools가 `workbench.html`을 로드 — 스크립트 태그는 `workbench-jetski-agent.html`에만 패치됨
- **해결**: `workbench.html`에도 스크립트 태그 필요. Antigravity 재설치 후 SDK patcher가 올바르게 패치하도록 함
- **주의**: SDK patcher는 `both` HTML 파일을 패치하지만, 수동 수정은 Antigravity integrity check에 의해 되돌려질 수 있음
### [2026-03-08] product.json 체크섬 불일치 → 렌더러 스크립트 미로딩
- **증상**: `<script>` 태그가 HTML에 존재하고 .js 파일도 디스크에 있으나, 렌더러 콘솔에 스크립트 로그가 전혀 없음
- **원인**: Antigravity 재설치 시 `product.json`의 SHA256 체크섬이 원본으로 리셋됨. Extension이 HTML을 패치하지만 `IntegrityManager.suppressCheck()`를 호출하지 않아 체크섬 불일치. `vscode-file://` 프로토콜이 체크섬 불일치 파일을 무시하고 **원본 캐시 HTML**을 서빙
- **해결**: `product.json``checksums` 항목에서 수정된 파일(workbench.html, workbench-jetski-agent.html)의 SHA256 해시를 실제 파일 기준으로 업데이트. SDK `IntegrityManager.suppressCheck()` 호출 또는 수동 스크립트로 해결
- **주의**: Extension `setupApprovalObserver()``suppressCheck()` 호출을 영구 추가해야 재설치마다 반복 안 됨. 해시 = `base64(sha256(file)).replace(/=+$/, '')`
### [2026-03-08] vscode-file:// 프로토콜 — 커스텀 .js 파일 서빙 불가
- **증상**: `<script src="./ag-sdk-variet-gravity-bridge.js">` 태그가 HTML에 있으나 `net::ERR_FILE_NOT_FOUND` 발생, GB Observer 로그 전혀 없음
- **원인**: `vscode-file://` 프로토콜은 원본 배포에 포함된 파일만 서빙. Extension이 디스크에 쓴 커스텀 `.js` 파일은 프로토콜 레벨에서 차단됨
- **해결**: 외부 `<script src>` 참조 대신 **인라인 `<script>...코드...</script>`** 방식으로 HTML에 직접 삽입
- **주의**: `ag-bridge-ports.json`도 같은 이유로 XHR 로딩 불가. 모든 렌더러 스크립트/데이터는 HTML 인라인으로 전달해야 함
### [2026-03-08] Renderer 포트 디스커버리 — ag-bridge-ports.json XHR 실패
- **증상**: `[GB Observer] Port discovery timeout after 2min` — 렌더러가 bridge 포트를 찾지 못함
- **원인**: 렌더러 스크립트가 `./ag-bridge-ports.json`을 동기 XHR로 읽으려 하나, `vscode-file://` 프로토콜이 `.json` 파일 서빙 거부
- **해결**: (1) 프로젝트명 해시 기반 **결정론적 포트** 사용 (`gravity_control→34332`), (2) 스크립트 생성 시 포트를 `HARDCODED_PORT=${port}`로 직접 삽입
- **주의**: `server.listen(0)` 랜덤 포트 → 매 재시작마다 변경되어 렌더러와 불일치. 결정론적 포트는 `EADDRINUSE` 시 랜덤 폴백 필요
### [2026-03-08] GetCascadeTrajectorySteps — cascadeId 파라미터 발견
- **증상**: `GetCascadeTrajectorySteps``trajectoryId` 파라미터 → 500 "trajectory not found"
- **원인**: 파라미터명이 `trajectoryId`가 아니라 **`cascadeId`**. 값은 `GetAllCascadeTrajectories.trajectorySummaries`의 맵 키(세션 ID)
- **해결**: `{ cascadeId: sessionId }`로 호출 → 전체 step 배열 반환 성공
- **주의**: `latestToolCallStep` 필드는 `GetAllCascadeTrajectories` 응답에 **존재하지 않음** (KI 오류)
### [2026-03-08] Step 구조 — CORTEX_STEP_STATUS_WAITING 즉시 감지
- **증상**: stall-based 감지(100초)가 너무 느림
- **원인**: 이제 `GetCascadeTrajectorySteps`로 최신 step의 status를 직접 확인 가능
- **해결**: stall 5초 후 step probe → `CORTEX_STEP_STATUS_WAITING` 확인 → 즉시 pending 생성
- **Step 구조**: `{type: "CORTEX_STEP_TYPE_RUN_COMMAND", status: "CORTEX_STEP_STATUS_WAITING", metadata: {toolCall: {name, argumentsJson}}, runCommand, requestedInteraction}`
- **주의**: 775-step 하드 리밋은 여전히 존재. 긴 세션에서는 fallback(40초) 사용
### [2026-03-08] Extension 재설치 안전성 — 자동 패치 메커니즘
- **증상**: Antigravity 삭제 후 재설치 시 렌더러 스크립트가 동작 안 함
- **원인**: 재설치 시 HTML/product.json이 원본으로 리셋됨
- **해결**: Extension의 `setupApprovalObserver()`**자동으로** 모든 패치를 수행
- **주의**: 패치 후 반드시 **Antigravity 풀 재시작** 필요 (Reload Window 불가)
### [2026-03-08] Response 파일 Race Condition — DOM Observer 승인 실패
- **증상**: Discord에서 승인 → `[RESPONSE] renderer-handled approval` 로그 출력 → 실제 버튼 클릭 안 됨
- **원인**: `processResponseFile` (파일 감시자)이 response 파일을 즉시 삭제 → renderer의 `pollResponse`가 HTTP `GET /response/:rid`로 조회 시 파일 이미 없음
- **해결**: DOM observer 소스일 때는 response 파일을 삭제하지 않도록 수정. HTTP endpoint가 renderer에게 서빙한 후 삭제
- **주의**: non-DOM (stall/step_probe relay)는 watcher에서 삭제해도 됨
### [2026-03-08] Renderer 스크립트 소스 혼동 — 3곳의 코드
- **증상**: `extension.ts`에 BTN-DUMP 추가 → Reload 2번 → 콘솔에 안 나옴
- **원인**: renderer 코드가 **3곳**에 존재: (1) `extension.ts``generateApprovalObserverScript()` (소스), (2) `ag-sdk-variet-gravity-bridge.js` (배포됨, Reload시 소스에서 재생성), (3) `workbench-jetski-agent.html` inline (HTML, JS파일과 중복로드 방지됨)
- **해결**: 항상 `extension.ts``generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
- **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트
---
## 승인 / RPC 관련 (2026-03-09)
### [2026-03-09] VS Code Accept — SDK 승인 명령이 AG에 미등록
- **증상**: Discord 승인 → `antigravity.terminalCommand.run` 등 7개 명령 → 모두 `command not found`
- **원인**: SDK(command-bridge.ts)에 정의된 7개 승인 명령이 현재 AG 빌드에 **등록되어 있지 않음**
- **해결**: Renderer DOM Click 구현 → v3 `deepFindButtons()` 업그레이드
- **주의**: `agentPanel.focus`도 미등록, `agentSidePanel.focus`만 존재
### [2026-03-09] Renderer DOM — webview iframe 격리 확인 + v3 deep traversal
- **증상**: Renderer trigger-click이 `document.querySelectorAll('button')`으로 버튼 검색 → Run 버튼 미발견
- **원인**: Run/Accept 버튼은 AG 채팅 webview iframe (`vscode-webview://` origin) 안에 렌더링
- **해결**: Renderer v3 `deepFindButtons()` 구현 (iframe contentDocument + webview.executeJavaScript + shadow DOM)
- **주의**: CDP(Chrome DevTools Protocol)는 **사용자 결정에 의해 명시적으로 거부됨**
### [2026-03-09] Deep Inspect HTTP Endpoint — curl로 DOM 분석 트리거
- **증상**: AG DevTools 콘솔에 붙여넣기 불가 (Chromium 보안 정책)
- **원인**: Electron 렌더러 DevTools에서 `allow pasting`이 SyntaxError 발생
- **해결**: Extension HTTP bridge에 `/deep-inspect` 엔드포인트 추가
- **주의**: 시작 3초 후 자동 실행 + curl로 재트리거 가능
### [2026-03-09] workbench.html inline 스크립트 미삽입 — jetski만 패치한 버그
- **증상**: AG 재시작 후 `/deep-inspect` timeout — renderer v3 스크립트 미로딩
- **원인**: `setupApprovalObserver()``workbench-jetski-agent.html`에만 inline 삽입
- **해결**: HTML 패치 로직을 **양쪽 모두 inline** 삽입으로 변경
- **주의**: **항상 양쪽 HTML을 동일하게 패치**
### [2026-03-09] V8 CachedData — 체크섬 정상이어도 스크립트 미실행
- **증상**: HTML 패치 + product.json 체크섬 일치 → 그런데도 renderer 스크립트 미실행
- **원인**: V8 바이트코드 캐시가 `vscode-file://` 프로토콜에도 적용
- **해결**: `%APPDATA%\Antigravity\CachedData\*` 전체 삭제 후 AG 풀 재시작
- **주의**: **HTML 패치 변경 시마다 CachedData 삭제 필수**
### [2026-03-09] CSP script-src — 인라인 스크립트 무조건 차단
- **증상**: HTML 패치 ✅, 체크섬 ✅, CachedData 삭제 ✅ — 그런데도 renderer 미실행
- **원인**: CSP `script-src``'unsafe-inline'` 없음
- **해결**: CSP `script-src``'unsafe-inline'` 추가
- **주의**: `style-src`에는 `'unsafe-inline'`이 있어 스타일은 동작 → 스크립트만 차단되는 것이 함정
### [2026-03-09] 중복 승인 요청 — DOM scan + Step probe 동시 발동
- **증상**: Discord에 같은 명령에 대해 승인 요청이 2개 도착
- **원인**: DOM Observer + Step probe가 동일 step에 대해 2개 파일 생성
- **해결**: `writePendingApproval()`에 15초 dedup 윈도우 추가
- **주의**: DOM scan은 제거 불가 — step probe가 감지 못하는 UI 버튼 존재
### [2026-03-09] Step probe reject → ResolveOutstandingSteps가 AI 작업 취소
- **증상**: Discord에서 거부 클릭 → AI의 현재 step뿐 아니라 진행 중인 작업 전체가 중단
- **원인**: step probe 경로의 `tryApprovalStrategies(approved=false)``ResolveOutstandingSteps` RPC 호출
- **해결**: step probe 경로에서 reject 시 `tryApprovalStrategies` 호출 제거
- **주의**: `ResolveOutstandingSteps`는 이름과 달리 "해결"이 아닌 "취소". 승인에 **절대 사용 금지**
### [2026-03-09] Pending 파일 무한 누적 — write_response 후 미삭제
- **증상**: `bridge/pending/` 디렉토리에 79개 이상의 .json 파일 누적
- **원인**: `write_response()`가 pending 파일을 삭제하지 않음
- **해결**: pending 파일 삭제 + 5분 age filter + 시작 시 cleanup
- **주의**: 봇 재시작 시 자동 정리
### [2026-03-09] Discord 승인 "Run" 표시 — DOM/step_probe 타이밍 불일치
- **증상**: Discord에 상세 명령어 대신 "Run"만 표시
- **원인**: DOM observer가 "Run" pending 생성 → 봇이 3초 후 전송 → step_probe MERGE가 10초 후 완료
- **해결**: step_probe가 기존 DOM pending에 MERGE + 봇에서 짧은 명령어 대기
- **주의**: MERGE 타이밍은 최소 10초
### [2026-03-09] DOM observer false positive — Proceed/Continue/Open 버튼 오감지
- **증상**: 작업 전환 시 승인 요청 없는데도 Discord에 승인 요청 도착
- **원인**: DOM observer가 AG UI의 PathsToReview 버튼을 승인 버튼으로 오인
- **해결**: FALSE_POSITIVE_RE 필터 추가 + sessionStalled 조건
- **주의**: 렌더러 인라인 스크립트는 VSIX 빌드 필요
### [2026-03-09] Discord ApprovalView timeout — 5분 후 버튼 무응답
- **증상**: 시간이 지난 후 Discord 승인 버튼 클릭해도 반응 없음
- **원인**: Discord.py View의 기본 timeout이 300초
- **해결**: timeout을 1800초로 증가
- **주의**: Discord View timeout은 서버 재시작 후만 적용
---
## 멀티 프로젝트 / Pending 관련 (2026-03-10)
### [2026-03-10] DOM Observer 승인 ENOENT — response 파일 Race Condition
- **증상**: Discord에서 ✅승인 → `ENOENT: response/xxx.json` 에러
- **원인**: `processResponseFile()`이 response 파일 즉시 삭제 → renderer 조회 실패
- **해결**: DOM observer 경로에서 response 파일을 삭제하지 않고 HTTP handler가 서빙 후 삭제
- **주의**: DOM observer와 step_probe 두 경로가 독립적
### [2026-03-10] Allow Once + Allow This Conversation — 별개 pending으로 분리되는 문제
- **증상**: 파일 접근 시 Discord에 2개 별도 메시지로 도착
- **원인**: renderer `scan()`이 한 사이클에 한 버튼만 처리
- **해결**: `findButtonContainer()` + `collectSiblingButtons()`로 그룹화, `buttons` 배열 전송
- **주의**: `buttons` 배열이 없는 legacy pending은 기존 2버튼(✅승인/❌거부)으로 표시
### [2026-03-10] step_probe verbosity — argumentsJson 미포함
- **증상**: Discord 승인 메시지에 파라미터 이름만 표시, 값 없음
- **원인**: `GetCascadeTrajectorySteps` 기본 verbosity에서 `argumentsJson` 빈 문자열
- **해결**: `verbosity: 1` (DEBUG) 추가
- **주의**: verbosity 0=NORMAL (키만), 1=DEBUG (값 포함)
### [2026-03-10] 파일 권한 응답 — "unexpected user interaction type: not file permission"
- **증상**: `.agents` 디렉토리 접근 시 에러
- **원인**: 봇이 file_permission pending에 잘못된 interaction type 전송
- **해결**: step_type별 올바른 RPC 라우팅
- **주의**: file_permission과 run_command가 동시에 대기 시 올바른 RPC 라우팅 필수
### [2026-03-10] active_project.lock — 멀티 프로젝트 동시 사용 차단
- **증상**: 여러 AG 프로젝트 실행 시 첫 번째만 bridge 연결
- **원인**: `active_project.lock` 파일이 단일 프로젝트만 허용
- **해결**: lock 메커니즘 완전 제거, `project_name` 필터 기반으로 전환
- **주의**: bridge 격리는 `project_name` 필드 기반 filtering으로 충분
### [2026-03-10] step_probe file_permission — 3-button 미주입
- **증상**: Discord에 3개 선택지 대신 2개만 표시
- **원인**: `writePendingApproval()`이 buttons 배열 미주입
- **해결**: `step_type === 'file_permission'`일 때 자동 3-button 배열 주입
- **주의**: DOM observer 경로는 기존 command 텍스트 기반 감지 유지
### [2026-03-10] GetAllCascadeTrajectories — 크로스 윈도우 세션 가로채기
- **증상**: Deriva AG에서 대화 시작 → gravity_control 채널에 Deriva 내용이 릴레이
- **원인**: `GetAllCascadeTrajectories`가 모든 인스턴스의 세션 반환
- **해결**: `workspaces[0].workspaceFolderAbsoluteUri` 비교하여 자기 workspace 세션만 처리
- **주의**: workspace URI normalize 필수 (protocol strip, %3A decode, 슬래시 통일, lowercase)
### [2026-03-10] 크로스 프로젝트 Response Watcher 우회
- **증상**: Deriva 세션 승인 시도가 gravity_control에서 실패
- **원인**: pending 파일 삭제 후 response watcher가 project_name 체크 건너뜀
- **해결**: response JSON의 project_name으로 fallback 필터
- **주의**: response 데이터 자체에 project_name 필수
### [2026-03-10] file_permission — write 도구 3-button 미주입
- **증상**: `replace_file_content` 등 파일 수정 시 Discord에 2개만 표시
- **원인**: step_probe의 file_permission 도구 리스트에 write 도구 누락
- **해결**: write 도구를 file_permission 리스트에 추가
- **주의**: AG가 파일 접근 권한을 요청하는 모든 도구는 이 리스트에 포함필요
### [2026-03-10] bestSession IDLE 고착 — RUNNING 세션 못 잡는 버그
- **증상**: 새 대화 시작 → bridge가 구 IDLE 세션만 추적
- **원인**: `bestSession` 선택이 `lastModifiedTime`만 비교
- **해결**: RUNNING 세션이 IDLE보다 항상 우선
- **주의**: Reload Window로도 해결되지만 근본적으로는 RUNNING 우선 로직 필요
### [2026-03-10] Bot IDLE 채널 자동 생성 — 불필요한 Discord 채널 증식
- **증상**: 봇 시작 시 모든 등록된 프로젝트의 채널을 자동 생성
- **원인**: `pending_approval_scanner`가 매 사이클마다 채널 생성
- **해결**: 자동 채널 생성 루프 제거, on-demand 생성
- **주의**: `_get_channel()`은 이미 on-demand 생성 로직 포함
### [2026-03-10] Reload Window 후 세션 stale
- **증상**: Reload Window 후 세션이 IDLE/구 stepCount로 고정
- **원인**: LS 프로세스는 유지되어 trajectory tracker 캐시 미갱신
- **해결**: AG 완전 종료 → 재실행 (Full restart)
- **주의**: Extension 코드 변경 후 배포 시 Full restart 권장
### [2026-03-10] start_bot.bat — Windows Store Python 스텁 우선 실행
- **증상**: `start_bot.bat` 실행 시 스텁이 먼저 실행
- **원인**: `where python`이 Windows Store의 Python 스텁을 먼저 찾음
- **해결**: conda 경로를 우선 확인
- **주의**: Windows 10/11에서 App Aliases의 python.exe가 PATH에 기본 포함
### [2026-03-10] VSIX 빌드 — SDK JS 파일 미포함 (require 실패)
- **증상**: Extension 활성화 후 `SDK not initialized`
- **원인**: TypeScript 컴파일러가 `.js` 파일을 `out/`에 복사하지 않음
- **해결**: `compile` 스크립트에 복사 단계 추가 (`src/sdk/``out/sdk/`)
- **주의**: VSIX 패키징은 `out/sdk/`를 포함함. 문제는 빌드 단계 복사 누락
### [2026-03-10] SDK _findLSProcess — 대소문자 구분 workspace hint 매칭 실패
- **증상**: variet-agent AG에서 Discord에 신호 미도달
- **원인**: SDK가 workspace hint를 대소문자 구분으로 비교
- **해결**: `fixLSConnection()` 함수로 대소문자 무시 비교 + 재연결
- **주의**: 각 AG 창마다 별도 LS 프로세스 존재 (workspace_id로 구분)
---
## 환경변수 / 설정 관련 (2026-03-11)
### [2026-03-11] config.py BRAIN_PATH — `.env` 빈 문자열 → CWD 해석 버그
- **증상**: 봇이 Extension의 snapshot/pending을 전혀 읽지 못함
- **원인**: `.env``BRAIN_PATH=` (빈 값)이면 빈 문자열 반환
- **해결**: `os.getenv("BRAIN_PATH") or default` 패턴
- **주의**: `os.getenv(key, default)`는 빈 값이라도 default 미사용
### [2026-03-11] Extension DEDUP MERGE — 크로스 프로젝트 pending 오염
- **증상**: `#ag-lifetimepd` 채널에 variet_agent의 승인 요청 표시
- **원인**: DEDUP 로직이 `project_name`을 체크하지 않음
- **해결**: 3곳 dedup 조건에 `project_name` 가드 추가
- **주의**: 모든 Extension 인스턴스가 동일한 `bridge/pending/` 디렉토리 공유
### [2026-03-11] Collector 동기 HTTP — aiohttp 전환
- **증상**: Collector가 이벤트 루프 전체 블로킹
- **원인**: `urllib.request.urlopen()` 사용 (blocking I/O)
- **해결**: `aiohttp.ClientSession` 기반 비동기 전환
- **주의**: `import aiohttp`는 lazy
---
## Rate Limit / 무한 루프 관련 (2026-03-12)
### [2026-03-12] RemoteTransport 429 무한 루프 — Extension 크래시 + AG 먹통
- **증상**: `429 Rate limited` 로그가 초당 수십 건 무한 반복
- **원인**: 3가지 복합 (백오프 없음 + 개별 HTTP 요청 + 공격적 rate limit)
- **해결**: 지수 백오프 + `Retry-After` 지원 + rate limit 완화
- **주의**: AG 먹통은 봇 자체가 유발한 문제
### [2026-03-12] workbench.html 0-byte 파괴 — AG 새 창 먹통
- **증상**: AG 새 창 열면 화면 먹통
- **원인**: 3개 Extension 인스턴스가 동시에 workbench.html 읽기/쓰기 → 0 bytes로 덮어쓰기
- **해결**: pre-patch backup(.orig) + 구조 검증 + 자동 복원
- **주의**: 멀티 윈도우 환경에서 HTML 패치 race condition은 파일 잠금 없이 완전 해결 불가
### [2026-03-12] workbench.html 크로스 복원 — CSS 미로딩으로 레이아웃 깨짐
- **증상**: 아이콘은 보이지만 레이아웃 완전 깨짐
- **원인**: workbench.html을 jetski HTML에서 복원할 때 CSS 교체 누락
- **해결**: 파일별 `requiredMarker` 검증 + `.orig` 백업 + 자동 복원
- **주의**: **workbench.html과 workbench-jetski-agent.html은 교환 불가능**
### [2026-03-12] Collector 단일 프로젝트 폴링 — 멀티 프로젝트 command 전달 불가
- **증상**: Deriva AG IDE에 명령 전달되지 않음
- **원인**: Collector가 단일 프로젝트만 폴링
- **해결**: `_discover_local_projects()`로 모든 프로젝트 폴링
- **주의**: `/api/commands/all` 엔드포인트는 크로스 PC 명령 오염을 유발
### [2026-03-12] RemoteTransport backing off 무한 반복
- **증상**: IDLE 시 `backing off 1s` 경고가 영구 반복
- **원인**: 3가지 구조적 결함 (즉시 리셋 + 불필요 요청 + asyncio burst)
- **해결**: 연속 5회 성공 후 절반 감소 + adaptive 간격 + 루프 stagger
- **주의**: `_reset_backoff()` 즉시 리셋 패턴은 다중 소비자 환경에서 **절대 사용 금지**
---
## DEDUP / 크로스 세션 관련 (2026-03-15)
### [2026-03-15] DEDUP step_index 크로스 세션 충돌 — 승인 신호 누락
- **증상**: WAITING step 감지 → pending 미생성 → 10분+ 대기
- **원인**: DEDUP 로직이 `conversation_id`를 비교하지 않음
- **해결**: DEDUP 조건에 `conversation_id` 가드 추가
- **주의**: `project_name` 가드만으로는 불충분 — 같은 Extension이 여러 세션을 볼 수 있음
### [2026-03-15] Discord Gateway MESSAGE_CREATE 중복 — embed 이중 전송
- **증상**: Discord 명령 시 동일 embed가 2개 전송
- **원인**: Discord Gateway가 WebSocket 불안정 시 이벤트 중복 전달
- **해결**: `on_message``_processed_message_ids` dedup 추가
- **주의**: Gateway reconnection, RESUME 실패 시 발생 빈도 증가
### [2026-03-15] HTML 패치 멀티 인스턴스 race condition — 화면 파괴
- **증상**: Extension 패치 후 AG 재시작 시 전체 화면 날아감
- **원인**: 2+ Extension 인스턴스가 동시에 같은 HTML에 readFileSync/writeFileSync
- **해결**: `.patch-lock` 파일 기반 cross-instance lock 추가
- **주의**: Lock은 "방지", .orig 백업은 "복구". 둘 다 유지
### [2026-03-15] 로컬 승인 ↔ Discord 승인 교차 race condition
- **증상**: AG에서 직접 Run 클릭 후 Discord 승인 요청이 "완료됨" 표시 안 됨
- **원인**: auto_resolve가 Discord에 알림 없음 + processResponseFile 상태 미체크
- **해결**: writeChatSnapshot 추가 + 상태 확인 후 skip + _approval_messages dict
- **주의**: processResponseFile L2534의 리셋이 핵심 gate
### [2026-03-15] 크로스 프로젝트 DEDUP MERGE — Deriva→gravity_control 오염
- **증상**: Deriva의 데이터가 gravity_control pending에 MERGE됨
- **원인**: MERGE 조건에 `project_name` 가드 없음
- **해결**: MERGE 조건에 `project_name` 추가
- **주의**: `bridge/pending/` 디렉토리는 모든 Extension 인스턴스가 공유
### [2026-03-15] Double-Fire Auto-Approve — AI 세션 중단
- **증상**: auto-approve ON 시 AI 세션이 간헐적으로 중단
- **원인**: Extension auto-approve 경로 + Bot auto-approve 경로 동시 실행 → 2번 RPC
- **해결**: Extension auto-approve 경로 제거. Bot만 담당
- **주의**: 단일 경로 원칙 유지
### [2026-03-15] DOM Observer "Deny" False Positive — Auto-approve 세션 크래시
- **증상**: auto-approve ON 시 "Deny" command가 자동 승인됨 → 세션 크래시
- **원인**: DOM observer가 Deny를 독립 pending으로 생성. default 분기로 잘못된 RPC 전송
- **해결**: FALSE_POSITIVE_RE에 Deny/Allow Once 등 추가 + reject-word 차단 가드
- **주의**: VSIX 빌드 → AG 풀 재시작 필요
### [2026-03-15] PATS 배열 Deny 트리거 — 근본 수정
- **증상**: Deny가 주 트리거로 사용됨
- **원인**: PATS 배열에 Deny 패턴 포함
- **해결**: PATS에서 거절/보조 버튼 제거. 긍정 버튼만 그룹 트리거
- **주의**: PATS = "그룹 생성 트리거", ALL_ACTION_RE = "형제 수집 패턴"
### [2026-03-15] Auto-Resolved 채팅 폭주 — 루프 내 writeChatSnapshot
- **증상**: "✅ AG에서 직접 승인됨" 메시지가 반복 전송
- **원인**: 루프 내부에서 매 파일마다 writeChatSnapshot 호출
- **해결**: 루프 바깥에서 1회 + conversation_id 조건 추가
- **주의**: 외부 시스템에 메시지 보낼 때는 반드시 루프 바깥에서 집계 후 1회 발송
### [2026-03-15] projectName=default 승인 오발
- **증상**: workspace 없는 AG 창이 다른 프로젝트의 WAITING을 감지
- **원인**: `detectProjectName()`이 workspace 없으면 "default" 반환
- **해결**: `projectName === 'default'`이면 pending 생성/auto-approve 억제
- **주의**: Empty Window에서는 bridge 기능을 최소화
### [2026-03-15] 이전 분석 오판(False Positive) — 교훈
- **증상**: P0/P1으로 보고한 문제들이 이미 방어되고 있었음
- **원인**: 로컬 코드 스니펫만 보고 판단
- **해결**: 전체 Flow 추적으로 교차 검증
- **주의**: **코드 감사 시 반드시 producer→transport→consumer→side effects 전체 경로를 추적**
---
## processResponseFile 상태 관리 (2026-03-16)
### [2026-03-16] processResponseFile 상태 리셋 — 무한 루프 vs auto_resolve 회귀
- **증상**: Discord 승인 후 같은 step에 대해 pending이 반복 생성 → 무한 auto-approve 루프
- **원인**: processResponseFile이 무조건 리셋 → step_probe가 같은 WAITING step을 새 step으로 착각
- **해결**: `sawRunningAfterPending = true`만 설정. lastPendingStepIndex와 stallProbed 유지
- **주의**: **processResponseFile의 상태 리셋은 sawRunningAfterPending = true만 설정**. `docs/approval-flow.md` 참조
### [2026-03-16] recentPendingSteps 메모리 dedup
- **증상**: pending 파일 삭제 → 같은 step_index로 새 pending 생성
- **원인**: writePendingApproval()의 dedup이 파일 존재 여부에만 의존
- **해결**: `recentPendingSteps` Map (TTL 60초) 추가
- **주의**: DOM observer HTTP 경로는 이 메모리 dedup 미적용
### [2026-03-16] 멀티 프로젝트 동시 신호 정지 — Scanner O(N) Discord API 병목
- **증상**: 여러 프로젝트 동시 pending → 모든 프로젝트 신호 전달 정지
- **원인**: scanner가 1 tick에 모든 pending 순차 처리 → Discord 429 rate limit
- **해결**: `discord.utils.get(guild.channels)` 캐시 + per-tick cap (5건)
- **주의**: `guild.channels`는 discord.py 내부 캐시
---
## Diff Review 관련 (2026-03-16)
### [2026-03-16] step_type 매핑 버그 — write_to_file이 file_permission으로 잘못 매핑
- **증상**: 코드 편집 승인 시 잘못된 RPC 전송
- **원인**: 쓰기 도구가 읽기 도구와 함께 `file_permission`으로 매핑
- **해결**: 읽기/쓰기 도구 분리, 쓰기는 `code_edit` step_type 사용
- **주의**: AG는 대부분 파일 쓰기에 WAITING 안 만듦
### [2026-03-16] diff_review isDirty 실패 — AG diff는 VS Code dirty 아님
- **증상**: Accept 클릭 → `isDirty` 문서 0개 → 효과 없음
- **원인**: AG stacked code review는 VS Code `isDirty`와 무관
- **해결**: `AcknowledgeCascadeCodeEdit` RPC → fallback으로 VS Code 커맨드
- **주의**: diff_review pending에 `modified_files``edit_step_indices` 필수
### [2026-03-16] diff_review pending 순서 — AI 응답보다 먼저 Discord 도착
- **증상**: diff_review 버튼이 먼저, AI 응답 텍스트가 나중
- **원인**: pending_approval_scanner가 chat_snapshot_scanner보다 먼저 fire
- **해결**: diff_review pending 생성을 `setTimeout(8000)`으로 지연
- **주의**: 8초는 전체 전파 경로 고려
### [2026-03-16] diff_review AcknowledgeCascadeCodeEdit steps=[] — Collector pending 삭제 race
- **증상**: Accept all 클릭 → RPC SUCCESS → diff review 바 안 사라짐
- **원인**: Collector가 pending 파일 즉시 삭제 → Extension이 메타데이터 못 읽음
- **해결**: `diffReviewMetadata` 인메모리 Map 추가
- **주의**: Extension Reload 시 소실되지만 새 diff_review는 정상 동작
### [2026-03-16] AcknowledgeCascadeCodeEdit SUCCESS → diff review bar 미해제 — 잘못된 RPC 메서드명
- **증상**: RPC SUCCESS 반환 → diff review bar 여전히 표시
- **원인**: RPC 메서드명 자체가 틀렸음 (`AcknowledgeCascadeCodeEdit` → 실제는 `acknowledgeCodeActionStep`)
- **해결**: `agentAcceptAllInFile` / `agentRejectAllInFile` VS Code 커맨드 사용
- **주의**: AG의 RPC는 잘못된 메서드명도 에러 없이 `{}` 반환. **RPC `{}`는 실패로 간주해야 함**
### [2026-03-16] diff_review RPC 3개 전략 모두 dead-end
- **증상**: 3개 RPC 전략 모두 실패
- **원인**: (1) submitCodeAcknowledgement 미등록, (2) acknowledgeCodeActionStep 404, (3) AcknowledgeCascadeCodeEdit no-op
- **해결**: VS Code 커맨드 기반 (`agentAcceptAllInFile` / `agentRejectAllInFile`)
- **주의**: **diff_review를 RPC로 해결하려는 시도 모두 실패 확정. VS Code 커맨드 기반만 유효**
### [2026-03-16] AG 소스 역분석 — diff review 내부 동작 체인
- **증상**: Accept all / Reject all 내부 동작을 재현해야 함
- **원인**: AG 공식 API/문서 없음
- **해결**: AG 설치 경로의 JS 소스에서 역분석
- **주의**: minified JS에서 변수명은 버전마다 변경됨
### [2026-03-16] diff_review 이중 승인 요청 — DOM observer가 Accept/Reject 버튼 캡처
- **증상**: diff_review 외에 별도 "Accept"/"Reject" pending 도착
- **원인**: `openReviewChanges` 커맨드가 diff UI 패널 → DOM observer 감지
- **해결**: FALSE_POSITIVE_RE에 Accept/Reject all 추가
- **주의**: diff review bar 버튼은 전용 시스템에서 처리
### [2026-03-16] diff_review가 brain/ artifact에도 트리거
- **증상**: task.md만 수정해도 "코드 리뷰" pending 생성
- **원인**: diff_review 감지 로직이 모든 수정 파일 추적
- **해결**: `.gemini/antigravity/brain/` 경로 파일 필터링하여 제외
- **주의**: 코드 파일 + brain artifact 혼합 시 코드 파일만 diff_review
---
## WS Hub 전환 관련 (2026-03-16~17)
### [2026-03-16] !auto 이중 메시지 — Extension echo + Bot embed
- **증상**: `!auto` 토글 시 메시지 2개 표시
- **원인**: Bot embed + Extension writeChatSnapshot echo
- **해결**: Extension의 `!auto` handler에서 writeChatSnapshot echo 제거
- **주의**: Bot 재시작 시 auto_approve_projects 초기화 → 수동 모드 복귀
### [2026-03-16] 병렬 WAITING step 누락 — step_probe break문
- **증상**: 병렬 tool call 시 1개만 승인 요청 도착
- **원인**: step_probe 루프에서 `break`문이 첫 번째 WAITING 후 종료
- **해결**: `break` 제거, 모든 WAITING step에 pending 생성
- **주의**: 중복 방지는 `recentPendingSteps` Map이 처리
### [2026-03-16] Bot chat_snapshot 전송 로깅 부재
- **증상**: 전송 성공/실패를 Bot 로그에서 확인 불가
- **원인**: `channel.send()` 성공 후 INFO 로그 없음
- **해결**: 전송 성공/실패 로그 추가
- **주의**: `_get_channel()` 실패 시 WARNING은 이전에도 있었음
### [2026-03-16] 크로스 프로젝트 이벤트 폭주 — Watcher/Collector 무필터
- **증상**: /start 시 타 프로젝트 알림 유입
- **원인**: watcher.py가 brain/ 전체를 감시
- **해결**: `_is_my_session()` 필터 + 이벤트 전달 필터 추가
- **주의**: 미등록 세션은 allow-through 방식
### [2026-03-16] pending 파일 139개 누적 — 정리 로직 부재
- **증상**: bridge/pending/에 139개 파일 누적
- **원인**: auto_resolved/expired 파일을 아무도 삭제 안 함
- **해결**: Collector에서 auto_resolved/expired 전달 후 삭제 + 10분 자동 삭제
- **주의**: startup_pending은 정리 대상에서 제외
### [2026-03-17] NPM WebSocket 프록시 — Upgrade 헤더 미전달
- **증상**: `wss://ag.variet.net/ws` 연결 시 HTTP 400
- **원인**: Nginx Proxy Manager에 WebSocket Support 미활성화
- **해결**: NPM 대시보드에서 Websockets Support 체크
- **주의**: 새 프록시 호스트 생성 시 반드시 확인
### [2026-03-17] WS auth_fail 무한 재연결 — _cleanup() close 이벤트
- **증상**: Auth failed 후 60초마다 재연결 반복
- **원인**: `_cleanup()`이 ws.close() → close 이벤트 → _scheduleReconnect() 체인
- **해결**: `this.shouldReconnect = false``_cleanup()` 이전에 설정
- **주의**: `_cleanup()`은 이벤트 핸들러를 트리거하므로 상태 변경은 반드시 호출 전에
### [2026-03-17] initStepProbe workspaceUri 누락 — 세션 감지 완전 불능
- **증상**: POLL alive만 출력, SESSION-FILTER/SNAPSHOT 없음
- **원인**: `initStepProbe()` 호출 시 필수 필드 미전달 + `as` 캐스트가 런타임 검증 없음
- **해결**: `workspaceUri`, `diffReviewMetadata: new Map()` 추가
- **주의**: TypeScript `as` 캐스트는 런타임 검증 없음
### [2026-03-17] WS 명령어 에코 릴레이 — Discord 메시지 2번 표시
- **증상**: Discord 메시지 입력 → 같은 메시지가 다시 표시
- **원인**: handleWSCommand에서 `recentDiscordSentTexts`에 마킹 안 함
- **해결**: `recentDiscordSentTexts.set()` 추가
- **주의**: 파일 기반 경로에는 이미 마킹 있었음
### [2026-03-17] writeRegistration 이중 쓰기 — WS 전송 후 파일도 작성
- **증상**: WS 상태에서도 register/ 파일 생성
- **원인**: WS 전송 후 `return` 없이 파일 쓰기 실행
- **해결**: WS 전송 후 `return` 추가
- **주의**: 새 WS 전송 함수 추가 시 file fallback과 상호 배타적 `return` 확인

View File

@@ -0,0 +1,629 @@
# Known Issues & Lessons Learned
> **<2A><20><EFBFBD><EFBFBD><EC94AA><EFBFBD> SSOT(Single Source of Truth)<29><EFBFBD><EFBFBD>떎.**
> <EFBFBD>뵒踰꾧퉭<EFBFBD><EFBFBD>굹 援ы쁽 <20><EFBFBD>뿉 **諛섎뱶<EC848E>떆** <20><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>슂.
> <EFBFBD><EFBFBD>뀡 醫낅즺 <20><20>깉濡<EAB989> 諛쒓껄<EC9293><20><EFBFBD>뒋瑜<EB928B> <20><20><EFBFBD><EFBFBD>뿉 異붽<E795B0><EBB6BD><EFBFBD><EFBFBD><EFBFBD>떎.
# Known Issues & Lessons Learned
> **씠 뙆씪 SSOT(Single Source of Truth)엯땲떎.**
> 뵒踰꾧퉭씠굹 援ы쁽 쟾뿉 **諛섎뱶떆** 씠 뙆뙆씪쓣 솗씤븯꽭슂.
> 꽭뀡 醫낅즺 떆 깉濡 諛쒓껄맂 씠뒋瑜 씠 뙆뙆씪뿉 異붽빀땲떎.
> [!TIP]
> 빐寃 셿猷뚮맂 怨쇨굅 씠뒋뒗 [`known-issues-archive.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues-archive.md)뿉 蹂닿릺뼱 엳뒿땲떎.
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
### [2026-04-19] [Observer] ★ Accept all 버튼이 `<span>`으로 렌더링 — Observer 감지 실패 (v0.5.101)
- **증상**: "Accept all" diff review 버튼이 화면에 보이지만 Observer가 감지하지 못함. Discord에 "Accept all" 자동 승인 알림이 안 옴.
- **원인**: AG Native UI 업데이트로 "Accept all"이 `<button>`이 아닌 `<span class="cursor-pointer">`로 렌더링됨. Observer의 `allBtns = querySelectorAll('button, [role="button"]')`에 span이 포함되지 않음. ACCEPT-SCAN 로그: `tag=SPAN cls=hover:text-ide-button-hover-color cursor-po txt=Accept all`.
- **해결 (v0.5.101)**: `allBtns` 선택자에 `span.cursor-pointer` 추가.
- **주의**: observer-dev-guide 섹션 3.3 "Accept all — Observer 접근 불가"는 outdated. UI 변경으로 chat panel footer에 Accept all이 표시됨. 가이드 업데이트 필요.
### [2026-04-19] [Bridge] ★ auto-approve response 파일에 `_from_ws` 마커 누락 — Observer polling 실패 (v0.5.103)
- **증상**: Observer가 "Accept all"을 감지하고 bridge가 자동 승인했지만, Observer의 `pollResponseGroup` GET `/response/{rid}`가 항상 `{waiting: true}` 반환. 버튼 클릭이 실행되지 않음.
- **원인**: http-bridge의 auto-approve 경로에서 response JSON 파일에 `_from_ws: true` 마커가 없음 → `processResponseFile`(response watcher)이 Observer보다 먼저 파일을 읽고 삭제 → Observer polling 시 파일 부재. known-issues [2026-04-18] WS response 삭제 버그와 동일 패턴.
- **해결 (v0.5.103)**: auto-approve response에 `_from_ws: true` + `_auto_approve_ttl` 마커 추가.
- **주의**: **response 디렉토리에 파일을 쓰는 모든 경로**는 반드시 `_from_ws: true` 마커를 포함해야 함. processResponseFile이 먼저 소비하는 race condition 항상 존재.
### [2026-04-18] [Extension] ★ WS response 파일이 processResponseFile에 의해 삭제 → Observer pollResponseGroup 실패 (v0.5.78)
- **증상**: `!auto` Retry가 작동하지 않음. Observer가 `/response/{rid}`를 폴링하지만 항상 `{waiting: true}` 반환.
- **원인**: extension.ts의 WS 응답 핸들러가 `response/{rid}.json` 파일 작성 → 300ms 후 response watcher(`processResponseFile`)가 파일 감지 → pending 파일이 없어 `isDomObserver=false``fs.unlinkSync()` 실행 → Observer가 폴링할 때 파일이 이미 삭제됨.
- **해결 (v0.5.78)**: WS 응답 파일에 `_from_ws: true` 마커 추가. `processResponseFile`에서 `_from_ws` 파일 스킵 (WS 핸들러에서 이미 `tryApprovalStrategies` 실행하므로 중복 방지도 함께 해결).
- **주의**: http-bridge의 `_handlePending`는 pending 파일을 생성하지 않음 (WS 전송만 수행). 따라서 `processResponseFile``isDomObserver` 판별이 실패함. WS 경로로 들어오는 모든 response는 반드시 마커로 구분해야 함.
### [2026-04-18] [Extension] ★ extractContextFromNearby 조상 탐색만으로는 명령어 추출 불가 (v0.5.79)
- **증상**: Discord auto-approve 알림에 "Always run"만 표시되고 실제 명령어가 안 보임. depth를 10으로 늘려도 동일.
- **원인**: AG Native DOM 구조에서 "Always run" 버튼은 `footer` 내부에 있고, 실제 명령어(`pre.font-mono`)는 `footer`**형제(sibling)** 요소에 있음. 조상 탐색(parentElement)으로는 도달 불가. trail: `d0:button → d1:div → d2:footer` (footer.parentElement가 null).
- **해결 (v0.5.79)**: `extractContextFromNearby`에 형제 탐색 로직 추가. 각 depth에서 `node.parentElement.children`을 순회하며 `pre.font-mono, pre, code`를 찾음 → `CONTEXT-OK src=sibling` 성공.
- **주의**: Observer 코드 변경은 HTML 인라인 스크립트이므로 AG 2번 재시작(Quit + Relaunch × 2) 필요.
### [2026-04-18] [Extension] Thinking 블록이 AI 응답으로 릴레이됨
- **증상**: AI의 내부 사고 과정(thinking/reasoning)이 Discord에 릴레이됨.
- **원인**: Observer의 `scanChatBodies``.leading-relaxed.select-text` 블록을 전부 캡처하는데, thinking 블록도 이 셀렉터에 매칭됨.
- **해결**: thinking 블록의 조상에 `max-h-[200px]` 클래스가 있는지 확인하여 필터링.
- **주의**: AG UI 업데이트로 thinking 블록의 클래스가 변경될 수 있음.
- **증상**: Step Probe의 RT-CAPTURE, HB-CAPTURE가 현재 대화 중에 전혀 발동하지 않음. POLL에서 `status=IDLE, steps=928, delta=0` 고정. Heartbeat probe에서도 `real=928 known=928` 불변.
- **원인**: AG Native의 `GetCascadeTrajectorySteps` API는 **cascade가 완전히 종료(IDLE 전환)된 후에만** 새 step을 반환합니다. 진행 중인 cascade에서는 step count가 동결됩니다. `GetAllCascadeTrajectories``stepCount`도 마찬가지.
- **영향**: Step Probe 기반의 모든 실시간 캡처(RT-CAPTURE, HB-CAPTURE, USER_INPUT 감지)가 **구조적으로 불가능**. Observer DOM이 유일한 실시간 데이터 경로.
- **해결**: Observer DOM 기반 chat relay를 재활성화 (v0.5.72). Step Probe는 cascade 완료 후 보완 용도로만 사용.
- **주의**: 이 제약은 AG Native 아키텍처의 근본적 특성. Polling 주기나 heartbeat 빈도를 변경해도 해결 불가. **실시간 릴레이는 반드시 Observer DOM 경로를 사용해야 함.**
- **참조**: `.agents/references/relay-architecture.md` (상세 분석)
### [2026-04-18] [Extension] Observer의 /pending POST에 명령어 컨텍스트가 없음 (Always run만 전달)
- **증상**: Discord auto-approve 알림에 "Always run"만 표시되고 실제 명령어가 안 보임.
- **원인**: Observer가 `/pending` POST 시 `command: "Always run"`, `description: "Always run"`만 보냄. `extractContextFromNearby(btn)`이 버튼 주변 DOM에서 유의미한 텍스트를 찾지 못함.
- **해결 (부분)**: v0.5.69에서 bridge/pending/ 디렉토리의 최신 Step Probe pending 파일에서 명령어를 읽는 fallback 추가. Step Probe pending이 있을 때만 작동.
- **주의**: Step Probe WAITING 감지가 진행 중 cascade에서 불가하므로 (위 이슈 참조), 현재 대부분의 경우 fallback도 실패. Observer의 DOM 컨텍스트 추출 개선이 필요.
### [2026-04-18] [Extension] Observer의 사용자 메시지 셀렉터 미매칭
- **증상**: 사용자가 AG에서 입력한 메시지가 Discord에 전혀 전달되지 않음.
- **원인**: Observer의 셀렉터(`.text-ide-message-block-user-color`, `[data-message-role="user"]` 등)가 AG Native DOM에서 매칭되지 않음. AI 응답만 `.leading-relaxed.select-text`로 매칭됨.
- **해결**: DOM 덤프에서 사용자 메시지 블록의 실제 CSS 클래스를 식별 후 셀렉터 추가 필요.
- **주의**: Step Probe의 USER_INPUT 캡처도 진행 중 cascade에서 불가 (위 이슈 참조).
### [2026-04-16] [Extension] ★ AG Native 세션 AI 응답이 Discord에 CSS로 전달됨 (v0.5.52 수정, #632)
- **증상**: Discord에 AI 대화 응답 대신 **CSS 스타일시트 코드** (`remark-github-blockquote-alert/alert.css`)가 전달됨. `scanChatBodies()` → POST /chat 경로는 작동하지만 내용이 CSS.
- **원인**: `extractCleanStepText()`에서 clone한 DOM에서 버튼/SVG/아이콘은 제거하지만 **`<style>` 태그는 제거하지 않음**. AG Native 마크다운 렌더러가 `.leading-relaxed.select-text` 내부에 `<style>` 블록을 주입하는데, 이 CSS textContent가 AI 응답 텍스트로 추출됨.
- **해결 (v0.5.52)**: `extractCleanStepText()` 최상단에 `clone.querySelectorAll('style, script, noscript, link[rel="stylesheet"]')` 제거 로직 추가. CSS/JS가 텍스트로 포함되는 것을 원천 차단.
- **배포**: v0.5.52 VSIX 설치 + v0.5.50/out/ JS 직접 복사 + V8 CachedData 삭제. AG File→Quit 재시작 필요.
- **이전 블로커 해결 이력**:
- SDK 경로: AG Native는 Cascade trajectory API 미등록 → step-probe RT-CAPTURE 불가 (구조적 한계)
- DOM 경로: v15에서 `#conversation` + `.leading-relaxed.select-text` 셀렉터 추가로 해결
- BEACON=0: AG 프로세스 완전 재시작으로 해결 (Reload Window로는 렌더러 HTML 캐시 유지)
- **주의**: AG Native 마크다운 렌더링은 `<style>` 블록을 응답 DOM 내부에 인라인으로 삽입함. DOM 텍스트 추출 시 반드시 style/script 태그를 먼저 제거해야 함.
- **Vikunja**: #632
### [2026-04-16] [Extension] ★ AG Native Observer innerText로 인한 마크다운 포맷 유실 및 사용자 요청 누락
- **증상**: Discord로 릴레이되는 AI 응답에서 리스트 번호(`1. 2.`)나 불릿(`-`) 등 마크다운 포맷이 완전히 유실되고 텍스트만 이어져서 나옴. 그리고 사용자가 입력한 메시지(요청)는 아예 릴레이되지 않음.
- **원인 1**: `extractCleanStepText()`에서 `innerText`를 사용하여 텍스트를 추출할 때, 렌더링되지 않은 DOM이나 CSS 카운터로 생성된 list-item 마커가 무시됨.
- **원인 2**: `scanChatBodies()` 로직이 `.leading-relaxed.select-text` (AI 응답 블록)만을 타겟팅하여 사용자의 메시지 박스(예: `.text-ide-message-block-user-color`)는 추출 대상에서 제외됨.
- **해결 (계획 중)**: `innerText` 대신 HTML DOM 노드를 순회하며 `convertNodeToMarkdown` 변환 함수를 통해 마크다운 문법을 보존하도록 개선. User 블록도 함께 감지하여 브릿지에 `role: 'user'` 플래그를 추가 전송하도록 수정 예정.
- **주의**: Webview 내에서 텍스트 노드를 파싱할 때 `innerText`는 브라우저 레이아웃 엔진에 의존하므로 완전한 마크다운 보존을 위해서는 Node Type 순회를 활용한 구조 복원이 보장되어야 함.
### [2026-04-16] [Extension] 터미널 출력(stdout) 텍스트가 명령어로 Discord에 전송 (v0.5.50)
- **증상**: Discord에 `cmd="No extension.log found"`, `cmd="AG CLI not found..."`, `cmd="Log found: C:\..."` 등 터미널 **출력** 텍스트가 명령어로 전송됨
- **원인**: Observer가 code 블록 2개를 감지: (1) 프롬프트+명령어 → JUNK_CODE_RE로 스킵, (2) 터미널 출력 → 유효한 code로 판단 → description에 포함. http-bridge enrichment에서 description에 prompt marker(`>`)가 없으면 rawDesc 전체를 enrichedCmd로 채택
- **해결 (v0.5.50)**: `promptMatch` 실패 시(description에 `>` 없음) → 터미널 OUTPUT으로 판단하여 `terminal_output` 사유로 즉시 필터. 실제 명령어는 항상 `…\project > command` 프롬프트를 포함
- **주의**: enrichment은 반드시 prompt marker가 있는 경우에만 수행. description에 `>` 없으면 code 블록의 출력 텍스트
### [2026-04-15] [Extension] Observer fallback 컨텍스트가 채팅/UI 텍스트를 명령어로 추출 (v0.5.46)
- **증상**: Discord에 `cmd="실 동작검증을 해봐야하는데"`, `cmd="variet.gravity-bridge"` 등 사용자 채팅/AI 응답 텍스트가 명령어로 전송됨
- **원인**: v0.5.45에서 `PROMPT_ONLY_RE``code/pre` 요소 스킵 성공했으나, `extractContextFromNearby()`의 fallback(`span/div/p` 수집)이 여전히 작동하여 DOM 트리의 채팅 본문/UI 라벨을 명령어로 추출
- **해결 (v0.5.46)**: Observer v13에 `_promptOnlySkipped` 플래그 — code 요소가 모두 prompt-only이면 fallback 비활성화. http-bridge에 generic button + no-context일 때 무조건 필터
- **주의**: 프롬프트 스킵과 fallback 비활성화는 항상 연동해야 함. VSIX 설치 누락 방지를 위해 빌드 후 즉시 `code --install-extension` 확인 필수
### [2026-04-15] [Extension] PROMPT_ONLY_RE regex fixes — prompt-only terminal text leaking as enriched cmd (v0.5.45)
- **증상**: Discord에 `cmd="…\gravity_control >"` (실제 명령어 없는 빈 터미널 프롬프트)가 전송됨. 명령어가 포함된 경우는 정상 작동
- **원인 1 (Observer)**: `PROMPT_ONLY_RE``\\\\s`(4중 backslash)가 template literal 안의 regex 리터럴에서 "literal backslash+s"가 되어 whitespace class가 아닌 문자열 매칭
- **원인 2 (http-bridge)**: `PROMPT_ONLY_RE = /^[\s\\\/]*[\w_.-]+\s*[>»$#]\s*$/` — AG 터미널 프롬프트가 `…`(U+2026 ellipsis)로 시작하는데 첫 문자 클래스에 포함 안 됨
- **해결 (v0.5.45, `d2c44fe`)**: Observer `\\s``\s`, http-bridge 패턴을 `/^.*[>»$#]\s*$/`로 단순화
- **검증**: 11개 테스트 케이스 전체 통과
---
### [2026-04-14] [Extension] Observer template literal 정규식 이스케이핑 오류 — "Running N commands" 스킵 미작동
- **증상**: v9에서 `Running N commands` 그룹 헤더를 스킵하는 정규식 `/^Running\s*\d+\s*commands?$/i`를 추가했으나, 실제로 "Running2 commands" 텍스트를 전혀 매칭하지 못하여 여전히 Discord에 `cmd="Running2 commands"`가 전송됨
- **원인**: `observer-script.ts`의 전체 코드가 TypeScript template literal (`` ` `` ... `` ` ``) 안에 있으므로, 정규식 리터럴의 백슬래시가 2중 해석됨:
- TS 소스 `\\\\s` (4중) → template literal 출력 `\\s`**브라우저에서 regex 리터럴 `\\s` = 리터럴 백슬래시+s**
- TS 소스 `\\s` (2중) → template literal 출력 `\s`**브라우저에서 regex 리터럴 `\s` = whitespace class**
- TS 소스 `\s` (1중) → template literal에서 이스케이프 소멸 → **출력 `s`**
- **영향 범위**: `NOISE_CODE_RE`, `cleanLines()` (word boundary `\b`, newline `\n`), `cleanButtonText()` (whitespace `\s`), port 탐색 regex `\d`, "Running N commands" 스킵 regex 전부 미작동이었음
- 단, `NOISE_RE` (new RegExp() 사용)는 문자열 이스케이핑이므로 4중 백슬래시가 올바름 (string → RegExp는 별도 이스케이핑 레이어)
- **해결**: 모든 정규식 리터럴 (`/pattern/flags`) 안의 `\\\\s``\\s`, `\\\\d``\\d`, `\\\\b``\\b`, `\\\\n``\\n` 으로 수정 (v0.5.41, `8e89209`)
- **주의**: **template literal 안에서 정규식 특수문자를 쓸 때 반드시 구분:**
- `new RegExp('pattern')` 문자열: `\\\\s` (4중) — string 이스케이핑(1번)+regex 이스케이핑(1번) = 총 2번
- `/pattern/` 정규식 리터럴: `\\s` (2중) — template literal 이스케이핑(1번)만 = 총 1번
- **검증법**: `node -e "var f = require('./extension/out/observer-script.js').generateApprovalObserverScript; require('fs').writeFileSync('tmp.js', f(0)); console.log(require('fs').readFileSync('tmp.js','utf8').match(/Running.{10}/g));"` 으로 생성된 코드 확인
---
### [2026-04-13] [Extension] Observer v8 "Running N commands" 그룹 헤더를 승인 버튼으로 오인 — Discord 빈 내용+잘못된 버튼
- **증상**: Discord 승인 요청에 command="Running2 commands", description 비어있음, 버튼도 "Running2 commands / Always run" 형태. 실제 코드/명령어 내용이 전혀 표시되지 않음
- **원인 1**: `observer-script.ts``isActionBtn()``/Running\\s*\\d*\\s*command/i` 패턴이 있어 AG UI의 그룹 헤더 버튼("Running 3 commands")을 승인 버튼으로 분류. `scan()`이 이 버튼을 먼저 만나고 `break`로 나가 실제 "Always run"/"Cancel" 버튼은 처리 안 됨
- **원인 2**: `extractStepContext()``data-step-index` 속성 없으면 `cleanButtonText(btn)` = "Running2 commands"를 그대로 반환. AG Native에는 `data-step-index`/`data-testid` 속성이 없음 (DOM 덤프로 확인)
- **원인 3**: `http-bridge.ts`의 "Run/Always run" 필터가 step-probe 미활성(activeSessionId 비어있음) 시에도 DOM observer 신호를 차단
- **해결**: observer v9 (v0.5.40):
1. `isActionBtn()`에서 "Running N commands" 패턴 제거
2. `scan()`에서 `^Running\\s*\\d+\\s*commands?$` 명시적 스킵
3. `extractContextFromNearby()` 추가: `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출
4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent로 확대, 그룹 헤더 스킵
5. `http-bridge.ts`의 "Run" 필터에 `ctx.activeSessionId` 체크 추가 — step-probe 미활성 시 DOM observer 허용
- **주의**: AG Native UI의 "Running N commands"는 아코디언/그룹 헤더이며, 실제 승인 버튼은 하위 레벨의 "Run"/"Always run"/"Cancel". DOM 구조상 버튼 탐색 시 그룹 헤더를 반드시 스킵해야 함
### [2026-04-13] [Extension] HTTP Bridge UTF-8 인코딩 깨짐 — 한글 description 손실
- **증상**: pending/ 파일의 description 필드에서 한글이 `[AI ]`처럼 깨져서 저장됨. Discord로 전달되는 승인 요청 본문도 깨짐
- **원인**: Node.js HTTP 서버의 `req.on('data', chunk)` 콜백에서 chunk가 Buffer 타입으로 전달되는데, `body += chunk`로 string 결합 시 Buffer의 기본 인코딩(latin1)이 사용되어 multi-byte UTF-8 문자가 손실됨
- **해결**: 모든 POST 핸들러(`/pending`, `/dump-html`, `/chat`, `/deep-inspect-result`, `/test-rpc`)에 `req.setEncoding('utf8')` 추가 (v0.5.39)
- **주의**: Node.js HTTP 서버에서 POST body를 문자열로 수집할 때는 반드시 `req.setEncoding('utf8')`을 호출하거나, Buffer를 배열로 모은 후 `Buffer.concat().toString('utf8')`로 변환해야 함
### [2026-04-13] [Extension] Observer noise 필터 미작동 — textContent가 아이콘 텍스트를 줄바꿈 없이 합침
- **증상**: pending description에 `Thought for 1s`, `chevron_right` 등 Material 아이콘명과 UI 노이즈가 그대로 남아있음
- **원인**: DOM `textContent`는 block 요소 사이에 newline을 삽입하지 않아 `[AI 본문 요약]Thought for 1schevron_right[결행 명령]`처럼 한 줄로 합쳐짐. `cleanLines()`의 줄 단위 noise 필터(`^pattern$`)가 매칭 실패. 또한 `codeText` 추출에는 `cleanLines()`가 아예 미적용
- **해결**: `cleanLines()`에 인라인 pre-strip 추가 — icon명 18종을 regex로 먼저 `\n`으로 치환 후 줄 단위 필터 적용. `codeText`에도 `cleanLines()` 적용 (v0.5.39)
- **주의**: DOM에서 텍스트 추출 시 `textContent`는 레이아웃 무시, `innerText`는 detached 노드에서 미작동. 노이즈 필터링은 줄 단위뿐 아니라 인라인 패턴 제거도 병행해야 함
### [2026-04-13] [Extension] html-patcher String.replace() `$'` 특수 패턴으로 인라인 스크립트 SyntaxError
- **증상**: Observer v8 인라인 스크립트가 workbench.html에 삽입되었으나 렌더러에서 전혀 실행되지 않음 (BEACON 핑 0건). V8 캐시 삭제 + AG 재시작 후에도 동일
- **원인**: `html-patcher.ts`에서 `html.replace('</body>', '\n' + inlineBlock + '\n</body>')`를 사용. 인라인 스크립트의 NOISE_RE 정규식에 `')$', 'i'`가 있는데, `$'`는 JS `String.replace()`의 특수 대체 패턴(match 뒤의 텍스트)으로 해석됨. 이로 인해 `</body>` 뒤의 원본 HTML 구조(`<!-- Startup -->`, `<script src="workbench.js">`, `</html>`)가 JS 코드 중간(정규식 리터럴 안)에 삽입되어 **치명적 SyntaxError** 발생
- **해결**: `inlineBlock.replace(/\$/g, '$$$$')`로 모든 `$`를 이스케이프한 후 replacement 문자열로 사용 (v0.5.38, `d6ed876`)
- **주의**: `String.prototype.replace()`의 replacement 문자열에서 `$&`, `$'`, `` $` ``, `$1` 등은 특수 패턴. 동적 콘텐츠를 replacement로 사용할 때 반드시 `$` → `$$` 이스케이프 필요
### [2026-04-12] [Extension] V8 CachedData가 Observer 스크립트 로딩을 차단
- **증상**: html-patcher가 workbench-jetski-agent.html에 observer v7 인라인 스크립트를 성공적으로 삽입했지만, deep-inspect가 렌더러에서 응답 없음 (10s timeout). AG 재시작 후에도 observer가 로드되지 않음
- **원인**: `%APPDATA%\Antigravity\CachedData\` (50MB)에 V8 캐시가 남아있어, AG가 패치된 HTML 대신 캐시된 이전 버전을 로드. extension.log에 `patcher.install() called (needs reload)` 메시지가 표시되지만 실제 적용이 안 됨
- **해결**: `Remove-Item "$env:APPDATA\Antigravity\CachedData\*" -Recurse -Force` 실행 후 AG 재시작. known-issues-archive #6에도 동일 케이스 있음
- **주의**: HTML 패치 변경 시 **반드시** V8 CachedData 삭제 + AG 재시작 필요. 단순 AG 재시작만으로는 부족
### [2026-04-12] [DOM] text-ide-message-block-bot-color는 AI 응답 컨테이너가 아닌 NUX tooltip 전용
- **증상**: observer-script가 `.text-ide-message-block-bot-color`를 AI 응답 컨테이너로 사용하지만, 실제 AI 텍스트를 추출하지 못함
- **원인**: 번들 분석(jetskiAgent/main.js 10.8MB)으로 확인 결과, 이 클래스는 `hsn` 컴포넌트(NUX Tooltip 텍스트 색상)에서만 사용. AI 응답 텍스트는 `plannerResponse` step의 `Whi` 렌더러 → `div.px-2.py-1``MarkdownRenderer` 내부에 렌더링됨
- **해결**: observer-script에서 `.text-ide-message-block-bot-color` 의존성 제거 필요. `markdown-body` 클래스도 AG Native에 존재하지 않음
- **주의**: AI 응답 마크다운은 `prose` 관련 클래스나 MarkdownRenderer 내부 구조로 타겟팅해야 함. 실제 DOM 덤프로 정확한 셀렉터 확인 필요
### [2026-04-12] [SDK/DOM] AG Native 세션은 Cascade SDK API에 등록되지 않음 — DOM이 유일한 데이터 소스
- **증상**: AG Native 세션에서 Discord 릴레이로 AI 응답이 전혀 전달되지 않고, 대신 UI 노이즈(`content_copy`, `Always run`, `keyboard_arrow_up`, `Cancel`)가 전송됨
- **원인 1 (SDK)**: `GetCascadeTrajectorySteps(cascadeId=세션ID)``500 trajectory not found`. `GetDiagnostics``404`. AG Native 세션은 Cascade trajectory API에 전혀 등록되지 않는 별도 시스템
- **원인 2 (DOM)**: `observer-script.ts` v6의 `scanChatBodies()``.text-ide-message-block-bot-color` 컨테이너의 `textContent`를 통째로 가져오면서 내부 버튼/아이콘 텍스트까지 포함
- **해결**: `observer-script.ts` v7로 전면 재설계:
1. `[data-testid="conversation-view"]` + `[data-step-index]` 기반 step-aware 파싱
2. `extractCleanStepText()`: 클론 후 button/svg/icon 엘리먼트 제거 → 마크다운 텍스트만 추출
3. `extractStepContext()`: `getStepContainer()` → step 헤더 + code 블록만 추출
4. `NOISE_RE`: Material icon 이름, 버튼 레이블, UI 텍스트 전면 차단
5. 최초 `conversation-view` 감지 시 DOM 구조 자동 덤프 (`/dump-html`)
- **주의**: SDK 경로(step-probe RT-CAPTURE)는 AG Native에서 사용 불가. DOM이 유일한 콘텐츠 소스이므로 AG UI 업데이트 시 `data-testid`/`data-step-index` 속성 존재 여부 반드시 확인 필요
### [2026-04-09] [Bridge] Discord Body Content Missing Due to Step Probe Dummy Payload
- **利앹긽**: <20><><EFBFBD>洹쒕え UI 留덉씠洹몃젅<EBAA83><EFBFBD><20>썑, <20><EFBFBD>뒪肄붾뱶 <20><EFBFBD>씤 硫붿떆吏<EB9686> 蹂몃Ц<EBAA83><20><EFBFBD><EFBFBD>븷 肄붾뱶/紐낅졊<EB8285>뼱媛<EBBCB1> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>릺怨<EBA6BA> "Step #15"<22><><EFBFBD> 媛숈<E5AA9B><EC8888> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>듃留<EB9383> <20><EFBFBD><EFBFBD>맖.
- **<2A><EFBFBD>씤**: Native UI 蹂<>寃쎌쑝濡<EC919D> <20><EFBFBD>빐 DOM observer媛<72> 異붿텧<EBB6BF>븳 踰꾪듉 <20><EFBFBD><EFBFBD>듃("Always run")媛<> `http-bridge.ts` <20><EFBFBD><20><EFBFBD>쉶 諛<> bot.py<70><EFBFBD>꽌 吏<><EFA79E>뿰(defer) 泥섎━<EC848E>맖. 諛섎㈃ `step-probe.ts`<EFBFBD> `GetAllCascadeTrajectories` <20>뤃留곸쓣 <20><EFBFBD><20><EFBFBD><EFBFBD>뿉 諛쒖깮<EC9296><EFBFBD>궓 dummy pending payload (紐낅졊<EB8285><20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>`Step #XX` <20><EFBFBD><20><EFBFBD><EFBFBD>듃留<EB9383> <20><EFBFBD>븿)媛<> 遊뉗뿉 <20><EFBFBD>빐 癒쇱<E79992><EC87B1> <20><EFBFBD><20><EFBFBD><EFBFBD>릺硫댁꽌 <20><EFBFBD><20><EFBFBD>젣 肄붾뱶 <20><EFBFBD><20>젙蹂닿<E8B982><EB8BBF> 利앸컻<EC95B8>븿.
- **<2A>빐寃<EBB990>**: `step-probe.ts` <20><EFBFBD>`formatStepProbeCommand` <20><EFBFBD><20>븿<EFBFBD>닔瑜<EB8B94> 異붽<E795B0><EBB6BD><EFBFBD><EFBFBD>뿬, WAITING <20><EFBFBD><20><EFBFBD><EFBFBD>`argumentsJson` <20><EFBFBD><EFBFBD>꽣瑜<EABDA3> 吏곸젒 <20><EFBFBD><EFBFBD>븯怨<EBB8AF> `CommandLine`, `TargetFile` <20><20><EFBFBD>젣 紐낅졊<EB8285><EFBFBD><EBBCB1><EFBFBD> <20><EFBFBD><20><EFBFBD>옄/肄붾뱶瑜<EBB1B6> `command`<EFBFBD><EFBFBD><EFBFBD> `description`<EFBFBD>쑝濡<EFBFBD> <20><EFBFBD><EFBFBD><EFBFBD>뿬 釉뚮┸吏<E294B8><EFBFBD> <20>꽆湲곕룄濡<EBA384> <20>뙣移섑븿. DOM <20><EFBFBD><EC8383><EFBFBD>踰꾩쓽 遺덉븞<EB8D89><EFBFBD>꽦怨<EABDA6><>怨꾩뾾<EABEA9><20>씪愿<EC94AA><E684BF>맂 蹂몃Ц <20><EFBFBD>떖 蹂댁옣.
- **二쇱쓽**: UI <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20>쓽議댄븯<EB8C84>뒗 DOM Observer 諛⑹떇<E291B9><EB9687><EFBFBD> UI <20><EFBFBD><EFBFBD><EFBFBD>썐, <20><EFBFBD>씠肄<EC94A0> <20><EFBFBD><20><EFBFBD>뿉 痍⑥빟<E291A5>븯誘<EBB8AF><EFBFBD>, <20><EFBFBD><20><EFBFBD>씠濡쒕뱶 異붿텧<EBB6BF><ED85A7><EFBFBD> <20><EFBFBD>긽 100% <20>떊猶<EB968A><><E5AA9B><EFBFBD>븳 SDK RPC(`step-probe.ts`) <20><EFBFBD><EFBFBD>꽣瑜<EABDA3> <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>룄濡<EBA384> 援ъ꽦<D18A><EFBFBD><20>븿.
## <20>룷留<EBA3B7>
### [2026-03-23] [Extension] Cross-Project DOM Observer Leakage
- **利앹긽**: <20>떎以<EB968E> <20>썝寃<EC8D9D> 而댄벂<EB8C84><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20>봽濡쒖젥<EC9296>듃紐낆쑝濡<EC919D> <20><EFBFBD><EFBFBD>맂 VS Code<64><EFBFBD><20>꽌濡쒖쓽 `execute JavaScript` (Allow) <20><EFBFBD><20><EFBFBD>샇瑜<EC8387><>濡쒖콈嫄곕굹 <20><EFBFBD><EFBFBD><20>꽌踰꾨줈 蹂대깂.
- **<2A><EFBFBD>씤**: Extension<6F>`workbench.html`<EFBFBD><20><EFBFBD>겕由쏀듃瑜<EB9383> 二쇱엯<EC87B1><20>븣 寃곗젙濡좎쟻 <20><EFBFBD>듃瑜<EB9383> <20><EFBFBD>뱶肄붾뵫<EBB6BE><EFBFBD><EFBFBD>뜲, <20><EFBFBD>뿭 罹먯떆<EBA8AF>맂 HTML <20><EFBFBD><EFBFBD>쓣 紐⑤뱺 濡쒖뺄/<2F>썝寃<EC8D9D> <20>뿰寃곗씠 怨듭쑀<EB93AD>븯硫댁꽌 留덉<EFA78D><EB8D89>留됱뿉 <20>뿴由<EBBFB4> <20>봽濡쒖젥<EC9296><EFBFBD><20><EFBFBD>듃 踰덊샇濡<EC8387> <20><EFBFBD><EFBFBD><EFBFBD>썙吏<EC8D99>.
- **<2A>빐寃<EBB990>**: `extension.ts`<EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>떆以<EB9686>(Status Bar) `tooltip`<EFBFBD><20><EFBFBD>듃瑜<EB9383> 二쇱엯<EC87B1>븯怨<EBB8AF>, `observer-script.ts`<EFBFBD><EFBFBD>꽌 DOM 荑쇰━瑜<E29481> <20><EFBFBD><20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><EFBFBD>쓽 李<>(Window)<29><20><EFBFBD><EFBFBD><20><EFBFBD>듃瑜<EB9383> 李얠븘<EC96A0><EFBFBD>룄濡<EBA384> <20><EFBFBD>젙. `vscode.env.asExternalUri`<EFBFBD> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>듃 異⑸룎 <20><20><EFBFBD><EFBFBD>맂 二쇱냼源뚯<E6BA90><EB9AAF> 濡쒖뺄 <20><EFBFBD><EFBFBD><EFBFBD>뿉 留ㅽ븨<E385BD><EFBFBD>룄濡<EBA384><><EFA79E>썝.
- **二쇱쓽**: VS Code UI 肄붿뼱(HTML) <20>뙣移<EB99A3> <20>떆, <20><EFBFBD>윭 李<>(Window)<29><EFBFBD><20>떎以<EB968E> <20>썝寃<EC8D9D> <20><EFBFBD><20><20>솚寃<EC869A>(Scope) 遺꾨━<EABEA8>뿉 媛곷퀎<EAB3B7>븳 二쇱쓽媛<EC93BD> <20><EFBFBD><EFBFBD>븿. <20><EFBFBD><20><EFBFBD><EFBFBD><20>쓽議댄븯<EB8C84><20><EFBFBD>뱶肄붾뵫 吏<><EFA79E>뼇.
### [<5B>궇吏<EAB687>] [<5B><EFBFBD><EFBFBD>뱶] <20><><EFBFBD> <20>븳以<EBB8B3> <20><EFBFBD>
- **利앹긽**: 臾댁뾿<EB8C81><20>옒紐삳릺<EC82B3><EFBFBD>뒗媛<EB9297>
- **<2A><EFBFBD>씤**: 洹쇰낯 <20><EFBFBD>
- **<2A>빐寃<EBB990>**: <20>삱諛붾Ⅸ <20>빐寃<EBB990> 諛⑸쾿
- **二쇱쓽**: <20>옱諛<EC98B1> 諛⑹<E8AB9B><E291B9><EFBFBD> <20><EFBFBD>븳 援먰썕
```
---
### [2026-04-08] [Discord Bot] Channel Deletion Cache Desync
- **利앹긽**: 遊뉗씠 耳쒖졇 <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>꽌 Discord 梨꾨꼸(g-project-name)<29><20><EFBFBD><EFBFBD>븯硫<EBB8AF>, 遊뉗씠 <20><EFBFBD>젣瑜<ECA0A3> <20>씤吏<EC94A4><EFA79E>븯吏<EBB8AF> 紐삵븯怨<EBB8AF> <20>깉 梨꾨꼸<EABEA8><20><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>쑝硫<EC919D> 硫붿떆吏<EB9686><EFA79E>룄 利앸컻<EC95B8>븿.
- **<2A><EFBFBD>씤**: ot.py<70>쓽 self.project_channels <20><EFBFBD><EFBFBD>꼫由ъ뿉 梨꾨꼸 媛앹껜媛<EABB9C> 罹먯떆<EBA8AF><EFBFBD><20><EFBFBD>뼱, API <20>샇異<EC8387> <20><EFBFBD>씠 罹먯떆<EBA8AF>맂(<28><EFBFBD><EFBFBD>맂) 梨꾨꼸濡<EABCB8> 硫붿떆吏<EB9686><EFBFBD> 蹂대궡<EB8C80><20><EFBFBD><EFBFBD><EFBFBD>떎 404 <20><EFBFBD>윭 諛쒖깮 <20><20><EFBFBD><EFBFBD>븿.
- **<2A>빐寃<EBB990>**: 梨꾨꼸 留듯븨<EB93AF>씠 瑗ъ<E79197><D18A><EFBFBD><20><EFBFBD>뒗 **Python 遊<>(Docker 而⑦뀒<E291A6><EFBFBD>꼫)<29><20><EFBFBD><EFBFBD>옉**<2A><EFBFBD>뿬 罹먯떆瑜<EB9686> 珥덇린<EB8D87><EFBFBD>븯怨<EBB8AF> 梨꾨꼸 紐⑸줉<E291B8><20>깉濡<EAB989> 媛깆떊<EAB986>븯寃<EBB8AF> <20>븿.
- **二쇱쓽**: 梨꾨꼸 愿<>由щ뒗 罹먯떆<EBA8AF><20>쓽議댄븯湲<EBB8AF> <20>븣臾몄뿉 媛뺤젣濡<ECA0A3> Discord UI<55><EFBFBD>꽌 梨꾨꼸<EABEA8>쓣 吏<><EFA79E><EFBFBD><20><EFBFBD>뒗 諛섎뱶<EC848E>떆 遊뉗쓣 <20>옱援щ룞<D189><EFBFBD><20>븿.
### [2026-04-08] [Extension] Multiple Workspace LS Cross-Connection
- **利앹긽**: ariet-llm 李쎌뿉<EC8E8C>꽌 耳곗쑝<EAB397>굹 gravity_control<6F>쓽 諛깃렇<EAB983><EFBFBD><EFBFBD>뱶 援щ룞 以묒씤 LS<4C><20>뿰寃곕릺<EAB395><20>옄湲<EC9884> <20><EFBFBD>떊 李쎌쓽 <20><EFBFBD>샇瑜<EC8387> <20>옟吏<EC989F> 紐삵븿.
- **<2A><EFBFBD>씤**: <20><EFBFBD>윭 VS Code 李쎌쓣 <20><EFBFBD><EFBFBD><20><20><EFBFBD>뼡 李쎌뿉<EC8E8C><EFBFBD>뒗 Antigravity <20><EFBFBD><EFBFBD><20>늻瑜댁<E7919C><EB8C81> <20><EFBFBD><20><EFBFBD>슜 LS媛<53> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬. ixLSConnection()<29><20>옄湲<EC9884> 紐レ쓽 LS瑜<53> 李얠<EFA7A1><EC96A0> 紐삵븯怨<EBB8AF> fallback<63>쑝濡<EC919D> 湲곗〈<EAB397><20><20><EFBFBD><20>떎瑜<EB968E> 李쎌쓽 LS<4C><20>뿰寃곕맖.
- **<2A>빐寃<EBB990>**: <20><><EFBFBD><EFBFBD>긽 李쎌뿉<EC8E8C>꽌 Developer: Reload Window <20><EFBFBD><20>썑 **<2A><EFBFBD><EFBFBD>뱶諛붿쓽 濡쒖뺄 Antigravity 梨쀫큸 <20><EFBFBD><EFBFBD><20>븳 踰<> <20><EFBFBD>뼱** <20><EFBFBD><EFBFBD>쓽 LS <20>봽濡쒖꽭<EC9296>뒪瑜<EB92AA> <20><EFBFBD><20><EFBFBD>뿉 Gravity Bridge瑜<65> Start<72>븿.
- **二쇱쓽**: LS<4C><20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20>븡怨<EBB8A1> <20><EFBFBD><EFBFBD>옄媛<EC9884> 梨꾪똿 <20><EFBFBD><EFBFBD><20>븳 踰<> <20>겢由<EAB2A2>/<2F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>빞留<EBB99E> Spawn <20>맖.
## <20><EFBFBD> Active/Recent Issues
### [2026-04-09] [Extension] Agent UI Native Migration & Icon Text Gluing
- **利앹긽**: UI Tailwind/Native 留덉씠洹몃젅<EBAA83><EFBFBD>뀡 諛<> <20><EFBFBD>씠肄<EC94A0> <20><EFBFBD><20>썑, Discord 釉뚮┸吏<E294B8><EFBFBD> <20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬.
- **<2A><EFBFBD>씤**: <20><EFBFBD><EFBFBD>떚釉<EB969A> UI 踰꾪듉<EABEAA>쓽 `textContent` 異붿텧 <20>떆, Codicons <20><20><EFBFBD>씠肄<EC94A0> <20><EFBFBD>듃 臾몄옄<EBAA84>뿴(e.g., `<EFBFBD> Accept`)<29><20>븵遺<EBB8B5>遺꾩뿉 蹂묓빀(Gluing)<29>릺硫댁꽌, 湲곗〈<EAB397>쓽 `^` <20>빑而ㅺ<E8808C><E385BA> <20><EFBFBD>븿<EFBFBD><20>젙洹쒖떇 留ㅼ묶(`/^(?:Always\s*)?Run/i`)<29><20><EFBFBD><EFBFBD>븿.
- **<2A>빐寃<EBB990>**: `observer-script.ts`<60><20>뒪罹<EB92AA>, Sibling 踰꾪듉 <20>닔吏<EB8B94>, Webview Trigger-click <20>벑 `textContent`瑜<> 異붿텧<EBB6BF><EFBFBD>뒗 紐⑤뱺 DOM <20>씫湲<EC94AB> 援ш컙<D188>뿉 `txt.replace(/^[^a-zA-Z0-9]+/, '')` <20>쟾泥섎━瑜<E29481> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>뻾 湲고샇/<2F><EFBFBD>씠肄섏쓣 <20><EFBFBD><EFBFBD>븯寃<EBB8AF> <20>젣嫄<ECA0A3>.
- **二쇱쓽**: Native UI 而댄룷<EB8C84><EFBFBD><20>솚寃쎌뿉<EC8E8C><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>뱶肉먮쭔 <20><EFBFBD><EFBFBD><20><EFBFBD>씠肄<EC94A0>/SVG 而댄룷<EB8C84><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>듃 湲<>猷⑥엵 <20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><20>뾼寃⑺븳 <20><EFBFBD><EFBFBD>젏(`^`) <20>젙洹쒖떇<EC9296>씠 源⑥쭏 <20><20><EFBFBD>쑝誘<EC919D><EFBFBD>, <20><EFBFBD>긽 遺덊븘<EB8D8A><EFBFBD><20><EFBFBD>닔臾몄옄 <20>쟾泥섎━瑜<E29481> <20><EFBFBD><EFBFBD><EFBFBD><20>븿.
### [2026-04-09] [Extension] Agent UI Native Migration & CodeLens False Positive Filter
- **利앹긽**: UI Tailwind/Native 留덉씠洹몃젅<EBAA83><EFBFBD><20><EFBFBD><20>썑, Discord 釉뚮┸吏<E294B8><EFBFBD> <20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EC9FBE><EFBFBD> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>
- **<2A><EFBFBD>씤**: Agent <20><EFBFBD><EFBFBD><20>꺆/<2F><EFBFBD><EFBFBD>꽣 蹂몃Ц<EBAA83>뿉 吏곸젒 <20><EFBFBD>뜑留곷릺硫댁꽌, 湲곗〈 <20><EFBFBD><EFBFBD>룞 諛⑹<E8AB9B><E291B9> 濡쒖쭅(`if (b.closest('.monaco-editor'))`)<29><20><EFBFBD><20>쟾泥<EC9FBE> 踰꾪듉<EABEAA><20>룷李⑸릺<E291B8>뼱 臾댁떆<EB8C81>
- **<2A>빐寃<EBB990>**: <20>꼫臾<EABCAB> 愿묐쾾<EBAC90><EFBFBD>븳 `.monaco-editor` 諛⑹뼱瑜<EBBCB1> <20><EFBFBD><EFBFBD>븯怨<EBB8AF>, 肄붾뱶 <20>젋利<ECA08B> 怨좎쑀 而⑦뀒<E291A6><EFBFBD><EFBFBD>씤 `.codelens-decoration` <20>궡遺<EAB6A1><E981BA>씪 寃쎌슦<EC8E8C>뿉留<EBBF89> 臾댁떆<EB8C81><EFBFBD>룄濡<EBA384> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>
- **二쇱쓽**: DOM <20><EFBFBD><EC8383><EFBFBD><EFBFBD> <20><EFBFBD>꽣 議곌굔 <20><EFBFBD><20><20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>뒗 UI <20><EFBFBD><EFBFBD>씤 媛쒗렪(Native, Editor Tab <20><20>쐞移<EC909E><><EFBFBD>)<29>뿉 留ㅼ슦 痍⑥빟<E291A5>븿. 媛<><E5AA9B>옣 援ъ껜<D18A><EFBFBD><20>궡遺<EAB6A1> <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><><EFBFBD><EFBFBD> 怨좎쑀 <20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>꽣留곹븷 寃<>
### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop)
- **利앹긽**: `guitar_score` <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>뒪肄붾뱶 <20><EFBFBD><20><EFBFBD>샇瑜<EC8387> "怨꾩냽<EABEA9><EFBFBD>꽌" <20>옟吏<EC989F> 紐삵븿. (WS 60珥<30> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐蹂대떎 <20>뜑 移섎챸<EC848E><EFBFBD>쑝濡<EC919D> <20><EFBFBD>샇媛<EC8387> <20><EFBFBD>삁 媛<><EFBFBD> <20><EFBFBD>쓬)
- **<2A><EFBFBD>씤**: Extension<6F><20><EFBFBD><20><EFBFBD><EFBFBD>쓣 李얘린 <20><EFBFBD><20>샇異쒗븯<EC9297>뒗 `GetAllCascadeTrajectories` LS API媛<49> `{}`(鍮<> <20><EFBFBD>옄)濡<> <20>샇異쒕맆 <20>븣, 湲곕낯<EAB395><EFBFBD>쑝濡<EC919D> **10媛쒖쓽 <20><EFBFBD>뀡留<EB80A1> 諛섑솚<EC8491><EFBFBD><20><EFBFBD>뱶 由щ컠(Pagination Limit)**<2A>씠 嫄몃젮<EBAA83><EFBFBD>쓬. <20>씠濡<EC94A0> <20><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>릺硫<EBA6BA> <20>닔留롮<EFA78D><EBA1AE> 理쒖떊/吏꾪뻾 以<> <20><EFBFBD><EFBFBD><EFBFBD>씠 10媛<30> 紐⑸줉<E291B8><EFBFBD>꽌 諛<><E8AB9B><EFBFBD><20><EFBFBD><EFBFBD>맖. <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EB80A1><EFBFBD> <20><EFBFBD><EFBFBD><20><EFBFBD>떎怨<EB968E> <20><EFBFBD><EFBFBD>빐 媛뺤젣濡<ECA0A3> `IDLE` 紐⑤뱶<E291A4>뿉 吏꾩엯<EABEA9>븯硫<EBB8AF>, <20><EFBFBD><20><><EFBFBD>湲곗뿴(WAITING) <20>옄泥대<EFA7A3><EB8C80><><E5AF83><EFBFBD>븯吏<EBB8AF> <20>븡寃<EBB8A1> <20>맖.
- **<2A>빐寃<EBB990>** (v0.5.14): `v0.5.13`<60><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>뜕 `{ limit: 100 }`<60>씠 LS <20><EFBFBD>쓽 荑쇰━ 怨쇰<E680A8><EC87B0><EFBFBD>븯濡<EBB8AF> <20><EFBFBD>븳 VS Code UI <20>봽由ъ쭠(DoS)<29><20>쑀諛쒗븯<EC9297>뿬 濡ㅻ갚<E385BB><EFBFBD>뒗 以<> <20><EFBFBD><20><EFBFBD><20><EFBFBD>씪誘명꽣(`descending: true`)源뚯<E6BA90><EB9AAF> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>닔瑜<EB8B94> 援먯젙<EBA8AF>븿. 理쒖쥌<EC9296><EFBFBD>쑝濡<EC919D> `{ limit: 30, descending: true }`瑜<> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>떛 遺<><E981BA>븯 理쒖냼<EC9296>솕 諛<> 理쒖떊 <20><EFBFBD>뀡 理쒖긽<EC9296>떒(Index 0) 議고쉶瑜<EC89B6> <20><EFBFBD><EFBFBD>븯寃<EBB8AF> 援ы쁽<D18B>븿.
- **二쇱쓽**: LS<4C>쓽 湲곕낯 SQLite/DB <20><EFBFBD>떟 Limit 洹쒖튃<EC9296><20>쓽議댄븯<EB8C84><20>쟾泥<EC9FBE> <20><EFBFBD><EFBFBD><20>뒪罹붿쓣 <20><EFBFBD><EFBFBD><EFBFBD>뒗 濡쒖쭅<EC9296><ECAD85><EFBFBD> <20><EFBFBD><EFBFBD>뱺 Truncation <20><EFBFBD>뒋(Data Loss)瑜<> <20>쑀諛쒗븷 <20><20><EFBFBD>쓬.
### [2026-03-31] [WS] Browser API Fallback 60s Timeout (Zombie Connection)
- **利앹긽**: `guitar_score` <20>벑 紐⑤뱺 <20><EFBFBD><20>솚寃쎌뿉<EC8E8C><20>빟 60珥덈쭏<EB8D88>떎 WebSocket <20>뿰寃곗씠 <20>걡湲곌퀬 <20><EFBFBD>뿰寃곕릺<EAB395><20><EFBFBD><EFBFBD>씠 諛섎났<EC848E>릺硫<EBA6BA>(extension.log<6F>뿉 `Heartbeat timeout` 怨꾩냽 異쒕젰), 洹<> <20><EFBFBD><20><EFBFBD>뒪肄붾뱶 <20><EFBFBD><20><EFBFBD>샇瑜<EC8387> <20>넃移<EB8483>.
- **<2A><EFBFBD>씤**: Extension<6F>씠 `ws` 紐⑤뱢 濡쒕뱶 <20><EFBFBD>뙣(VS Code <20>솚寃<EC869A> <20>벑)濡<> <20><EFBFBD>빐 釉뚮씪<EB9AAE><EFBFBD><EC8AA6><EFBFBD> <20><EFBFBD>옣 `WebSocket` 媛앹껜濡<EABB9C> Fallback <20>맖. 釉뚮씪<EB9AAE><EFBFBD><EC8AA6><EFBFBD> WS<57><20>꽌踰꾩쓽 <20><EFBFBD><EFBFBD>떚釉<EB969A> ping<6E>쓣 諛쏆븘 pong<6E><20><EFBFBD><20><EFBFBD><EFBFBD>븯吏<EBB8AF><EFBFBD> JS<4A><20>씠踰ㅽ듃瑜<EB9383> <20>끂異쒗븯吏<EBB8AF> <20><EFBFBD>쓬. <20>씠濡<EC94A0> <20><EFBFBD>빐 `lastPongTime` 媛깆떊<EAB986>씠 遺덇<E981BA><EB8D87><EFBFBD><EFBFBD><EFBFBD>졇, `Date.now() - lastPongTime > 60000` 議곌굔<EAB38C>씠 臾댁“嫄<E2809C> <20>넻怨쇰릺<EC87B0>뼱 硫<>姨≫븳 <20>뿰寃곗쓣 媛뺤젣 醫낅즺<EB8285>븿 (False Positive).
- **<2A>빐寃<EBB990>** (v0.5.12):
1. `hub.py`: `{"type": "heartbeat"}` JSON 硫붿떆吏<EB9686> <20><EFBFBD><20>떆 紐낆떆<EB8286><EFBFBD>쑝濡<EC919D> `{"type": "pong"}` JSON<4F><20><EFBFBD><EFBFBD><EFBFBD>룄濡<EBA384> <20><EFBFBD>젙.
2. `ws-client.ts`: 紐낆떆<EB8286>쟻 `pong` <20><EFBFBD><EFBFBD>윭 異붽<E795B0><EBB6BD>. JSON pong 吏<><EFA79E><20>꽌踰꾧굅<EABEA7>굹 Node.js ws瑜<73> <20><EFBFBD><EFBFBD><20>븣留<EBB8A3> 60珥<30> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐 寃<>利앹쓣 嫄곗튂<EAB397>룄濡<EBA384> 議곌굔 蹂닿컯 (`forceHeartbeatTimeoutIfNoPong`).
- **二쇱쓽**: 釉뚮씪<EB9AAE><EFBFBD><EC8AA6><EFBFBD> <20>몴以<EBAAB4> WebSockets(W3C)<29>뒗 ping/pong <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>쓣 JS濡<53> <20>끂異쒗븯吏<EBB8AF> <20><EFBFBD>쓬. <20>뤃由ы븘/<2F>겕濡쒖뒪<EC9296><EFBFBD><EFBFBD>뤌 WS <20><EFBFBD><20><EFBFBD><20><20><EFBFBD>듃鍮꾪듃<EABEAA>뒗 諛섎뱶<EC848E>떆 JSON 硫붿꽭吏<EABDAD> <20><EFBFBD><EFBFBD>쓽 Application Layer Ping/Pong<6E>쑝濡<EC919D> <20><><EFBFBD><EFBFBD><EFBFBD>궡嫄곕굹, Native WS API <20>뿬遺<EBBFAC><EFBFBD> <20><EFBFBD><EFBFBD>엳 泥댄겕<EB8C84><EFBFBD><20>븿.
### [2026-03-28] [step-probe] GetCascadeTrajectorySteps UTF-8 <20><EFBFBD>윭 臾댄븳 猷⑦봽
- **利앹긽**: `guitar_score` <20>봽濡쒖젥<EC9296><EFBFBD><EFBFBD>꽌 `[STEP-PROBE] error: ...invalid UTF-8` <20><EFBFBD>윭媛<EC9CAD> 5珥덈쭏<EB8D88>떎 諛섎났<EC848E>릺硫<EBA6BA> Discord <20><EFBFBD><20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬.
- **<2A><EFBFBD>씤**: AG LS <20>꽌踰꾩뿉<EABEA9><20><EFBFBD>젙 step<65>쓽 `CortexStepEphemeralMessage.content`<60>뿉 諛붿씠<EBB6BF>꼫由<EABCAB> <20><EFBFBD><EFBFBD>꽣(<28>씠誘몄<E8AA98><EBAA84> <20>벑) <20><EFBFBD>븿 <20>넂 proto UTF-8 吏곷젹<EAB3B7>솕 500 <20><EFBFBD>윭. `catch(e)` 釉붾줉<EBB6BE><EFBFBD>꽌 `stallProbed=true`瑜<> <20><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>븘 `!ctx.stallProbed` 議곌굔<EAB38C><20><EFBFBD>긽 true <20>넂 5珥덈쭏<EB8D88><20><EFBFBD><20>슂泥<EC8A82> 臾댄븳 <20><EFBFBD><EFBFBD>룄.
- **<2A>빐寃<EBB990>** (v0.5.11): `catch` 釉붾줉<EBB6BE><EFBFBD>꽌 UTF-8 <20><EFBFBD>윭 媛먯<E5AA9B><EBA8AF> <20>떆 `stepOffset=currentCount-20`<60>쑝濡<EC919D> fallback <20>슂泥<EC8A82>. offset<65><20><EFBFBD><20>떆 `stallProbed=true` <20><EFBFBD><EFBFBD><EFBFBD>뿬 猷⑦봽 李⑤떒. `delta>0` <20>씠踰ㅽ듃 諛쒖깮 <20>떆 L433<33><EFBFBD><20><EFBFBD>룞 由ъ뀑.
- **二쇱쓽**: `stallProbed=true`<60><20>쁺援<EC81BA> Lock<63><20><EFBFBD><20><><EFBFBD> `delta>0` <20><20><EFBFBD>룞 由ъ뀑. UTF-8 <20><EFBFBD><EFBFBD>뒗 AG <20>꽌踰<EABD8C><> 臾몄젣(<28>씠誘몄<E8AA98><EBAA84>/諛붿씠<EBB6BF>꼫由<EABCAB> <20><EFBFBD><EFBFBD>꽣媛<EABDA3> ephemeral message<67><20><EFBFBD>븿)<29>씠誘<EC94A0><EFBFBD> Extension<6F><EFBFBD>꽌 graceful fallback留<6B> 泥섎━.
### [2026-03-28] [approval-handler] stepIndex 誘명솗<EBAA85><20>떆 wrong-stepIndex RPC <20>궘鍮<EAB698>
- **利앹긽**: DOM observer 寃쎈줈濡<ECA488> `terminal_command` pending <20><EFBFBD><20>썑 Discord <20><EFBFBD><20>떆 `HandleCascadeUserInteraction(stepIndex=0)` <20>넂 `"input not registered for step 0"` <20>넂 LS reconnect <20><20><EFBFBD><EFBFBD><20>넂 DOM click fallback<63>쑝濡<EC919D> <20><><EFBFBD><EFBFBD>븯. (wrong-LS<4C><53><EFBFBD> <20><EFBFBD><EFBFBD>븳 利앹긽<EC95B9><EFBFBD><20>떎瑜<EB968E> <20><EFBFBD>씤)
- **<2A><EFBFBD>씤**: `ctx.lastPendingStepIndex=-1` (step-probe媛<65> UTF-8 <20><EFBFBD>윭濡<EC9CAD> WAITING 誘멸컧吏<ECBBA7>)<29><EFBFBD><EFBFBD>룄 `Math.max(0, -1)=0`<60>쑝濡<EC919D> clamp<6D><EFBFBD>뼱 議댁옱<EB8C81>븯吏<EBB8AF> <20><EFBFBD>뒗 step 0<>뿉 RPC <20><EFBFBD>넚.
- **<2A>빐寃<EBB990>** (v0.5.11): `effectiveStepIndex = stepIndex >= 0 ? stepIndex : (lastPendingStepIndex >= 0 ? lastPendingStepIndex : -1)`. `effectiveStepIndex < 0`<60>씠硫<EC94A0> RPC 釉붾줉 <20>쟾泥<EC9FBE> skip <20>넂 DOM click 吏곹뻾 (湲곗〈怨<E38088> <20><EFBFBD><20><EFBFBD>씪, LS reconnect <20>궘鍮<EAB698> <20>젣嫄<ECA0A3>).
- **二쇱쓽**: 湲곗〈 洹쒖튃 #14(`uint32`<60><20><EFBFBD>닔 湲덉<E6B9B2><EB8D89>)<29><><EFBFBD> 異⑸룎泥섎읆 蹂댁씠<EB8C81>굹, `effectiveStepIndex=-1`<60><20>븣 RPC <20>옄泥대<EFA7A3><EB8C80> **<2A><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>쑝誘<EC919D><EFBFBD>** <20>쐞諛<EC909E> <20><EFBFBD>떂. RPC <20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>븳 stepIndex留<78> <20><EFBFBD>슜.
### [2026-03-25] [Architecture] Discord Signal Drop & Extension Freezes
- **利앹긽**: <20><EFBFBD>떆媛<EB9686> <20>옄由щ퉬<D189><ED89AC><EFBFBD> <20>썑 蹂듦<E8B982><EB93A6> <20>떆 Discord濡<64> <20><EFBFBD><20><EFBFBD>샇媛<EC8387> <20>삤吏<EC82A4> <20>븡嫄곕굹 VS Code UI媛<49> 媛꾪뿉<EABEAA>쟻/吏<><EFA79E><EFBFBD><EFBFBD>쑝濡<EC919D> 硫덉땄(Freeze).
- **<2A><EFBFBD>씤**:
1. `ws.onerror` 諛쒖깮 <20>썑 `onclose` <20><EFBFBD><20><20><EFBFBD>뿰寃<EBBFB0> 肄쒕갚 <20>샇異쒖씠 <20>씠猷⑥뼱吏<EBBCB1><EFBFBD> <20><EFBFBD>븘 臾댄븳 <20><><EFBFBD><EFBFBD> (<28><EFBFBD>떆媛<EB9686> 留덈퉬)
2. `ws-client` <20><EFBFBD>뿰寃<EBBFB0> <20><20><EFBFBD><EFBFBD>맂 200媛<30> <20>걧瑜<EAB1A7> <20>룞湲곗떇 burst <20><EFBFBD><EFBFBD><EFBFBD>뿬 Hub<75><20><EFBFBD><20><EFBFBD>븳(60媛<30>/10珥<30>)<29>뿉 嫄몃젮 <20><EFBFBD><20>쁺援<EC81BA> <20><EFBFBD><EFBFBD>
3. 濡쒖뺄 釉뚮┸吏<E294B8> `http-bridge.ts`<60>쓽 怨쇨굅 <20><EFBFBD><EFBFBD>씤 `FALSE_POSITIVE_RE` <20>젙洹쒖떇<EC9296>씠 AI 怨좎쑀 踰꾪듉(Allow, Deny, Accept) 留덉<EFA78D><EB8D89> <20><EFBFBD>꽣留곹븯<EAB3B9>뿬 Discord <20><EFBFBD><20>썝泥<EC8D9D> 李⑤떒
4. `step-probe.ts` <20>뤃留<EBA483> 猷⑦봽 <20><20>룞湲곗떇 <20><EFBFBD>씪 I/O <20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><20>봽由ъ쫰
- **<2A>빐寃<EBB990>** (v0.5.10): ws-client<6E><20><EFBFBD><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐 諛<> 50ms Paced-flush <20><EFBFBD>슜, http-bridge<67><20>젙洹쒖떇 湲곕뒫 <20><EFBFBD>솕, step-probe 鍮꾨룞湲<EBA39E> I/O <20><EFBFBD>솚 泥댁젣 <20><EFBFBD>슜, observer-script<70><20><EFBFBD><EFBFBD><20><EFBFBD>샇 臾댄븳 HTTP <20>뤃留<EBA483> 諛⑹뼱 肄붾뱶 諛섏쁺.
- **二쇱쓽**: Extension <20>궡遺<EAB6A1> 濡쒖쭅 踰꾧렇<EABEA7><EBA087><EFBFBD><EFBFBD>쑝誘<EC919D><EFBFBD> Hub(Python) 肄붾뱶<EBB6BE>뒗 嫄대뱶由ъ<E794B1><D18A> <20><EFBFBD>쓬. Hub <20><EFBFBD><20><EFBFBD><EFBFBD><EBB8B3><EFBFBD> <20><EFBFBD>긽 諛⑹뼱 湲곗젣<EAB397>씠誘<EC94A0><EFBFBD> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>쓽 Pacing<6E><20>삱諛붾Ⅸ 諛⑺뼢<E291BA>엫.
### [2026-03-24] DOM Observer /trigger-click <20><EFBFBD>뜑留<EB9C91> <20><EFBFBD><20><EFBFBD><EFBFBD>룞 諛<> False Positive <20>봽由ъ쭠
- **利앹긽**: v0.5.9 <20>뙣移<EB99A3> <20><EFBFBD>썑 肄붾뵫 <20>떆 Agent <20>솕硫댁씠 <20><EFBFBD><EFBFBD><EFBFBD><20>꽌紐<EABD8C> <20><><EFBFBD><EFBFBD>(Pending) <20><EFBFBD>깭濡<EAB9AD> 硫덉땄. <20><EFBFBD><20><EFBFBD>뒪肄붾뱶<EBB6BE><EFBFBD>꽌 `Approve` <20><20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD>븳 `Run Test`(肄붾뱶 <20>젋利<ECA08B>)瑜<> <20>겢由<EAB2A2><E794B1>븿.
- **<2A><EFBFBD>씤**: <20><EFBFBD><EFBFBD><EFBFBD><EB9383><EFBFBD> <20>젙洹쒖떇(`/^Run/i` <20>벑)<29>뿉留<EBBF89> <20>쓽議댄븯<EB8C84>뿬 `querySelectorAll`<60><20><EFBFBD><EFBFBD>븷 寃쎌슦, DOM <20>듃由ъ뿉 <20><EFBFBD>뜑留곷맂 <20>닔留롮<EFA78D><EBA1AE> VS Code <20><EFBFBD><EFBFBD>떚釉<EB969A> 肄붾뱶 <20>젋利<ECA08B> 踰꾪듉<EABEAA>쓣 Agent 踰꾪듉蹂대떎 癒쇱<E79992><EC87B1> 李얠븘踰꾨━<EABEA8>뒗 諛쒖깮 <20>쐞移<EC909E>(Context)<29><20>븳怨꾩젏.
- **<2A>빐寃<EBB990>** (v0.5.10):
1. 媛먯<E5AA9B><EBA8AF>(Scan): `isVSCodeMainWindow` 諛<> <20><EFBFBD><20><EFBFBD>뱶 `isBodyRoot` <20><EFBFBD><EFBFBD><20><EFBFBD>빐, <20><EFBFBD><EFBFBD>꽣 蹂몃Ц <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>뒗 "Run", "Approve" 媛먯<E5AA9B><EBA8AF><EFBFBD> <20>썝泥<EC8D9D> <20>젣嫄<ECA0A3> (<28>삤吏<EC82A4> <20><EFBFBD><20>궡濡<EAB6A1> <20><EFBFBD>젙).
2. <20>겢由<EAB2A2>(Trigger-click): `deepFindButtons()` <20><EFBFBD><EFBFBD>꽌 `findPanel()`(<28><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>꼸) -> <20>븣由<EBB8A3> Toasts -> Document 蹂몃Ц <20><EFBFBD>쑝濡<EC919D> <20><EFBFBD>깋 **<2A><EFBFBD><EFBFBD><EFBFBD>쐞(Priority)**瑜<> 媛뺤젣 <20><EFBFBD>슜.
- **二쇱쓽**: 踰꾪듉 <20>씠踰ㅽ듃 <20><EFBFBD><20><20><EFBFBD><EFBFBD>듃 留ㅼ묶<E385BC>뿉留<EBBF89> <20>쓽議댄븯吏<EBB8AF> 留먭퀬, 諛섎뱶<EC848E>떆 DOM <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EC909E><EFBFBD> 而⑦뀓<E291A6><EFBFBD>듃 踰붿쐞瑜<EC909E> <20>븿猿<EBB8BF> <20><EFBFBD>꽣留곹븯<EAB3B9>뿬 False Positive瑜<65> 李⑤떒<E291A4>븷 寃<>.
### [2026-03-24] DOM Observer <20><><EFBFBD> VS Code Native UI Blind Spot
- **利앹긽**: "Always Allow" 諛<> <20>씪諛<EC94AA> "Allow Alt+<2B>넻" 沅뚰븳 <20>븣由<EBB8A3> 踰꾪듉<EABEAA><20><EFBFBD>뒪肄붾뱶 沅뚰븳 <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>맖.
- **<2A><EFBFBD>씤**: VS Code <20><EFBFBD><EFBFBD>떚釉<EB969A> <20>븣由<EBB8A3><> 梨꾪똿 <20><EFBFBD><20><EFBFBD>쓽 踰꾪듉<EABEAA><EB9389><EFBFBD> `<button>` <20>깭洹<EAB9AD> <20><><EFBFBD><EFBFBD>떊 `<a role="button">`, `<vscode-button>` <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>뜲, 湲곗〈 DOM scan 濡쒖쭅<EC9296>씠 `querySelectorAll('button')`<60>쑝濡<EC919D> <20><EFBFBD>뱶肄붾뵫<EBB6BE><EFBFBD><20><EFBFBD>뱶瑜<EBB1B6> <20><EFBFBD>삁 李얠<EFA7A1><EC96A0> 紐삵븿. (異붽<E795B0><EBB6BD><EFBFBD> Always Allow <20>젙洹쒖떇 <20><EFBFBD>씫)
- **<2A>빐寃<EBB990>** (v0.5.9): DOM scan, 由ъ뒯 <20><20>벑 紐⑤뱺 <20><EFBFBD>깋 濡쒖쭅 <20><><EFBFBD><EFBFBD><EFBFBD>꽣瑜<EABDA3> `button, [role="button"], vscode-button, .monaco-text-button` <20>쑝濡<EC919D> <20>쟾硫<EC9FBE> 媛쒗렪. <20>젙洹쒖떇<EC9296>쓣 `/^(?:Always )?Allow/i`濡<> <20><EFBFBD>젙.
### [2026-03-24] Python Hub <20><><EFBFBD><><EFBFBD> 而ㅻ꽖<E385BB>뀡 諛<> UI <20>봽由ъ쭠
- **利앹긽**: `npm run` 紐낅졊<EB8285>씠 `<EFBFBD><EFBFBD><20>젙梨<ECA099>` 愿<><E684BF><20>삤瑜섎줈 <20><EFBFBD>
- **<2A><EFBFBD>씤**: PowerShell <20><EFBFBD>겕由쏀듃 <20><EFBFBD><20>젙梨낆씠 <20><EFBFBD><EFBFBD>
- **<2A>빐寃<EBB990>**: `cmd /c npm run dev` <20><EFBFBD><EFBFBD>쑝濡<EC919D> cmd瑜<64> <20><EFBFBD><20><EFBFBD>
- **二쇱쓽**: npm 愿<><E684BF>젴 紐낅졊<EB8285><ECA18A><EFBFBD> <20><EFBFBD>긽 `cmd /c` <20><EFBFBD><EFBFBD><20><EFBFBD>슜 沅뚯옣
### [2026-03-08] PowerShell curl <20><><EFBFBD> Invoke-WebRequest 異⑸룎
- **利앹긽**: `curl` 紐낅졊<EB8285><20><EFBFBD>긽怨<EAB8BD> <20>떎瑜<EB968E> <20><EFBFBD><20><EFBFBD><EFBFBD>쓣 諛섑솚
- **<2A><EFBFBD>씤**: PowerShell<6C><EFBFBD>꽌 `curl`<60><><EFBFBD> `Invoke-WebRequest`<60>쓽 蹂꾩묶
- **<2A>빐寃<EBB990>**: **`curl.exe`**瑜<> 紐낆떆<EB8286><EFBFBD>쑝濡<EC919D> <20><EFBFBD>
- **二쇱쓽**: HTTP 愿<><E684BF>젴 紐⑤뱺 紐낅졊<EB8285><EFBFBD>꽌 `curl.exe` <20><EFBFBD><20><EFBFBD>
---
## 誘명빐寃<EBB990> <20><EFBFBD>
### [2026-03-23/24] <20><EFBFBD>깮 吏<><EFA79E><EFBFBD><EFBFBD>뒗 WebSocket 醫<><EFBFBD> 而ㅻ꽖<E385BB>뀡 諛<> False Positive 媛뺤젣 <20>뿰寃<EBBFB0> <20>걡源<EAB1A1> (v0.5.5 <20>넂 0.5.8)
- **利앹긽**:
1. (v0.5.5) <20><EFBFBD>쟾 紐⑤뱶 蹂듦뎄 <20><20><EFBFBD>뿰寃곗씠 <20><EFBFBD>뼱議뚯쓬<EB9AAF><EFBFBD><20><EFBFBD><EFBFBD><20>씠瑜<EC94A0> <20>씤吏<EC94A4><EFA79E>븯吏<EBB8AF> 紐삵븯<EC82B5>뒗 醫<><EFBFBD>(Half-open) <20>냼耳<EB83BC> 諛쒖깮.
2. (v0.5.6) 醫<><EFBFBD> <20>냼耳볦쓣 <20>옟湲<EC989F> <20><EFBFBD>빐 10珥<30> <20><><EFBFBD><EFBFBD>씠癒<EC94A0>(`pongTimeoutTimer`)瑜<> <20><EFBFBD><EFBFBD><EFBFBD>굹, VS Code<64>쓽 臾닿굅<EB8BBF><20><EFBFBD>씪 寃<><E5AF83><20>떆 Event Loop媛<70> 釉붾줈<EBB6BE><EFBFBD>릺硫<EBA6BA><>姨≫븳 <20>뿰寃곗씤<EAB397><EFBFBD><20><EFBFBD><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐(False Positive) <20><EFBFBD><EFBFBD>쑝濡<EC919D> <20>뿰寃곗쓣 媛뺤젣 醫낅즺<EB8285>븿. <20>씠濡<EC94A0> <20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>뿰寃<EBBFB0> <20><EFBFBD><EFBFBD>씠(Exponential Backoff)媛<> 60珥덇퉴吏<ED89B4> <20><EFBFBD><EFBFBD>굹硫댁꽌 <20><EFBFBD><EFBFBD><20>떖媛곹븯寃<EBB8AF> 硫덉땄(Freeze).
- **<2A><EFBFBD>씤**: Node.js `ws` <20><EFBFBD>씠釉뚮윭由ъ쓽 `ws.ping()`<60><><EFBFBD> 鍮꾨룞湲<EBA39E> I/O <20><EFBFBD><EFBFBD><EFBFBD><20>걧瑜<EAB1A7> <20><><EFBFBD><EFBFBD><EFBFBD>, `setTimeout(..., 10000)` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EC8D90><EFBFBD> Event Loop 釉붾줈<EBB6BE><20><EFBFBD>젣 吏곹썑 怨㏓컮濡<ECBBAE> 留뚮즺<EB9AAE><EFBFBD>뼱 踰꾨┝. <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>겕 I/O <20><EFBFBD>떟(pong)蹂대떎 濡쒖뺄 <20><><EFBFBD><EFBFBD>씠癒멸<E79992><EBA9B8> 癒쇱<E79992><EC87B1> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20>냼耳볦쓣 二쎌엫.
- **<2A>빐寃<EBB990>** (v0.5.8 <20><EFBFBD>꽦):
- <20><EFBFBD><EFBFBD>븳 `setTimeout` 諛⑹떇 <20>룓湲<EBA393>.
- 湲곗〈<EAB397>쓽 25珥<35> 二쇨린 `setInterval` <20><EFBFBD>듃鍮꾪듃 猷⑦봽 <20>궡遺<EAB6A1><E981BA>뿉 `Date.now() - lastPongTime > 60000` (60珥<30> 珥덇낵 <20><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐) 寃<><EFBFBD> 濡쒖쭅<EC9296><20><EFBFBD>엯.
- 留뚯빟 Event Loop媛<70> <20><EFBFBD>떗 珥<><>由щ뜑<D189><EFBFBD>룄, 釉붾줈<EBB6BE><20><EFBFBD><20><20><EFBFBD>맂 I/O <20>씠踰ㅽ듃(`pong`)媛<> `setInterval` <20><><EFBFBD><EFBFBD>씠癒<EC94A0> 肄쒕갚 <20><EFBFBD><EFBFBD>뿉 癒쇱<E79992><EC87B1> 泥섎━<EC848E>릺嫄곕굹(Node.js Phase 洹쒖튃), <20><EFBFBD><EFBFBD>룄 60珥덈씪<EB8D88>뒗 踰꾪띁 <20>뜒遺꾩뿉 **False Positive 媛<><E5AA9B><EFBFBD><EFBFBD><20>썝泥<EC8D9D> 李⑤떒**<2A>븿怨<EBB8BF> <20><EFBFBD><EFBFBD>뿉 醫<><EFBFBD> <20>냼耳볦쓣 <20><EFBFBD><EFBFBD><EFBFBD>쑝濡<EC919D> <20>젣嫄고븿.
- **二쇱쓽**: Node.js<6A><20><EFBFBD><20><EFBFBD><EFBFBD>뱶 Event Loop <20>솚寃<EC869A>(<28><EFBFBD>엳 臾닿굅<EB8BBF><20>룞湲<EBA39E> <20><EFBFBD><EFBFBD><20><EFBFBD><EC98A6><EFBFBD> VS Code Extension)<29><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>겕 I/O瑜<4F> 濡쒖뺄 `setTimeout`怨<> 寃쎌<(Race)<29><EFBFBD><EFBFBD><20>꽕怨꾨뒗 <20><EFBFBD><EFBFBD><EFBFBD>쑝濡<EC919D> False Positive瑜<65> <20><EFBFBD>쓬. Timestamp(`Date.now()`) 湲곕컲 媛꾧꺽 寃<><EFBFBD>(Interval check)<29><20><EFBFBD><20><EFBFBD><EFBFBD>븿.
### [2026-03-11] rejectAgentStep / !stop <20><><EFBFBD> AG 誘몃벑濡<EBB291> 而ㅻ㎤<E385BB>뱶 + <20><EFBFBD><EFBFBD><20><EFBFBD><20>븿<EFBFBD>닔 + <20><EFBFBD><EFBFBD><20>봽由щ<E794B1>명떚釉<EB969A>
- **利앹긽**: `!stop` 紐낅졊<EB8285>씠 AI瑜<49> 硫덉텛吏<ED859B> 紐삵븿. 濡쒓렇: "No active cascade" / "no session tracked yet"
- **<2A><EFBFBD>씤**: (1) `antigravity.agent.rejectAgentStep`<60><><EFBFBD> AG 誘몃벑濡<EBB291> 而ㅻ㎤<E385BB>뱶. (2) <20><><EFBFBD>泥댄븳 `getActiveCascadeId()`<60>뒗 **<2A><EFBFBD><EFBFBD>윭(DOM) <20><EFBFBD><20>븿<EFBFBD>닔** <20><><EFBFBD> Extension host<73><EFBFBD><20><EFBFBD>긽 `undefined` 諛섑솚. (3) **v0.4.5 <20><EFBFBD><EFBFBD><20><EFBFBD>뙣**: `extension.ts`<60>쓽 `getActiveSessionId: () => activeSessionId`媛<> module-level <20><EFBFBD>듃留<EB9383> <20>봽由щ<E794B1>명떚釉뚮<E98789><EB9AAE> 李몄“ <20><><EFBFBD> step-probe媛<65> `ctx.activeSessionId`瑜<> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>룄 extension.ts<74>쓽 蹂<><E8B982><EFBFBD>뒗 遺덈<E981BA><EB8D88> (<28>봽由щ<E794B1>명떚釉<EB969A> 蹂듭궗)
- **<2A>빐寃<EBB990>** (2026-03-18 v0.4.6): `step-probe.ts`<60><EFBFBD>꽌 `getActiveSessionId()` getter <20>븿<EFBFBD>닔 export <20>넂 extension.ts closures<65><EFBFBD>꽌 `getStepProbeSessionId()` <20>샇異<EC8387>. <20><EFBFBD>젣 step-probe<62>쓽 live `ctx.activeSessionId`瑜<> 吏곸젒 <20><EFBFBD>쓬 (`ab0c116`)
- **二쇱쓽**: JS<4A><EFBFBD>꽌 **string/number<65><20>봽由щ<E794B1>명떚釉뚮씪 李몄“ <20><EFBFBD>떖 遺덇<E981BA><EB8D87>** <20><><EFBFBD> 媛앹껜 <20><EFBFBD><EFBFBD>쓣 怨듭쑀<EB93AD><EFBFBD>젮硫<ECA0AE> getter <20>븿<EFBFBD><EFBFBD>굹 媛앹껜 <20><EFBFBD><20><EFBFBD><20><EFBFBD>
- **Vikunja**: #411, #410
### [2026-03-19] browser_subagent Allow <20><><EFBFBD> <20>옒紐삳맂 RPC payload
- **利앹긽**: <20>꽌釉<EABD8C> <20><EFBFBD><EFBFBD><EFBFBD>듃 "execute JavaScript on localhost" Allow 踰꾪듉<EABEAA><20><EFBFBD><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>
- **<2A><EFBFBD>씤**: `step-probe.ts`<60><EFBFBD>꽌 `browser_subagent` toolName<6D>씠 step_type 遺꾨쪟 <20><EFBFBD>씠 raw toolName<6D>쑝濡<EC919D> <20><EFBFBD><20>넂 `approval-handler.ts`<60><EFBFBD>꽌 `runExtensionCode` 留ㅽ븨<E385BD><20><EFBFBD>븿<EFBFBD>릺吏<EBA6BA> <20><EFBFBD>븘 default `runCommand` RPC payload <20><EFBFBD><20>넂 AG媛<47> <20>옒紐삳맂 interaction type<70>쑝濡<EC919D> 臾댁떆
- **<2A>빐寃<EBB990>** (v0.5.1): `approval-handler.ts` L384<38>뿉 `browser_subagent` 異붽<E795B0><EBB6BD>, `step-probe.ts` L481/L549<34>뿉 `browser_subagent`/`open_browser_url` step_type 遺꾨쪟 異붽<E795B0><EBB6BD> (`549af6d`)
- **二쇱쓽**: <20>깉濡쒖슫 AG <20>룄援<EBA384> 異붽<E795B0><EBB6BD> <20>떆 諛섎뱶<EC848E>떆 (1) step-probe step_type 留ㅽ븨 (2) approval-handler RPC payload 留ㅽ븨 <20>뼇履<EBBC87> 紐⑤몢 <20><EFBFBD><EFBFBD><EFBFBD>
### [2026-03-21] Idle<6C>넂Resume <20><EFBFBD><20><EFBFBD><20><><EFBFBD> 3以<33> 踰꾧렇
- **利앹긽**: AG <20><EFBFBD>떆媛<EB9686> idle <20><20><EFBFBD><20>옱媛<EC98B1> <20>떆 Discord <20><EFBFBD><20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>
- **<2A><EFBFBD>씤**: (1) `ws-client.ts` `auth_fail` <20>떆 `shouldReconnect=false` <20><><EFBFBD> JWT 24h 留뚮즺 <20>떆 WS <20>쁺援<EC81BA> 醫낅즺. (2) `hub.py` `_disconnect`<60><EFBFBD><20><EFBFBD><20>뿰寃<EBBFB0> <20>떆 `pending_owners` <20><EFBFBD><20><><EFBFBD> <20><EFBFBD>뿰寃<EBBFB0> <20>썑 Discord 踰꾪듉 臾댄슚. (3) `step-probe.ts` `stallProbed=true` + `lastPendingStepIndex=N`<60>씠 WS <20><EFBFBD>뿰寃<EBBFB0> <20>떆 由ъ뀑 <20><20><20><><EFBFBD> WAITING step <20><EFBFBD><EFBFBD><20>쁺援<EC81BA> 李⑤떒
- **<2A>빐寃<EBB990>** (v0.5.2): (1) `auth_fail` <20>넂 `registrationCode` <20><EFBFBD><EFBFBD>룄. (2) `pending_owners` orphan 留덉빱濡<EBB9B1> 蹂댁〈+<2B><EFBFBD><EFBFBD>떦. (3) `resetPendingStateForReconnect()` + `onConnected`<60><EFBFBD><20>샇異<EC8387>
- **二쇱쓽**: WS `onConnected`<60><EFBFBD>꽌 諛섎뱶<EC848E>떆 step-probe <20><EFBFBD>깭 由ъ뀑 <20><EFBFBD>닔. `stallProbed`/`lastPendingStepIndex`<60>뒗 TTL <20><EFBFBD><20>쁺援<EC81BA><>
---
> [!NOTE]
> v0.4.5 <20><EFBFBD><20><EFBFBD>빆(Hub pending_owners, diff_review WS, auto_approve <20>씠以묒벐湲<EBB290>, WS dual-write, ApprovalView fallback)<29><><EFBFBD>
> 肄붾뱶 <20><EFBFBD><20>셿猷뚮맖. E2E <20><EFBFBD>빀 寃<>利앹<EFA79D><EC95B9> Vikunja #410<31><EFBFBD>꽌 異붿쟻 以<>.
### [2026-03-21] stepIndex=-1 <20><><EFBFBD> AG proto uint32 <20><EFBFBD>
- **利앹긽**: DOM observer媛<72> Allow 踰꾪듉 媛먯<E5AA9B><EBA8AF> <20>넂 Discord <20><EFBFBD><20>넂 RPC `HandleCascadeUserInteraction` 400 <20><EFBFBD>
- **<2A><EFBFBD>씤**: DOM observer 寃쎈줈<EC8E88>뒗 step index瑜<78> 紐⑤쫫 <20>넂 `stepIndex=-1` <20><EFBFBD><20>넂 AG proto `uint32` <20><EFBFBD><EFBFBD><20><EFBFBD>닔 遺덇<E981BA><EB8D87>
- **<2A>빐寃<EBB990>**: `Math.max(0, ...)` 濡<> clamp. `permission` type <20>넂 `runExtensionCode.confirm` 留ㅽ븨 異붽<E795B0><EBB6BD> (v0.5.4)
- **二쇱쓽**: DOM observer 寃쎈줈<EC8E88>쓽 step_type<70><65><EFBFBD> <20><EFBFBD>긽 `stepIndex=-1`<60><20><20><EFBFBD>쑝誘<EC919D><EFBFBD> proto <20><EFBFBD><20><20><EFBFBD>닔 蹂댁옣 <20><EFBFBD>
### [2026-03-21] reviewAbsoluteUris <20><><EFBFBD> latestNotifyUserStep <20><EFBFBD>뱶紐<EBB1B6> 遺덉씪移<EC94AA>
- **利앹긽**: `notify_user`<60>쓽 PathsToReview <20><EFBFBD>씪 由대젅<EB8C80>씠媛<EC94A0> <20>븳 踰덈룄 <20><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>
- **<2A><EFBFBD>씤**: AG <20><EFBFBD><20><EFBFBD>뱶紐<EBB1B6> `reviewAbsoluteUris` vs 肄붾뱶 `pathsToReview`/`paths_to_review`/`filePaths`
- **<2A>빐寃<EBB990>**: `reviewAbsoluteUris` 瑜<><> 踰덉㎏ <20>썑蹂대줈 異붽<E795B0><EBB6BD> (v0.5.3)
- **二쇱쓽**: AG RPC <20><EFBFBD>뱶紐낆<EFA78F><EB8286> extension.log `[NOTIFY-STEP] keys=` 濡<> <20><EFBFBD>씤 媛<><E5AA9B>뒫. 異붿륫 湲덉<E6B9B2><EB8D89>
### [2026-03-21] <20><EFBFBD><20><EFBFBD><20><><EFBFBD><> WAITING 媛먯<E5AA9B><EBA8AF> 20-25s 吏<><EFA79E>
- **利앹긽**: <20><20><><EFBFBD><EFBFBD><20><EFBFBD><20>썑 泥<> run_command <20><EFBFBD><EFBFBD>씠 Discord<72><20><20>삤怨<EC82A4> AG<41><EFBFBD>꽌 吏곸젒 <20><EFBFBD><EFBFBD><EFBFBD><20>븿
- **<2A><EFBFBD>씤**: `lastModTime=''` 由ъ뀑 <20>넂 `modTimeChanged=true` <20>넂 THINKING 遺꾧린 諛섎났 <20>넂 probe 15-25s 吏<><EFA79E>
- **<2A>빐寃<EBB990>**: `lastModTime=currentModTime` + `return` <20>젣嫄<ECA0A3> + 利됱떆 probe 媛뺤젣 + <20>쉶洹<EC89B6><><E5AA9B>뱶 異붽<E795B0><EBB6BD> (v0.5.3)
- **二쇱쓽**: <20><EFBFBD><20><EFBFBD><20>떆 `wasRunning`/`pendingModifiedFiles` 由ъ뀑 <20><EFBFBD>닔 (<28><EFBFBD><20><EFBFBD><20><EFBFBD>뿬臾쇰줈 false diff_review 諛⑹<E8AB9B><E291B9>)
---
## <20><EFBFBD><20><EFBFBD>뾽 洹쒖튃 (怨쇨굅 <20><EFBFBD><EFBFBD><EFBFBD>꽌 諛섎났<EC848E><20><EFBFBD>꽩)
> <20><EFBFBD><EFBFBD>뒗 怨쇨굅 <20><EFBFBD><EFBFBD><EFBFBD>꽌 諛섎났<EC848E><EFBFBD>쑝濡<EC919D> <20><EFBFBD><EAB5B9><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>쓣 洹쒖튃<EC9296>쑝濡<EC919D> <20>젙由ы븳 寃껋엯<EABB8B><EFBFBD>떎.
| # | 洹쒖튃 | 愿<><E684BF><20><EFBFBD>뒋 (archive 李몄“) |
|---|------|--------------------------|
| 1 | **Hub WS<57><53><EFBFBD> file bridge<67><20><EFBFBD>샇 諛고<E8AB9B><EAB3A0><EFBFBD>쟻** <20><><EFBFBD> `if hub: ws + return` / `else: file` | WS dual-write, _auto_approve <20>씠以<EC94A0> <20>벐湲<EBB290> |
| 2 | **WS 寃쎈줈 異붽<E795B0><EBB6BD> <20>떆 file-bridge<67>쓽 紐⑤뱺 遺꾧린瑜<EBA6B0> <20><EFBFBD>똿** | diff_review WS regression |
| 3 | **AG RPC `{}` <20><EFBFBD><EFBFBD><EB969F><EFBFBD> <20><EFBFBD>뙣濡<EB99A3> 媛꾩<** <20><><EFBFBD> 硫붿꽌<EBB6BF>뱶紐<EBB1B6> <20><><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>씠 `{}` 諛섑솚 | AcknowledgeCascadeCodeEdit |
| 4 | **ResolveOutstandingSteps<70>뒗 CANCEL <20><EFBFBD>옉** <20><><EFBFBD> <20><EFBFBD><EFBFBD><20><EFBFBD><ECA085><EFBFBD> <20><EFBFBD>슜 湲덉<E6B9B2><EB8D89> | Step probe reject |
| 5 | **Extension 肄붾뱶 <20><EFBFBD><20>썑 諛섎뱶<EC848E>떆 VSIX 鍮뚮뱶 + AG <20><><EFBFBD> <20><EFBFBD><EFBFBD>옉** | Extension 踰꾩쟾 誘몃같<EBAA83>룷 |
| 6 | **HTML <20>뙣移<EB99A3><><EFBFBD> <20>떆 V8 CachedData <20><EFBFBD><20><EFBFBD>닔** | V8 CachedData, CSP |
| 7 | **`bridge/pending/` 議곗옉 <20>떆 諛섎뱶<EC848E>떆 `project_name` + `conversation_id` <20><EFBFBD>꽣** | <20>겕濡쒖뒪 <20>봽濡쒖젥<EC9296>듃 DEDUP MERGE |
| 8 | **`processResponseFile` <20><EFBFBD>깭 由ъ뀑<D18A><EB8091><EFBFBD> `sawRunningAfterPending=true`留<>** | processResponseFile 臾댄븳 猷⑦봽 |
| 9 | **fs.watch Windows 遺덉븞<EB8D89><20><><EFBFBD> 諛섎뱶<EC848E>떆 polling fallback 蹂묓뻾** | fs.watch silent fail |
| 10 | **diff_review<65>뒗 VS Code 而ㅻ㎤<E385BB>뱶留<EBB1B6> <20><EFBFBD>슚** <20><><EFBFBD> RPC 3媛<33> <20><EFBFBD>왂 紐⑤몢 <20><EFBFBD><20><EFBFBD>젙 | diff_review RPC dead-end |
| 11 | **HttpBridgeContext<78><20>봽由щ<E794B1>명떚釉<EB969A> by-value 蹂듭궗 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> 蹂꾨룄 媛앹껜 <20><EFBFBD><20>떆 getter <20><EFBFBD>슜 | HttpBridgeContext stale primitive |
| 12 | **<2A>깉 AG <20>룄援<EBA384> 異붽<E795B0><EBB6BD> <20>떆 step-probe step_type 留ㅽ븨 + approval-handler RPC payload 留ㅽ븨 <20>뼇履<EBBC87> <20><EFBFBD>닔** | browser_subagent Allow |
| 13 | **WS `onConnected`<60><EFBFBD>꽌 step-probe <20><EFBFBD>깭 由ъ뀑 <20><EFBFBD>닔** <20><><EFBFBD> `stallProbed`/`lastPendingStepIndex`<60>뒗 TTL <20><EFBFBD><20>쁺援<EC81BA><> | Idle<6C>넂Resume <20><EFBFBD><20><EFBFBD>떎 |
| 14 | **AG proto `uint32` <20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>떖 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> `stepIndex` <20><EFBFBD><EBB291><EFBFBD> `Math.max(0, ...)` <20><EFBFBD>닔 | stepIndex=-1 RPC 400 |
| 15 | **RPC "input not registered" = wrong-LS <20>뿰寃<EBBFB0>** <20><><EFBFBD> `fixLSConnection()` <20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>닔, `lines.length<=1` 議곌린醫낅즺 湲덉<E6B9B2><EB8D89> | Deriva wrong-LS (v0.5.5) |
| 16 | **<2A><EFBFBD><EFBFBD><EFBFBD>뀡(Bridge)<29><><EFBFBD> <20><EFBFBD><EFBFBD>쟻 鍮꾩쫰<EABEA9><EFBFBD><20><EFBFBD><20><EFBFBD><ECA085><EFBFBD> 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> `SafeToAutoRun` <20><EFBFBD>쓽 議곌굔 釉뚮옖移<EC9896> 遺꾧린<EABEA7>뒗 紐⑤몢 遊뉗쑝濡<EC919D> <20><EFBFBD>엫 (Agnostic Bridge) | SafeToAutoRun Deadlock (v0.5.15) |
| 17 | **package.json 鍮뚮뱶 <20><EFBFBD>겕由쏀듃 媛뺤젣** <20><><EFBFBD> `vscode:prepublish` 異붽<E795B0><EBB6BD><EFBFBD> <20><EFBFBD><EAB68A><EFBFBD> <20><EFBFBD>뒪 諛고룷 <20>썝泥<EC8D9D> 李⑤떒 | VSIX v0.5.15 鍮뚮뱶 <20><EFBFBD>씫 |
| 18 | **<2A>룞湲곗떇 `cp.execSync` <20><EFBFBD>슜 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> Windows <20>솚寃쎌뿉<EC8E8C>꽌 硫붿씤 <20>씠踰ㅽ듃猷⑦봽 <20>봽由ъ쭠 諛<> WS heartbeat <20><EFBFBD><20>쑀諛<EC9180> | detectProjectName <20>봽由ъ쭠 |
### [2026-04-09] [Bot/Extension] Discord Signal Relay Failure & Empty Body
- **利앹긽**: <20><EFBFBD>뒪肄붾뱶 遊뉗<E9818A><EB8997> '<27><EFBFBD><20><EFBFBD><EFBFBD>맖'<27><20><EFBFBD>슦吏<EC8AA6><EFBFBD> <20><EFBFBD>젣 肄붾뱶 蹂몃Ц<EBAA83><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20>븡怨<EBB8A1>, 梨꾨꼸<EABEA8>뿉 吏꾩쭨 梨꾪똿 硫붿떆吏<EB9686><EFA79E><20>븣由쇱씠 <20><EFBFBD><20><20><EFBFBD>뿉 諛<><E8AB9B><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬.
- **<2A><EFBFBD>씤**: 1) observer-script.ts<74><EFBFBD>꽌 踰꾪듉 <20><EFBFBD><EFBFBD>듃 留ㅼ묶 <20>떆 Run <20><EFBFBD><EFBFBD>쓽 寃쎄퀎(\b) 泥섎━瑜<E29481> <20>븯吏<EBB8AF> <20><EFBFBD>븘 VS Code <20><EFBFBD><EFBFBD>쓽 'Running 1 command'瑜<><>濡쒖콈<EC9296>뼱 PENDING <20><EFBFBD>뙵 臾댄븳 <20><EFBFBD>꽦. 2) bot.py<70><EFBFBD><20><EFBFBD><20><EFBFBD>씤 Embed <20><EFBFBD><20>떆 req.description<6F>쓣 洹몃━吏<E29481> <20>븡怨<EBB8A1> 踰꾪듉 <20><EFBFBD><EFBFBD>듃(req.command)留<> <20><EFBFBD>떆. 3) step-probe.ts<74><EFBFBD><20><EFBFBD>뀡 援먯껜 <20>떆 理쒓렐 <20>븣由<EBB8A3> <20><EFBFBD><EFBFBD>뒪 珥덇린<EB8D87>솕瑜<EC8695> <20>옒紐삵븯<EC82B5><20><EFBFBD><EFBFBD>쓽 泥<> 硫붿떆吏<EB9686><EFBFBD> 臾댁“嫄<E2809C> <20>뱶濡<EBB1B6>.
- **<2A>빐寃<EBB990>**: DOM 媛먯<E5AA9B><EBA8AF> <20>젙洹쒖떇<EC9296>뿉 \b 媛뺤젣 遺<><E981BA>뿬 (/Run\b/), bot.py<70>쓽 Auto-Approve 履<> Embed 蹂몃Ц<EBAA83>뿉 req.description <20><EFBFBD>뜑留<EB9C91> 異붽<E795B0><EBB6BD>, step-probe.ts<74><EFBFBD>꽌 session init <20>떆 index瑜<78> -1濡<31> 由ъ뀑.
- **二쇱쓽**: Native UI <20><EFBFBD><EFBFBD>듃 媛먯<E5AA9B><EBA8AF> <20><20><EFBFBD>뼱 寃쎄퀎(\b)源뚯<E6BA90><EB9AAF><>利앺빐<EC95BA>빞 False Positive瑜<65> 留됱쓣 <20><20><EFBFBD>쑝硫<EC919D>, Auto-Approve<76>뒗 諛섎뱶<EC848E>떆 蹂몃Ц<EBAA83><20>끂異쒗빐<EC9297><20>븿.
### [2026-04-10] [Extension] AI Response Content Missing (Nested PlannerResponse)
- **利앹긽**: <20><EFBFBD>뒪肄붾뱶 梨꾪똿諛⑹뿉 Agent<6E><20><EFBFBD><EFBFBD><20><EFBFBD>떟(AI <20><EFBFBD>떟)<29><20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬.
- **<2A><EFBFBD>씤**: GetCascadeTrajectorySteps媛<73> 諛섑솚<EC8491><EFBFBD>뒗 plannerResponse媛<65> <20>봽濡쒗넗肄<EB8497> 諛⑹떇<E291B9><20><EFBFBD>씪 理쒖긽<EC9296>떒(s.plannerResponse)<29><20><EFBFBD>땶 s.step.plannerResponse<73>뿉 以묒꺽<EBAC92><EFBFBD><20><EFBFBD><EFBFBD><20><20><EFBFBD>쓬. 湲곗〈 <20><EFBFBD><EFBFBD><20><EFBFBD>뱶肄붾뵫<EBB6BE><20><EFBFBD>뱶 諛<> <20><EFBFBD>옯 援ъ“留<E2809C> 議고쉶<EAB3A0><EFBFBD><20><EFBFBD><EFBFBD>쓣 踰꾨┝.
- **二쇱쓽**: AG RPC <20><EFBFBD>뱶紐<EBB1B6> 援ъ“ 異붿륫 湲덉<E6B9B2><EB8D89>. <20><EFBFBD><20><20><EFBFBD>뱶諛뺤뒪濡<EB92AA> <20>몢 媛<><EFBFBD> 援ъ“(Flat, Nested) 紐⑤몢 紐⑦궧<E291A6><EFBFBD>뿬 吏곸젒 <20><EFBFBD><20><EFBFBD>씤.
### [2026-04-10] [Extension] Fast Execution `<5s` Response Capture Missed (IDLE-to-IDLE)
- **利앹긽**: <20><EFBFBD>뒪肄붾뱶濡<EBB1B6> <20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬. `[RT-CAPTURE]`, `[RESPONSE-CAPTURE]` 濡쒓렇 紐⑤몢 <20><EFBFBD><EC9FBE><EFBFBD> <20>궓吏<EAB693> <20><EFBFBD>쓬.
- **<2A><EFBFBD>씤**: AI <20><EFBFBD><EFBFBD><EFBFBD>굹 肄붾뵫 <20><EFBFBD><EFBFBD>씠 5珥<35>(<28>뤃留<EBA483> 二쇨린) 誘몃쭔<EBAA83>쑝濡<EC919D> 留ㅼ슦 鍮좊<E285A4> <20><EFBFBD>굹硫<EAB5B9>, <20><EFBFBD><EFBFBD>씠 `IDLE -> IDLE` <20><EFBFBD>깭留<EAB9AD><>李고븯硫<EBB8AF> `wasRunning` <20><EFBFBD>옒洹멸<E6B4B9><EBA9B8> `false`濡<> <20>쑀吏<EC9180><EFA79E>맖. 湲곗〈 `[RESPONSE-CAPTURE]` 議곌굔<EAB38C>떇(`wasRunning && !isRunning && currentCount > ...`)<29>씠 `wasRunning=false`濡<> <20><EFBFBD>빐 釉붾줉<EBB6BE><EFBFBD>뼱 罹≪쿂 <20>옄泥대<EFA7A3><EB8C80> <20><EFBFBD><EFBFBD>엳 嫄대꼫<EB8C80>쎇寃<EC8E87> <20>맖.
- **<2A>빐寃<EBB990>**: `wasRunning` 寃<>利앹쓣 <20><EFBFBD><EFBFBD>븯怨<EBB8AF> `!isRunning && currentCount > lastResponseCaptureStep` 議곌굔<EAB38C>쑝濡<EC919D> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>맂 step<65><20><EFBFBD><20>븣 臾댁“嫄<E2809C> 罹≪쿂<E289AA><EFBFBD>룄濡<EBA384><><EFBFBD>. 異붽<E795B0><EBB6BD><EFBFBD> <20><EFBFBD><EFBFBD>맂 `[RESPONSE-CAPTURE]` <20><20><EFBFBD>뱶肄붾뵫 <20><EFBFBD>꽌瑜<EABD8C> `extractPlannerText`濡<> <20><EFBFBD><EFBFBD><20><EFBFBD>슜.
- **二쇱쓽**: <20>뤃留<EBA483> 諛⑹떇<E291B9><EFBFBD><EFBFBD><20><EFBFBD>깭(RUNNING->IDLE) <20><EFBFBD>씠瑜<EC94A0> <20><EFBFBD><EFBFBD><20><20><EFBFBD>쑝誘<EC919D><EFBFBD>, Step Count(<28><EFBFBD><EFBFBD><20>쟾吏<EC9FBE>)<29><EFBFBD>뒗 100% <20>떊猶<EB968A><><E5AA9B><EFBFBD>븳 留덉빱瑜<EBB9B1> <20><EFBFBD><20><20><EFBFBD><20>뿬遺<EBBFAC><EFBFBD> 媛먯<E5AA9B><EBA8AF><EFBFBD><EFBFBD><20>븿.
### [2026-04-10] [Bot] chat_snapshot_scanner 臾댄븳 Abort 諛<> <20><EFBFBD><20>쟻泥<EC9FBB> (Exception <20><EFBFBD>씫)
- **利앹긽**: 遊뉗씠 <20><EFBFBD>뒪肄붾뱶濡<EBB1B6> AI <20>떟蹂<EB969F>(梨꾪똿 <20><EFBFBD><EFBFBD>꺑)<29><20><EFBFBD><EC9FBE><EFBFBD> <20><EFBFBD><EFBFBD>븯吏<EBB8AF> 紐삵븯怨<EBB8AF> <20><EFBFBD>씠 嫄몃┝. ridge/chat_snapshots/<2F>뿉 泥섎━<EC848E>릺吏<EBA6BA> <20><EFBFBD><EBB8A1><EFBFBD> JSON <20><EFBFBD><EFBFBD><20><EFBFBD>떗 媛<> <20>쟻泥대맖.
- **<2A><EFBFBD>씤**: ot.py<70>쓽 chat_snapshot_scanner<65><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD><20><20>궡遺<EAB6A1><E981BA> .unlink() 怨쇱젙<EC87B1><EFBFBD>꽌 諛쒖깮<EC9296><EFBFBD><20><EFBFBD><EFBFBD>굹 discord.Embed <20><EFBFBD><20><EFBFBD><20><EFBFBD>쓣 猷⑦봽 <20><EFBFBD><EFBFBD><20><EFBFBD>븘二쇱<E4BA8C><EC87B1> 紐삵븿. 泥<> <20><EFBFBD><20><EFBFBD>씪(poison pill)<29>쓣 留뚮굹<EB9AAE><20>닚媛<EB8B9A> 猷⑦봽 <20>쟾泥닿<EFA7A3><EB8BBF> <20><EFBFBD><EFBFBD><EFBFBD><20>뮘履쎌쓽 <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>엳 泥섎━<EC848E>릺吏<EBA6BA> <20>븡怨<EBB8A1> <20><EFBFBD><20><20>뒪耳<EB92AA>以꾩뿉<EABEA9><20><EFBFBD>떆 泥<> <20><EFBFBD><EFBFBD>뿉 留됲옒.
- **<2A>빐寃<EBB990>**: 猷⑦봽 <20>궡遺<EAB6A1><E981BA>뿉 except Exception<6F>쓣 異붽<E795B0><EBB6BD><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>쇅瑜<EC8785> <20><EFBFBD>븘 諛⑹뼱. <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EC94AA><EFBFBD> glob<6F><EFBFBD>꽌 諛섎났 <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20>븡寃<EBB8A1> .json.failed濡<64> <20><EFBFBD>쉶(rename)<29>떆耳<EB9686> <20>걧瑜<EAB1A7> 鍮꾩썙以<EC8D99>.
- **二쇱쓽**: <20>뤃留<EBA483>/<2F>뒪罹먮꼫 or 猷⑦봽 <20>궡遺<EAB6A1><E981BA><EFBFBD><EFBFBD>뒗 媛쒕퀎 <20><EFBFBD><EFBFBD><20><EFBFBD><20>떒怨꾩뿉<EABEA9>꽌 諛쒖깮 媛<><E5AA9B><EFBFBD>븳 紐⑤뱺 <20><EFBFBD><20><EFBFBD><EFBFBD><20><><EFBFBD><EFBFBD>븳 Defensive Catch 諛<> Continue(<28><EFBFBD>쉶) 濡쒖쭅<EC9296><20><EFBFBD><EFBFBD>엫.
### [2026-04-10] [Extension] GetAllCascadeTrajectories 10-Item Hard Limit Bypass (Signal Drop)
- **증상**: 기존에 작성했던 { limit: 30 } 파라미터가 LS 백엔드에서 무시되어 최신 세션이 10개 제한에 걸려 잘려나감. (Discord로 메시지 단 한 글자도 안 넘어옴).
- **원인**: GetAllCascadeTrajectories는 구조적으로 pagination 옵션을 무시하거나 강제 10 제한이 걸림.
- **해결**: step-probe.ts에서 기본 GetAllCascadeTrajectories와 더불어 모든 트래젝토리를 덤프하는 GetDiagnostics API를 병행 호출하고 머지하여 최신 Session ID를 놓치지 않고 추출하게 함.
- **주의**: LS Backend에서 정의한 RPC의 한계상 Argument 조작으로 제한을 회피할 수 없으므로, 향후 GetDiagnostics 등 백도어 데이터를 활용할 것.
### [2026-04-10] [Probe Logging] <20><><EFBFBD> AI<41><EFBFBD><20><EFBFBD><EFBFBD>듃 & WAITING <20><EFBFBD><20><EFBFBD><20><EFBFBD>씫 踰꾧렇
- **利앹긽**: 援됱옣<EB90B1>엳 鍮좊Ⅸ AI <20><EFBFBD>떟(<28><EFBFBD>뒗 利됯컖<EB90AF><EFBFBD><20><20>샇異<EC8387>) <20>떆 `step-probe.ts`媛<> 硫붿떆吏<EB9686><EFA79E><EFBFBD><EFBFBD> <20><EFBFBD><20><EFBFBD><EFBFBD>뼹濡쒓렇瑜<EBA087> 紐⑤몢 Discord濡<64> 由대젅<EB8C80><EFBFBD>븯吏<EBB8AF> 紐삵븿.
- **<2A><EFBFBD>씤**: <20><EFBFBD>떆媛<EB9686> <20><EFBFBD><EFBFBD>듃 罹≪쿂(`delta > 0`) 議곌굔<EAB38C>뿉 `isRunning &&`<60>씠 嫄몃젮<EBAA83><EFBFBD>뼱, <20><EFBFBD>깭媛<EAB9AD> `WAITING`<60><EFBFBD>굹 `IDLE`濡<> 利됱떆 <20><EFBFBD>뼱媛<EBBCB1><EFBFBD> <20><EFBFBD><EFBFBD>듃瑜<EB9383> 罹≪쿂<E289AA><EFBFBD>뒗 猷⑦떞<E291A6><20>쟾遺<EC9FBE> <20><EFBFBD><EFBFBD>맖. <20><EFBFBD><20><20>닚媛<EB8B9A> `isStall` 議곌굔<EAB38C><20><><EFBFBD><EFBFBD> <20><EFBFBD>븘 `WAITING` <20><EFBFBD><EFBFBD><EFBFBD>룄 利앸컻<EC95B8>븿.
- **<2A>빐寃<EBB990>**: <20><EFBFBD>떆媛<EB9686> 罹≪쿂 濡쒖쭅<EC9296><EFBFBD>꽌 `isRunning &&` 議곌굔<EAB38C><20>젣嫄고븯怨<EBB8AF>, `delta > 0`<60><20>븣 異붽<E795B0><EBB6BD><EFBFBD>맂 理쒖떊 <20><EFBFBD><EFBFBD><20>뒪罹뷀븯硫댁꽌 `PLANNER_RESPONSE`<60><><EFBFBD> `WAITING` <20><EFBFBD><EFBFBD>쓣 紐⑤몢 泥섎━<EC848E><EFBFBD>룄濡<EBA384> <20><EFBFBD><EFBFBD>븿.
- **二쇱쓽**: LS Backend 10媛<30> Session <20><EFBFBD>븳 踰꾧렇媛<EBA087> <20><EFBFBD>뼱, <20>떎瑜<EB968E> 李쎌뿉<EC8E8C><20><EFBFBD>룞 梨꾪똿(`1fbca84c`)<29>씠 IDLE濡<45> <20><EFBFBD><EFBFBD><EFBFBD>쑝硫<EC919D> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>뀡怨<EB80A1> <20>뿷媛덈┫ <20><20><EFBFBD><EFBFBD>굹, <20>씠 踰꾧렇<EABEA7>뒗 polling <20><><EFBFBD><EFBFBD>씠諛<EC94A0> 臾몄젣<EBAA84><ECA0A3><EFBFBD><EFBFBD>쓬.
### [2026-04-10] [Extension] AI Response Missing for New Sessions (Session Tracking Failure)
- **利앹긽**: <20>깉濡쒖슫 <20><><EFBFBD><EFBFBD>솕(Session) <20><EFBFBD><20>떆 泥<> AI <20><EFBFBD><20><EFBFBD><EFBFBD>듃媛<EB9383> <20><EFBFBD>뒪肄붾뱶<EBB6BE><20><EFBFBD><EC9FBE><EFBFBD> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD><20><EFBFBD>긽.
- **<2A><EFBFBD>씤**: 諛깆뿏<EAB986><EFBFBD>쓽 `GetAllCascadeTrajectories`媛<> 10媛<30> <20><EFBFBD>뀡留<EB80A1> 諛섑솚<EC8491><EFBFBD><20><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>맖. <20>씠瑜<EC94A0> 蹂댁셿<EB8C81>븯湲<EBB8AF> <20><EFBFBD>빐 `brain/` <20><EFBFBD><EFBFBD>넗由щ<E794B1><D189> <20>뒪罹뷀븯<EBB780>뒗 Fallback 濡쒖쭅<EC9296><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>굹, <20>떊洹<EB968A> <20><EFBFBD><EFBFBD>쓽 泥<> <20>떒怨꾩뿉<EABEA9>꽌 `GetCascadeTrajectorySteps`(stepOffset: 0) <20>샇異<EC8387> <20><20>궡遺<EAB6A1> <20><EFBFBD>떟(UTF-8 <20><EFBFBD><20>벑) <20><EFBFBD>윭濡<EC9CAD> <20><EFBFBD>빐 Exception<6F>씠 諛쒖깮, `trajectorySummaries`<60><20><EFBFBD><EFBFBD><20><EFBFBD><20>벑濡앸릺吏<EBA6BA> <20><EFBFBD>쓬. <20><EFBFBD><EFBFBD>씠 異붿쟻<EBB6BF>릺吏<EBA6BA> <20><EFBFBD><EFBFBD>땲 `delta > 0` 湲곕컲<EAB395><20><EFBFBD>떟 罹≪쿂媛<ECBF82> 諛쒖깮<EC9296>븯吏<EBB8AF> <20><EFBFBD>쓬.
- **<2A>빐寃<EBB990>**: `step-probe.ts`<60>쓽 Fallback 2 `catch` 釉붾줉<EBB6BE><EFBFBD><20><EFBFBD>윭媛<EC9CAD> 諛쒖깮<EC9296><EFBFBD><EFBFBD><EFBFBD>룄 媛뺤젣濡<ECA0A3> `stepCount: 1`濡<> <20><EFBFBD><EFBFBD><20>벑濡앺븯<EC95BA>룄濡<EBA384> <20>뙣移섑븯<EC8491><20><EFBFBD><20><EFBFBD><20><EFBFBD>떎 諛⑹<E8AB9B><E291B9>.
- **二쇱쓽**: API <20>샇異<EC8387> <20><EFBFBD>뙣瑜<EB99A3> 議곗슜<EAB397>엳 `catch`濡<> <20>꽆湲곕㈃ <20>쟾泥<EC9FBE> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>씤(<28>뿬湲곗꽌<EAB397><20><EFBFBD><20>뤃留<EBA483>)<29><20><EFBFBD><20><EFBFBD><EFBFBD>꽣瑜<EABDA3> <20><EFBFBD><EFBFBD>엳 臾댁떆<EB8C81>븯寃<EBB8AF> <20><EFBFBD>뒗 移섎챸<EC848E>쟻 踰꾧렇媛<EBA087> 諛쒖깮<EC9296>븿. <20><EFBFBD><20><EFBFBD><20>꽕怨<EABD95> <20>떆 湲곕낯媛<EB82AF> 蹂듭썝(Fallback State) <20><EFBFBD><20><EFBFBD>닔.
### [2026-04-10] [Extension] Trigger-Click False Positives & Button Matching Failure
- **利앹긽**: <20><EFBFBD>뒪肄붾뱶<EBB6BE><EFBFBD><20><EFBFBD>씤(Approve)<29><20>늻瑜대㈃, <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><20>봽濡쒓렇<EC9293><EFBFBD><20>븣留욎<EFA78D><EC9A8E> 踰꾪듉(<28>삁: `Always run`)<29><20>늻瑜댁<E7919C><EB8C81> 紐삵븯嫄곕굹, <20><EFBFBD><EFBFBD>븳 踰꾪듉(<28>삁: <20><EFBFBD><EFBFBD>쓽 `Running1 command`)<29><20><EFBFBD>윭踰꾨젮 <20><EFBFBD><20><EFBFBD>씤 泥섎━媛<E29481> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>긽.
- **<2A><EFBFBD>씤**: 1) UI 踰꾪듉 <20><EFBFBD><EFBFBD><EFBFBD>뿉 `keyboard_arrow_up` <20>벑 癒명떚由ъ뼹 <20><EFBFBD>씠肄<EC94A0> <20><EFBFBD><EFBFBD>듃媛<EB9383> <20>젒李<ECA092>(`Always runkeyboard_arrow_up`)<29><EFBFBD><20>젙洹쒖떇<EC9296><20><EFBFBD><EFBFBD>븷 寃껋쓣 <20><EFBFBD><EFBFBD><20><EFBFBD>뼱 寃쎄퀎(`\b`)瑜<> <20>젣嫄고븳 <20>뙣移섍<E7A7BB><EC848D> <20><EFBFBD>씤. <20><EFBFBD>뼱 寃쎄퀎媛<ED808E> <20><EFBFBD>씪吏<EC94AA>硫댁꽌 `/Run/i` <20><EFBFBD><EFBFBD>씠 `Running1 command` 媛숈<E5AA9B><EC8888> <20>떎瑜<EB968E> <20><EFBFBD><20><EFBFBD><EFBFBD>듃 踰꾪듉<EABEAA><20><EFBFBD>깘(False Positive)<29>맖. 2) DOM <20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD>듃 踰꾪듉<EABEAA><20><EFBFBD><20><EFBFBD>쑝誘<EC919D><EFBFBD> <20><EFBFBD><EFBFBD>맂 踰꾪듉<EABEAA><20><EFBFBD><20>겢由<EAB2A2><E794B1>맖.
- **<2A>빐寃<EBB990>**: `trigger-click` 濡쒖쭅 <20><EFBFBD><20>쟾 踰꾪듉<EABEAA>쓽 `textContent`<60><EFBFBD>꽌 `keyboard_arrow_up` <20><20><EFBFBD>젮吏<ECA0AE> 瑗щ━ <20><EFBFBD>씠肄<EC94A0> 臾몄옄<EBAA84><EFBFBD>쓣 紐낆떆<EB8286><EFBFBD>쑝濡<EC919D> <20>젣嫄<ECA0A3>(strip)<29>븯怨<EBB8AF>, 紐⑤뱺 <20>듃由ш굅 <20>젙洹쒖떇<EC9296><20><EFBFBD><20><EFBFBD>뼱 寃쎄퀎(`\b`)瑜<> 媛뺤젣 <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20>썝泥<EC8D9D> 李⑤떒<E291A4>븿.
- **二쇱쓽**: UI <20><EFBFBD>냼瑜<EB83BC> DOM<4F><EFBFBD>꽌 湲곸뼱<EAB3B8><20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20>닲寃⑥쭊 <20><EFBFBD>씠肄<EC94A0>/<2F><EFBFBD><EFBFBD>듃 由ш굅爾<EAB585>(ligatures)媛<> <20><EFBFBD>뒗吏<EB9297><><E5AF83><EFBFBD><EFBFBD><20>븿. <20><EFBFBD>꽩 留ㅼ묶 <20>떆 瑗щ━<D189>몴瑜<EBAAB4> 癒쇱<E79992><EC87B1> <20>젣嫄고븯怨<EBB8AF> 紐낇솗<EB8287>븳 寃쎄퀎瑜<ED808E><><E981BA><EFBFBD>븷 寃<>.
### [2026-04-10] [Extension] Ghost Session Hijack & Infinite Polling Loop (trajectory not found)
- **利앹긽**: <20>떊洹<EB968A> <20><EFBFBD><20>떆 '<27><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EBBCB1><EFBFBD>' (Discord濡<64> 由대젅<EB8C80><20><20>맖). 濡쒓렇<EC9293>뿉 500 error trajectory not found 臾댄븳 諛섎났.\n- **<EFBFBD><EFBFBD>씤**: Antigravity媛<79> <20><EFBFBD><EFBFBD>븯硫댁꽌 brain/<2F>뿉 36湲<36><E6B9B2><20><EFBFBD>뜑瑜<EB9C91> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>뜲, Cascade媛<65> <20><EFBFBD>땲誘<EB95B2><EFBFBD> GetCascadeTrajectorySteps<70><EFBFBD>꽌 500 <20><EFBFBD>윭瑜<EC9CAD> <20><EFBFBD><EFBFBD>떎. <20>븯吏<EBB8AF><EFBFBD> <20><EFBFBD><20>떊洹<EB968A> <20><EFBFBD><20><EFBFBD>떎 諛⑹<E8AB9B><E291B9> <20>뙣移섍<E7A7BB><EC848D> <20>씠 Ghost <20><EFBFBD><EFBFBD>쓣 RUNNING<4E>쑝濡<EC919D> 媛뺤젣 <20>벑濡앺븯硫댁꽌, <20><EFBFBD><20><EFBFBD>뀡(activeSessionId)<29><20>깉痍⑦븯怨<EBB8AF> 臾댄븳 <20><EFBFBD>윭 猷⑦봽<E291A6>뿉 鍮좎<E98DAE><ECA28E><EFBFBD> 留뚮뱾<EB9AAE><EFBFBD><EFBFBD><EFBFBD>떎.\n- **<EFBFBD>빐寃<EFBFBD>**: step-probe.ts<74><EFBFBD><20>뤃諛<EBA483> <20>벑濡<EBB291> <20>떆 error message<67>뿉 'trajectory not found'媛<> <20><EFBFBD>븿<EFBFBD>릺硫<EBA6BA> Ghost <20><EFBFBD><EFBFBD>쑝濡<EC919D> 媛꾩<EABEA9>빐 媛뺤젣 <20>벑濡<EBB291>(continue)<29>쓣 嫄대꼫<EB8C80>쎇寃<EC8E87> <20>븯怨<EBB8AF>, Stall Probe <20><EFBFBD>윭 catch<63><EFBFBD><EFBFBD>룄 UTF-8 <20><EFBFBD>윭媛<EC9CAD> <20><EFBFBD>땲硫<EB95B2> stallProbed=true瑜<65> 二쇱뼱 <20><EFBFBD><EFBFBD>룄 臾댄븳 猷⑦봽瑜<EBB4BD> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>떎.\n- **二쇱쓽**: uuid 湲몄씠(36<33>옄)留뚯쑝濡<EC919D> <20><EFBFBD><EFBFBD>넗由щ<E794B1><D189> <20>떇蹂꾪븷 <20>븣 Antigravity<74><79><EFBFBD> Google Agent媛<74> 紐⑦샇<E291A6>빐吏<EBB990> <20><20><EFBFBD>쑝誘<EC919D><EFBFBD>, 諛섎뱶<EC848E>떆 Backend <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>윭(trajectory not found) 硫붿떆吏<EB9686><EFBFBD> <20><EFBFBD><20>뙋蹂꾩쓣 <20><EFBFBD><20><EFBFBD><EFBFBD>떎.\n

View File

@@ -0,0 +1,205 @@
# Observer Script 개발 가이드 — SSOT
> **이 문서는 Observer 코드 변경 전 반드시 확인하는 SSOT입니다.**
> 모든 Observer 관련 설계, 배포, 제약사항이 이 문서에 있습니다.
---
## 1. Observer 코드 특성
### 1.1 실행 환경
- Observer는 **workbench-jetski-agent.html**에 인라인 `<script>`로 삽입됨
- **AG Native의 Electron 렌더러 프로세스**에서 실행 (VS Code extension host가 아님)
- 렌더러는 **strict mode 아님** (확인 필요), 하지만 V8 parser는 일부 strict-like 규칙 적용
- `generateApprovalObserverScript(port)` 함수가 **TypeScript template literal**로 스크립트 생성
### 1.2 코드 작성 규칙 (위반 시 Observer 전체 크래시)
| 규칙 | 이유 | 예시 |
|------|------|------|
| **for 루프 안에 function 선언 금지** | V8 strict mode error | `var fn = function(){}` 사용 |
| **문자열 리터럴에 특수문자 금지** | template literal 이스케이핑 깨짐 | `'??'``'MAX'` |
| **regex에 `\\s` 등 이스케이프 금지** | template literal이 `\\\\s``\\s` (리터럴) | 문자열 비교 사용 |
| **ES6+ 구문 금지** | 구 V8 호환 | `var` 사용, `let/const/arrow` 금지 |
| **배포 전 SYNTAX CHECK 필수** | Observer 크래시 방지 | 아래 검증 명령어 참조 |
### 1.3 필수 검증 명령어 (모든 빌드 전 실행)
```powershell
npm.cmd run compile; node -e "const {generateApprovalObserverScript}=require('./out/observer-script'); let s=generateApprovalObserverScript(18080); try { new Function(s); console.log('SYNTAX OK'); } catch(e) { console.log('ERROR:', e.message); }"
```
**SYNTAX OK가 나오지 않으면 절대 배포하지 않는다.**
### 1.4 배포 전 자기검증 체크리스트 (MANDATORY)
**재시작을 요구하기 전 반드시 다음을 모두 통과해야 한다:**
1. [ ] **SYNTAX CHECK 통과**: `new Function(s)``SYNTAX OK`
2. [ ] **수정 방향 검증**: 이 수정이 문제를 해결하는 올바른 접근인지 스스로 2번 재검증
3. [ ] **template literal 규칙 위반 없음**: regex 이스케이프, 특수문자, function 선언 등
4. [ ] **변경 범위 최소화**: 불필요한 코드 포함 여부 확인
5. [ ] **재시작 사유 명시**: 사용자에게 (a) 무엇을 수정했고 (b) 왜 재시작이 필요한지 1~2줄로 설명
6. [ ] **재시작 횟수 명시**: Observer 변경 = 2회, Extension host만 변경 = 1회
7. [ ] **log() relay 필터 확인**: 새 로그 키워드 추가 시 log() 함수의 키워드 필터에도 추가했는지 확인 (섹션 3.5 참조)
8. [ ] **regex E2E 테스트**: Observer에서 사용하는 새 regex는 생성된 코드에서 직접 실행하여 매칭 검증
9. [ ] **구현 전 가정 검증**: 새 접근을 코딩하기 전에, 핵심 가정이 성립하는지 로그 1줄로 먼저 확인 (예: "Step Probe가 WAITING을 볼 수 있는가?" → `STEP-PROBE.*WAITING` 로그 검색)
**정당한 사유 없이 재시작을 요구하지 않는다.**
**DOM 구조를 먼저 파악하고 설계한 후 코드를 작성한다.**
**시행착오식(trial-and-error) 접근을 하지 않는다.**
**추측으로 코딩하지 않는다. 로그/데이터로 확인한 사실에 기반하여 코딩한다.**
---
## 2. 배포 프로세스
### 2.1 Observer 코드가 포함된 변경
Observer 코드 변경은 **extension host 코드 변경보다 비용이 높다**:
```
VSIX 빌드 → VSIX 설치 → AG 재시작 #1 (extension이 HTML 패치)
→ AG 재시작 #2 (패치된 HTML 로드) → Observer 실행
```
**총 2번 AG 재시작 필요**
### 2.2 Extension host 코드만 변경 (approval-handler, http-bridge 등)
```
VSIX 빌드 → VSIX 설치 → AG 재시작 #1 → 즉시 적용
```
**1번 AG 재시작 필요**
### 2.3 VSIX 설치 확인
```powershell
Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Filter "*gravity*" -Directory |
ForEach-Object { $j = Get-Content (Join-Path $_.FullName "package.json") | ConvertFrom-Json; "$($_.Name): v$($j.version)" }
```
### 2.4 HTML 패치 확인 (Observer 코드가 반영되었는지)
```powershell
Select-String -Path "$env:LOCALAPPDATA\Programs\Antigravity\resources\app\out\vs\code\electron-browser\workbench\workbench-jetski-agent.html" -Pattern "검색할_함수명" -Quiet
```
---
## 3. AG Native DOM 구조
### 3.1 Chat Panel (Observer가 접근 가능)
- 위치: `document.querySelector('#conversation')` 또는 `document.querySelector('[class*="conversation"]')`
- AI 응답 블록: `.leading-relaxed.select-text`
- 사용자 메시지 블록: `.select-text.rounded-lg` (v0.5.74+)
- Thinking 블록: 조상에 `max-h-[200px]` 클래스 있음 → 필터링
### 3.2 승인 버튼 — "Always run" (v0.5.93 BTN-DOM-DUMP 확인)
실제 DOM 구조 (v0.5.92 로그로 확인):
- d0: button.flex.cursor-pointer (Always run 버튼)
- d1: div.min-w-0
- d2: div.flex.items-center.justify-between.rounded-b.border-t (버튼 바)
- d3: div (이름 없는 컨테이너)
- div.mb-1 = "Running command" (헤더)
- div.flex = " gravity_control > ..." (실제 명령어, plain div!)
- div.flex = "Always run Cancel" (버튼들)
> 명령어는 pre.font-mono나 code가 아닌 plain div.flex에 있음.
> v30: "Running command" div의 형제를 탐색하여 프롬프트 마커 뒤의 명령어 추출.
- "Retry" 버튼: button 태그, chat panel 내
### 3.3 Diff Review (Accept all / Reject all) — Observer 접근 가능 (v0.5.101+)
- **v0.5.101 이전**: 에디터 webview에 렌더링, Observer document에서 접근 불가
- **v0.5.101 이후**: AG UI 업데이트로 chat panel footer에 `<span class="cursor-pointer">` 태그로 렌더링
- Observer의 `allBtns` 선택자에 `span.cursor-pointer` 포함 필수
- `matchedType = 'diff_review'`로 분류됨 (L1120: `txt.includes('Accept')`)
- auto-approve response 파일에 `_from_ws: true` 마커 필수 (processResponseFile race condition 방지)
### 3.4 DOM 렌더링 타이밍
- "Always run" 버튼이 DOM에 나타날 때 명령어 div도 함께 렌더링됨
- v30의 "Running command" div 탐색은 즉시 성공
### 3.5 log() relay 필터 규칙
Observer의 log() 함수는 키워드 필터로 일부 로그만 extension.log에 relay.
새 로그 키워드 추가 시 반드시 필터도 함께 수정해야 함.
현재 필터 키워드 (v0.5.92+):
CV-CLASSES, CV-CHILDREN, child[, CV found, Conversation view,
BEACON, ERROR, chat relay, user-cls,
CONTEXT, BTN-DOM, DEFERRED, DETECTED
---
## 4. 파일 경로 매핑
| 항목 | 경로 |
|------|------|
| AG 설치 경로 | `$env:LOCALAPPDATA\Programs\Antigravity\` |
| workbench HTML | `...\resources\app\out\vs\code\electron-browser\workbench\workbench-jetski-agent.html` |
| Extension 로그 | `$env:USERPROFILE\.gemini\antigravity\bridge\extension.log` |
| Pending 파일 | `$env:USERPROFILE\.gemini\antigravity\bridge\pending\*.json` |
| Response 파일 | `$env:USERPROFILE\.gemini\antigravity\bridge\response\*.json` |
| VSIX extensions | `$env:USERPROFILE\.vscode\extensions\variet.gravity-bridge-*\` |
| HTTP Bridge 포트 | 34332 (또는 `getDeterministicPort('gravity_control')`) |
| Discord 채널 | `#ag-gravity_control` (ID: 1483082084540223663) |
---
## 5. 과거 실수 및 교훈
### 5.1 VSIX 미설치 (v0.5.78~83)
- **증상**: 빌드만 하고 `code --install-extension` 실행 안 함
- **결과**: 설치된 버전이 v0.5.50, 모든 수정사항 미적용
- **교훈**: 빌드 후 반드시 `code --install-extension *.vsix --force` 실행
- **확인**: `Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Filter "*gravity*"`
### 5.2 function 선언 → Observer 크래시 (v0.5.84~86)
- **증상**: `function _isGenericDesc(d){}` 를 for 루프 내부에 선언
- **결과**: Observer 전체 크래시, chat relay + auto-approve 중단
- **교훈**: `var fn = function(){}` 사용, 배포 전 SYNTAX CHECK 필수
### 5.3 깨진 문자열 리터럴 (v0.5.86)
- **증상**: `{tag:'??',...}` (특수문자가 따옴표 깨뜨림)
- **결과**: SYNTAX ERROR, Observer 미작동
- **교훈**: template literal 안에서 특수문자/이모지 사용 주의
### 5.4 regex 이스케이핑 실패 (v0.5.83~84)
- **증상**: `/Always\\s+run/` → 생성 시 `\\s` (리터럴 백슬래시+s)로 출력
- **결과**: "Always run" 매칭 실패
- **교훈**: template literal 안에서 regex 대신 **문자열 비교** 사용
### 5.5 _from_ws 파일 무한 누적 (v0.5.78~84)
- **증상**: response 파일에 `_from_ws: true` 마커 → processResponseFile이 스킵 → 영원히 삭제 안 됨
- **결과**: 3초마다 4개 파일 × SKIP 로그 → 로그 스팸, 다른 처리 방해
- **교훈**: 보존 파일에는 반드시 **TTL(자동 만료)** 추가
---
## 6. 디버깅 체크리스트
### Observer가 작동하지 않을 때
1. `extension.log`에서 `setup complete` 확인 → Observer 로드 여부
2. `OBSERVER-LOG` 패턴 검색 → 스캔 활동 여부
3. `HTTP-REQ` 검색 → HTTP bridge에 요청 도달 여부
4. **SYNTAX CHECK** 실행 → 생성 스크립트 문법 검증
5. `SKIP _from_ws` 반복 확인 → stale response 파일 정리
### Discord에 메시지가 안 올 때
1. `POST /chat` 검색 → chat relay 전송 여부
2. `WS.*send` 검색 → WebSocket 전송 여부
3. Discord API로 직접 확인: `node extension/scratch/discord_read.js`
4. stale response 파일 확인: `Get-ChildItem $env:USERPROFILE\.gemini\antigravity\bridge\response\*.json`
---
## 7. 버전 히스토리 요약
| 버전 | 핵심 변경 | 결과 |
|------|----------|------|
| v0.5.50 | 기본 릴레이 시스템 | ✅ 안정 |
| v0.5.78 | `_from_ws` 마커 (Retry 보존) | ✅ 작동 (TTL 미구현) |
| v0.5.79 | sibling 탐색 + thinking 필터 | ✅ 작동 |
| v0.5.80~81 | Accept all offsetParent 완화 | ❌ 구조적 불가 (에디터 webview) |
| v0.5.82 | 버튼 셀렉터 확장 + ACCEPT-SCAN | 진단용 |
| v0.5.83 | DEFERRED 컨텍스트 500ms | regex 이스케이핑 실패 |
| v0.5.84 | regex → 문자열 비교 | function 선언 크래시 |
| v0.5.85 | `_from_ws` TTL 60초 | ✅ stale 정리 |
| v0.5.86 | function → var expression | 깨진 문자열 미발견 |
| v0.5.87 | 깨진 문자열 2건 수정 | ✅ SYNTAX OK |

View File

@@ -0,0 +1,216 @@
# AG Native 릴레이 아키텍처 분석
> **이 문서는 AG Native ↔ Discord 릴레이의 데이터 흐름 SSOT입니다.**
> 구현/디버깅 전 반드시 확인합니다.
---
## 1. 데이터 경로 요약
AG Native에서 Discord로 메시지를 전달하는 경로는 크게 2개:
| # | 경로 | 소스 | 실시간? | 상태 |
|---|------|------|---------|------|
| 1 | **Observer DOM** | workbench.html 인라인 스크립트 → DOM 관찰 → HTTP POST → http-bridge | ✅ 실시간 | AI 응답: ✅ 작동 (v0.5.72+), 사용자 메시지: ✅ 작동 (v0.5.74+) |
| 2 | **Step Probe (trajectory API)** | LS RPC `GetCascadeTrajectorySteps` → step 분석 | ❌ cascade 완료 후에만 | AI 응답: ❌ 실시간 불가, 사용자 메시지: ❌ 실시간 불가 |
### 1.1 핵심 API 제약 (2026-04-18 확인)
> [!CAUTION]
> **`GetCascadeTrajectorySteps`는 진행 중인 cascade의 step을 실시간으로 반환하지 않습니다.**
> step count는 cascade가 **완전히 종료**(IDLE 전환)된 후에만 업데이트됩니다.
> 따라서 Step Probe의 RT-CAPTURE, HB-CAPTURE 모두 **현재 진행 중인 대화에서는 작동하지 않습니다.**
**검증 데이터**:
- POLL에서 `status=CASCADE_RUN_STATUS_IDLE`, `steps=928`, `delta=0` 고정
- HEARTBEAT probe: `offset=927 got=1 real=928 known=928` → 변함 없음
- 실제로 수십 개의 tool call이 실행되었지만 step count 불변
- Cascade 종료 후 다음 poll에서 step count가 점프 (예: 733 → 865 → 928)
### 1.2 AG Native SDK EventMonitor
SDK에 이벤트 시스템이 있으나 **모두 polling 기반**:
- `EventMonitor.onStepCountChanged` — getDiagnostics 기반 polling
- `EventMonitor.onActiveSessionChanged` — state.vscdb 기반 polling
- **실시간 push (WebSocket/SSE)는 없음**
- 현재 상태: ERR_CONNECTION_REFUSED 문제로 비활성화됨
---
## 2. Observer DOM 경로 상세
### 2.1 Observer 스크립트 삽입 체인
```
extension.ts activate()
→ html-patcher.ts setupApprovalObserver()
→ observer-script.ts generateApprovalObserverScript(port)
→ workbench-jetski-agent.html에 인라인 <script> 삽입
→ AG 재시작 시 렌더러가 로드
```
### 2.2 Observer 주요 함수
| 함수 | 역할 |
|------|------|
| `scanChatBodies()` | 3초마다 실행, conversation view에서 메시지 블록 탐색 |
| `extractCleanStepText(el)` | DOM 클론 → style/script/button 제거 → textContent 추출 |
| `extractContextFromNearby(btn)` | 승인 버튼 주변 DOM에서 명령어 텍스트 추출 (v23: sibling 탐색 포함) |
| `pollResponseGroup(rid, btnRefs)` | response 파일 polling → 버튼 자동 클릭 |
### 2.3 AI 응답 감지 셀렉터
```javascript
var responseBlocks = cv.querySelectorAll(
'.leading-relaxed.select-text, ' // ← AI 응답 마크다운 블록 (주력)
+ '.text-ide-message-block-user-color, ' // ← 사용자 메시지 (미매칭)
+ '.text-ide-message-block-bot-color, ' // ← NUX tooltip 전용 (오매칭)
+ '.bg-ide-message-block-user-background, '// ← 사용자 메시지 (미매칭)
+ '[data-message-role="user"], ' // ← 사용자 메시지 (미매칭)
+ '[data-role="user"]' // ← 사용자 메시지 (미매칭)
);
```
> [!NOTE]
> **v0.5.74에서 사용자 메시지 셀렉터가 추가되었습니다.**
> AG Native 소스(`jetskiAgent/main.js`)의 `Esn` 컴포넌트 분석으로
> 사용자 메시지 CSS 클래스(`msn = "bg-gray-500/10 border border-gray-500/20 p-2 rounded-lg w-full text-sm select-text"`)를 식별.
> 셀렉터: `.select-text.rounded-lg`, 역할 판별: `rounded-lg` 있고 `leading-relaxed` 없으면 → user
### 2.4 AI 응답 추출 흐름
```
scanChatBodies() 3초 간격
→ cv = document.querySelector('#conversation')
→ responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, ...')
→ lastBlock = responseBlocks[last] (가장 최근 블록)
→ 이미 scrape 됐으면 skip
→ blockText = extractCleanStepText(lastBlock)
→ 안정화 대기 (3초 동안 텍스트 변경 없으면)
→ POST /chat { text, source, block_index, role }
→ http-bridge → writeChatSnapshot() → WS → Discord
```
### 2.5 Observer 업데이트 제약
> [!CAUTION]
> **Observer 코드는 workbench.html에 인라인 삽입됩니다.**
> extension reload만으로는 Observer 코드가 업데이트되지 않습니다.
> **AG 재시작 + V8 CachedData 삭제**가 필요합니다.
> (단, product.json 체크섬이 맞으면 CachedData 삭제 없이 AG 재시작만으로 충분할 수 있음)
---
## 3. 승인 버튼 (Auto-Approve) 경로
### 3.1 "Always run" 자동 승인 흐름
```
Observer DOM scan
→ "Always run" 버튼 텍스트 감지
→ POST /pending { command: "Always run", description: "...", buttons: [...] }
→ http-bridge _handlePending()
→ alwaysRunDetected = true
→ enrichment 시도:
1. rawDesc에서 > 프롬프트 마커 찾기 → ✅ 성공 (buttons=2일 때 desc에 프롬프트 포함)
2. rawDesc 최장 라인 사용 → buttons=1일 때 desc="Always run"이라 실패
3. v20 fallback: bridge/pending/ 최신 파일에서 command 읽기 → Step Probe pending 있을 때만
4. v23 sibling: Observer가 footer 형제 요소에서 pre.font-mono 탐색 → ✅ 성공
→ response 파일 작성 → Observer pollResponseGroup → 버튼 클릭
→ WS sendPending { status: 'auto_approved', command: displayCmd }
→ Discord embed 표시
```
### 3.2 명령어 enrichment 현황 (2026-04-18 검증)
| 조건 | 결과 | 빈도 |
|------|------|------|
| Observer가 buttons=2 (`["Always run","Cancel"]`)이고 desc에 `>` 포함 | ✅ 명령어 표시 | ~50% |
| Observer가 buttons=1 (`["Always run"]`)이고 desc="Always run" | ❌ "Always run" 표시 | ~50% |
**로그 증거** (04:25:59):
```
AUTO-APPROVE raw: cmd="Always run" desc="…\extension > npm.cmd run compile..." buttons=["Always run","Cancel"]
→ cmd="npm.cmd run compile 2>&1; npm.cmd version patch..." ✅ 성공
AUTO-APPROVE raw: cmd="Always run" desc="Always run" buttons=["Always run"]
→ cmd="Always run" ❌ 실패
```
> buttons=2인 경우("Always run" + "Cancel")는 Observer가 code 블록을 찾아 description에 포함.
> buttons=1인 경우는 code 블록이 DOM에서 아직 렌더링되지 않았거나 접근 불가.
---
## 4. 사용자 메시지 릴레이 상태
### 4.1 현재 상태: ✅ 작동 (v0.5.74+)
| 경로 | 상태 | 비고 |
|------|------|------|
| Observer DOM | ✅ | `.select-text.rounded-lg` 셀렉터로 캡처 (v0.5.74) |
| Step Probe (trajectory API) | ❌ | cascade 진행 중 step 조회 불가 |
| Step Probe (observer [USER-MSG]) | ❌ | `lastUserInputStepIndex`가 갱신되지 않음 |
### 4.2 해결 방안
1. **DOM 덤프에서 사용자 메시지 클래스 식별** → Observer 셀렉터 추가
2. **Cascade 완료 후** Step Probe HB-CAPTURE에서 `USER_INPUT` step 캡처 (지연 릴레이)
---
## 5. 파일/포트 매핑
| 항목 | 값 |
|------|-----|
| Observer 삽입 대상 | `workbench-jetski-agent.html` |
| HTTP Bridge 포트 | `getDeterministicPort('gravity_control')` = **18080** |
| Extension 로그 | `~/.gemini/antigravity/bridge/extension.log` |
| Pending 파일 | `~/.gemini/antigravity/bridge/pending/*.json` |
| Response 파일 | `~/.gemini/antigravity/bridge/response/*.json` |
| Chat Snapshot 파일 | `~/.gemini/antigravity/bridge/chat_snapshots/*.json` |
| Discord 채널 | `#ag-gravity_control` (ID: 1483082084540223663) |
| Discord Bot 토큰 | `.env``DISCORD_TOKEN` |
---
## 6. 디버깅 도구
| 도구 | 경로 | 용도 |
|------|------|------|
| Discord 메시지 읽기 | `extension/scratch/discord_read.js` | API로 채널 최근 메시지 조회 |
| Discord 채널 목록 | `extension/scratch/discord_channels.js` | 서버 채널 목록 조회 |
| Extension 로그 확인 | `Select-String -Path $logFile -Pattern "패턴"` | 실시간 로그 분석 |
| DOM 구조 덤프 | Observer 자동 (CV-CHILDREN 로그) | AG Native DOM 클래스 식별 |
---
## 7. 버전 히스토리 (v0.5.67~)
| 버전 | 변경 | 결과 |
|------|------|------|
| v0.5.67 | Observer DOM relay 비활성화, Step Probe RT-CAPTURE로 전환 | ❌ API가 진행중 step 미반환 |
| v0.5.68 | auto-approve enrichment 디버그 로그 추가, 조건 >10 → >3 완화 | Observer가 desc="Always run" 보냄 확인 |
| v0.5.69 | pending 파일 fallback으로 auto-approve 명령어 enrichment | 일부 개선 (Step Probe pending 있을 때만) |
| v0.5.70 | heartbeat 로깅 강화 | API step count 동결 확인 |
| v0.5.71 | heartbeat 3 poll마다 실행, HB-CAPTURE 추가 | API가 진행중 step 미반환 재확인 |
| v0.5.72 | Observer DOM relay 재활성화 | AG 재시작 필요 (Observer HTML 캐시) |
| v0.5.74 | 사용자 메시지 셀렉터 추가 (`.select-text.rounded-lg`) | ✅ 사용자 메시지 릴레이 작동 |
| v0.5.76 | DOM 탐색 depth 5→10, `pre.font-mono` 우선 탐색 | Observer HTML 업데이트 필요 |
| v0.5.77 | WS response 파일 작성 (pollResponseGroup용) | Retry 클릭 경로 추가 |
| v0.5.78 | `_from_ws` 마커로 processResponseFile 삭제 방지 | ✅ Retry auto-approve 작동 |
| v0.5.79 | sibling 탐색 추가 + thinking 블록 필터링 | ✅ 명령어 컨텍스트 부분 추출 |
---
## 8. 남은 작업 (TODO)
- [x] AG 재시작하여 Observer 반영 확인 — ✅ v0.5.72 작동 확인
- [x] Observer의 AI 응답 릴레이가 작동하는지 Discord에서 확인 — ✅ 작동
- [x] 사용자 메시지 셀렉터 추가 — ✅ v0.5.74
- [x] Retry auto-approve 흐름 복구 — ✅ v0.5.78 (_from_ws 마커)
- [x] 명령어 컨텍스트 sibling 탐색 — ✅ v0.5.79
- [x] Thinking 블록 필터링 — ✅ v0.5.79
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패) #636
- [ ] Observer pollResponseGroup 미시작 케이스 (trigger-click 선점)
- [ ] AI 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)

View File

@@ -0,0 +1,99 @@
# Tech Stack
> AI 에이전트는 구현 전 이 문서를 확인하여 올바른 기술/버전을 사용합니다.
## 언어 & 런타임
| 항목 | 버전 | 경로/비고 |
|------|------|-----------|
| Python | 3.12 (miniforge3) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` |
| Node.js | 시스템 설치 | `node`, `npm` (PowerShell에서 `cmd /c npm` 권장) |
| TypeScript | 5.3+ | `extension/src/*.ts``tsc``extension/out/*.js` |
> [!IMPORTANT]
> Python은 **반드시** 위 miniforge3 경로를 사용. WindowsApps의 python stub은 동작하지 않음.
## 프레임워크 & 라이브러리
### Python (서버)
| 패키지 | 버전 | 용도 |
|--------|------|------|
| discord.py | 2.x | Discord 봇 (슬래시 명령, 버튼 UI, 이벤트) |
| aiohttp | 3.x | Gateway HTTP 서버 + WebSocket endpoint |
| watchdog | - | Brain 디렉토리 파일시스템 감시 |
| python-dotenv | - | .env 파일 로드 |
| PyJWT | - | ❌ 미사용 (자체 HMAC-SHA256 구현) |
### TypeScript (Extension)
| 패키지 | 용도 |
|--------|------|
| @types/vscode | VS Code Extension API 타입 |
| @types/node | Node.js 타입 |
| typescript | 컴파일러 |
| ws | WebSocket Hub 연결 (`.vscodeignore``!node_modules/ws/**` 필수) |
| antigravity-sdk | AG RPC 호출 (로컬 임베드 `sdk/`) |
## 패키지 관리
| 측 | 도구 | 파일 |
|----|------|------|
| Python | pip | `requirements.txt` |
| Extension | npm | `extension/package.json` |
## 개발 도구 & 명령어
| 작업 | 명령어 |
|------|--------|
| **봇 실행** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe main.py` |
| **봇 실행 (gateway)** | `.env`에서 `BOT_MODE=gateway` 설정 후 위 명령 |
| **Extension 구문 검사** | `cd extension && npx tsc --noEmit` |
| **Extension 컴파일** | `cd extension && cmd /c npm run compile` |
| **Extension VSIX** | `cd extension && npx @vscode/vsce package --no-dependencies` |
| **Python 구문 검사** | `python -c "import ast; [ast.parse(open(f).read()) for f in ['bot.py','hub.py',...]]"` |
| **Hub WS 테스트** | `python tests/test_ws_hub.py` (서버 기동 상태에서) |
## 환경 변수 (.env)
### 필수
| 변수명 | 용도 | 기본값 |
|--------|------|--------|
| DISCORD_TOKEN | Discord 봇 토큰 | (필수) |
| DISCORD_GUILD_ID | Discord 서버 ID | (필수) |
### 선택
| 변수명 | 용도 | 기본값 |
|--------|------|--------|
| BRAIN_PATH | AG 브레인 경로 | `~/.gemini/antigravity/brain` |
| BOT_MODE | `local` / `gateway` | `local` |
| DEBOUNCE_SECONDS | Watcher 디바운스 간격 | `5` |
| PROJECT_NAME | 프로젝트 이름 | `gravity_control` |
### Gateway 모드 전용
| 변수명 | 용도 | 기본값 |
|--------|------|--------|
| GATEWAY_PORT | Gateway HTTP/WS 포트 | `8585` |
| GATEWAY_API_KEY | REST API 인증 키 | (미설정 시 인증 미사용) |
| GRAVITY_HUB_SECRET | WS Hub JWT 서명 시크릿 (64char hex) | (미설정 시 인증 생략) |
| GRAVITY_REGISTRATION_CODE | Extension 등록 코드 (32char hex) | (미설정 시 인증 생략) |
## Extension VS Code 설정
| 설정 키 | 용도 |
|---------|------|
| `gravityBridge.bridgePath` | Bridge 디렉토리 경로 |
| `gravityBridge.projectName` | 프로젝트 이름 (기본: git remote) |
| `gravityBridge.hubUrl` | Hub WS URL (예: `ws://localhost:8585/ws`) |
| `gravityBridge.registrationCode` | Hub 등록 코드 |
## 빌드 산출물
| 항목 | 경로 | 설명 |
|------|------|------|
| VSIX | `extension/gravity-bridge-{ver}.vsix` | VS Code 확장 패키지 |
| JS 출력 | `extension/out/*.js` | TypeScript 컴파일 결과물 |
| SDK 복사 | `extension/out/sdk/` | compile 시 자동 복사 |

View File

@@ -0,0 +1,40 @@
---
description: Gitea API로 저장소 커밋/이슈/PR 현황을 조회하는 워크플로우
---
# Gitea 저장소 현황 조회
서비스 정보는 `.agents/workflows/services.md` 참조.
// turbo-all
## 절차
1. 최근 커밋 조회 (최신 10개):
```powershell
$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"}
$commits = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/gravity_control/commits?limit=10&sha=main" -Headers $h
$commits | ForEach-Object { Write-Host "$($_.sha.Substring(0,7)) $($_.commit.message.Split("`n")[0])" }
```
2. 열린 이슈 조회:
```powershell
$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"}
$issues = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/gravity_control/issues?state=open&type=issues" -Headers $h
$issues | ForEach-Object { Write-Host "#$($_.number) $($_.title)" }
```
3. Wiki 페이지 목록:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py list
```
4. Wiki 페이지 읽기:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py read "Architecture"
```
5. Wiki 페이지 업데이트:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py update "페이지-제목" /tmp/wiki_content.md
```

View File

@@ -0,0 +1,41 @@
---
description: Vikunja API로 프로젝트 태스크 현황을 조회하는 워크플로우
---
# Vikunja 태스크 현황 조회
서비스 정보는 `.agents/workflows/services.md` 참조.
// turbo-all
## 절차
1. 전체 목록:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list
```
2. TODO만:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list todo
```
3. DONE만:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list done
```
4. 태스크 완료 처리 (**⚠️ 반드시 이 방법 사용 — 직접 API 호출 금지**):
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {TASK_ID}
```
5. 새 태스크 생성:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High
```
> [!CAUTION]
> **절대로** `Invoke-RestMethod -Method Post -Body '{"done": true}'` 같은 직접 API 호출을 사용하지 마세요.
> Vikunja API는 POST 시 body에 포함되지 않은 필드를 빈값으로 덮어씁니다.
> `vikunja_helper.py`는 항상 GET → 기존 필드 보존 → POST 패턴을 사용합니다.

View File

@@ -0,0 +1,52 @@
---
description: 에러/버그 발생 시 체계적 디버깅 워크플로우 (에러, 안돼요, 왜 안돼, 버그, 디버그, 수정)
---
# Debug Workflow
> [!IMPORTANT]
> 추측으로 코드를 수정하지 마세요. 반드시 이 순서를 따릅니다.
## 1단계: 정보 수집 (추측 금지)
- [ ] 에러 메시지 **전문** 확인 (절대 잘라내지 않기)
- [ ] 관련 로그 파일 확인
- [ ] 환경 정보 확인 (OS, Node/Python 버전, 의존성 버전 등)
- [ ] 에러가 발생하는 **정확한 입력/조건** 파악
## 2단계: Known Issues 확인
`.agents/references/known-issues.md`를 읽고 동일하거나 유사한 문제가 있는지 확인합니다.
> [!CAUTION]
> **known-issues 확인 없이 해결 시도를 시작하지 마세요.**
> 이미 해결된 문제를 다시 삽질하는 것은 시간 낭비입니다.
## 3단계: 근본 원인 분석
- [ ] 에러가 발생하는 **정확한 코드 위치** 확인
- [ ] 가설을 세우고, 가설을 검증할 수 있는 **최소한의 테스트** 수행
- [ ] 가설이 틀렸다면 **즉시 다른 가설로 전환**
> [!WARNING]
> **동일한 접근을 2회 초과 시도하지 마세요.**
> 2회 실패 시 유저에게 보고하고 판단을 요청합니다.
> 보고 내용: 시도한 것 / 실패한 것 / 원인 가설 / 다음 제안
## 4단계: 수정 및 검증
- [ ] 수정 적용
- [ ] 동일 에러가 재현되지 않는지 확인
- [ ] 사이드 이펙트(다른 기능에 영향) 없는지 확인
## 5단계: 기록
- [ ] `known-issues.md`에 새 항목 추가 (아래 포맷 사용)
```markdown
### [날짜] [키워드] — 한줄 요약
- **증상**: 무엇이 잘못되었는가
- **원인**: 근본 원인
- **해결**: 올바른 해결 방법
- **주의**: 재발 방지를 위한 교훈
```

165
.agents/workflows/end.md Normal file
View File

@@ -0,0 +1,165 @@
---
description: 세션 종료 시 devlog 기록 + git commit + Vikunja 동기화 (끝, 마무리, 커밋해, 완료)
---
# 세션 종료 프로토콜
작업 완료, "끝", "마무리", "커밋해" 등 요청 시 이 워크플로우를 실행합니다.
// turbo-all
## 0. 학습 기록 (실패/시행착오 저장)
이번 세션에서 발생한 실패, 시행착오, 새로 알게 된 사실을 정리합니다:
- [ ] `.agents/references/known-issues.md`에 추가할 항목이 있는지 확인
- [ ] 있다면 아래 포맷으로 추가:
```markdown
### [날짜] [키워드] — 한줄 요약
- **증상**: ...
- **원인**: ...
- **해결**: ...
- **주의**: ...
```
## 1. Devlog 기록
### Index 업데이트 (필수 — 매 작업)
오늘 날짜의 index 파일에 완료된 작업 1줄을 추가합니다.
- **파일**: `docs/devlog/YYYY-MM-DD.md`
- **형식**:
```markdown
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
```
> [!TIP]
> - ✅ = 완료, 🔧 = 미완료 (다음 세션에서 이어받기)
> - 파일이 없으면 새로 생성 (테이블 헤더 포함)
### Entry 작성 (선택적 — 필요할 때만)
> [!IMPORTANT]
> Entry는 **git/Vikunja/wiki에 없는 정보**가 있을 때만 작성합니다.
**Entry 작성 기준:**
- ✅ 설계 결정이 있었을 때 (왜 A가 아닌 B를 선택했는지)
- ✅ 미완료 사항이 있을 때 (다음 세션이 이어받아야 할 맥락)
- ✅ 삽질/트러블슈팅이 있었을 때 (같은 실수 방지)
**Entry 불필요:**
- ❌ 단순 버그 픽스 (커밋 메시지로 충분)
- ❌ 문서 업데이트 (git diff로 충분)
- ❌ 이미 Vikunja 태스크에 상세 설명이 있는 경우
**Entry 파일**: `docs/devlog/entries/YYYYMMDD-NNN.md`
```markdown
# 작업 제목
- **시간**: YYYY-MM-DD HH:MM~HH:MM
- **Commit**: `해시`
- **Vikunja**: #태스크번호 → done/진행중
## 결정 사항
- 왜 이 방식을 선택했는지
## 미완료
- 남은 작업 (있을 경우)
```
---
## 2. Vikunja 동기화
> [!CAUTION]
> **반드시 `vikunja_helper.py` 사용.** 직접 API 호출 금지.
> Vikunja API는 POST 시 body에 없는 필드를 빈값으로 덮어씁니다.
### 2-1. 커밋 전수 검사
이번 세션의 **모든 커밋을 하나씩 검사**하고 Vikunja에 매핑합니다.
```powershell
git log --oneline -20
```
| 커밋 유형 | Vikunja 액션 |
|-----------|-------------|
| 기존 태스크 해당 작업 **완료** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {ID}` |
| 신규 작업 완료 (기존 태스크 없음) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --done --labels Backend,Priority:High` |
| 작업 중 발견된 **미완료 TODO** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:Mid` |
> [!IMPORTANT]
> 모든 커밋이 기존 또는 신규 태스크에 매핑되었는지 확인.
### 2-2. 완료 처리
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {TASK_ID}
```
### 2-3. 신규 태스크 생성
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High
```
### 라벨 규칙
**영역 (필수 1개 이상):** `Backend` / `Frontend` / `Engine` / `Infra` / `Test`
**우선순위 (필수 1개):** `Priority:High` / `Priority:Mid` / `Priority:Low`
---
## 3. Wiki 동기화 (해당 시에만)
| 코드 변경 | 대상 Wiki |
|-----------|----------|
| 서버 변경 | Architecture |
| 프론트엔드 변경 | Architecture |
| 인프라 변경 | Architecture |
| 새 모듈/패키지 추가 | Architecture |
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\wiki_helper.py update "Architecture" /tmp/wiki_content.md
```
---
## 4. Git Commit & Push
```powershell
git add -A
git status --short
```
```powershell
git commit -m "커밋 메시지"
```
```powershell
git push origin main
```
**커밋 메시지 컨벤션:**
```
<type>(<scope>): <description>
type: feat|fix|refactor|test|docs|chore|ci|infra
scope: (선택)
```
---
## 5. 최종 체크리스트
> [!WARNING]
> 아래 항목 중 하나라도 누락되면 세션 종료를 완료할 수 없습니다.
- [ ] known-issues 업데이트됨 (새 이슈가 있었다면)
- [ ] devlog index 업데이트됨
- [ ] devlog entry 작성됨 (필요한 경우만)
- [ ] Vikunja 태스크 생성/완료 처리됨 (커밋 전수 검사 기반)
- [ ] Wiki 동기화됨 (아키텍처 변경이 있었다면)
- [ ] git push 완료
- [ ] 사용자에게 완료 보고

View File

@@ -0,0 +1,160 @@
"""Analyze AG Native DOM structure to find AI response containers."""
import json, os, sys
def load_dump():
bridge = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge')
# Try deep-inspect result first, then dump_html
for fname in ['deep-inspect-result.json', 'dump_html.json']:
fpath = os.path.join(bridge, fname)
if os.path.exists(fpath):
print(f"Loading: {fname} ({os.path.getsize(fpath)} bytes)")
with open(fpath, 'r', encoding='utf-8-sig') as f:
return json.load(f), fname
return None, None
def find_text_containers(node, path="", depth=0, results=None):
"""Recursively find nodes with substantial text content (potential AI response containers)."""
if results is None:
results = []
if not isinstance(node, dict):
return results
tag = node.get('tag', '')
cls = node.get('cls', '')
text = node.get('text', '')
attrs = node.get('attrs', {})
children = node.get('children', [])
cur_path = f"{path}/{tag}"
if cls:
short_cls = cls[:60]
cur_path += f".{short_cls}"
# Look for nodes with long text (potential AI responses)
if text and len(text) > 50:
results.append({
'path': cur_path,
'depth': depth,
'tag': tag,
'cls': cls[:100],
'text_len': len(text),
'text_preview': text[:120],
'attrs': {k:v for k,v in attrs.items() if k not in ('style',)}
})
for child in children:
find_text_containers(child, cur_path, depth+1, results)
return results
def find_by_class_pattern(node, patterns, path="", depth=0, results=None):
"""Find nodes matching class patterns."""
if results is None:
results = []
if not isinstance(node, dict):
return results
tag = node.get('tag', '')
cls = node.get('cls', '')
attrs = node.get('attrs', {})
children = node.get('children', [])
text = node.get('text', '')
cur_path = f"{path}/{tag}"
for pattern in patterns:
if pattern.lower() in cls.lower() or pattern.lower() in str(attrs).lower():
child_count = len(children)
results.append({
'path': cur_path,
'depth': depth,
'tag': tag,
'cls': cls[:150],
'pattern': pattern,
'text_preview': text[:80] if text else '',
'child_count': child_count,
'attrs': {k:v[:50] for k,v in attrs.items() if k != 'style'}
})
for child in children:
find_by_class_pattern(child, patterns, cur_path, depth+1, results)
return results
def analyze_chat_structure(node, path="", depth=0):
"""Find the chat/conversation area by looking at the main layout."""
if not isinstance(node, dict):
return
tag = node.get('tag', '')
cls = node.get('cls', '')
children = node.get('children', [])
text = node.get('text', '')
attrs = node.get('attrs', {})
# Print interesting structural nodes at shallow depths
if depth <= 6:
child_count = len(children)
has_text = bool(text and len(text) > 10)
info = f"{' '*depth}{tag}"
if cls:
info += f" .{cls[:80]}"
if attrs:
attr_str = ' '.join(f'{k}={v[:30]}' for k,v in attrs.items() if k not in ('style','class'))
if attr_str:
info += f" [{attr_str}]"
info += f" children={child_count}"
if has_text:
info += f" text=\"{text[:50]}...\""
print(info)
for child in children:
analyze_chat_structure(child, f"{path}/{tag}", depth+1)
data, fname = load_dump()
if not data:
print("No dump file found!")
sys.exit(1)
# Handle both dump formats
body = data.get('body', data)
qi = data.get('quickInfo', {})
print("=" * 60)
print("QUICK INFO")
print("=" * 60)
if qi:
for k, v in qi.items():
if k == 'buttons':
print(f"buttons ({len(v)}):")
for b in v[:15]:
print(f" [{b.get('tag')}] \"{b.get('text','')[:50]}\" visible={b.get('visible')} cls={b.get('cls','')[:60]}")
elif k == 'dataAttrs':
print(f"dataAttrs: {v[:30]}")
else:
print(f"{k}: {v}")
print("\n" + "=" * 60)
print("CHAT-RELATED CLASS PATTERNS")
print("=" * 60)
patterns = ['chat', 'message', 'conversation', 'response', 'answer', 'reply',
'markdown', 'prose', 'content', 'panel', 'agent', 'assistant',
'planner', 'step', 'trajectory', 'bot', 'ai-', 'turn']
matches = find_by_class_pattern(body, patterns)
for m in matches:
print(f" [{m['tag']}] cls=\"{m['cls']}\" pattern={m['pattern']} children={m['child_count']} {m.get('attrs',{})}")
print("\n" + "=" * 60)
print("LONG TEXT NODES (potential AI responses)")
print("=" * 60)
texts = find_text_containers(body)
texts.sort(key=lambda x: x['text_len'], reverse=True)
for t in texts[:20]:
print(f" [{t['tag']}] depth={t['depth']} len={t['text_len']} cls=\"{t['cls'][:60]}\"")
print(f" text: \"{t['text_preview']}\"")
if t['attrs']:
print(f" attrs: {t['attrs']}")
print("\n" + "=" * 60)
print("DOM TREE (depth<=6)")
print("=" * 60)
analyze_chat_structure(body)

View File

@@ -0,0 +1,19 @@
import json, os, sys
dump_path = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html.json')
with open(dump_path, 'r', encoding='utf-8') as f:
data = json.load(f)
qi = data.get('quickInfo', {})
print('=== Quick Info ===')
print('hasConversationView:', qi.get('hasConversationView'))
print('hasStepIndex:', qi.get('hasStepIndex'))
print('hasBotColor:', qi.get('hasBotColor'))
print('hasMarkdownBody:', qi.get('hasMarkdownBody'))
print('hasProse:', qi.get('hasProse'))
print('totalElements:', qi.get('totalElements'))
print('dataTestIds:', qi.get('dataTestIds'))
print('dataAttrs (first 20):', qi.get('dataAttrs', [])[:20])
print('buttons (first 10):')
for b in qi.get('buttons', [])[:10]:
print(f" [{b.get('tag')}] {b.get('text', '')[:60]} visible={b.get('visible')}")

View File

@@ -0,0 +1,83 @@
"""Search AG Native DOM dump for chat content and buttons."""
import json, os
fpath = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html_5.json')
with open(fpath, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
body = data.get('body', data.get('bodyTree', {}))
qi = data.get('quickInfo', {})
# Show all buttons
print('=== BUTTONS ===')
for b in qi.get('buttons', []):
print(f' [{b["tag"]}] "{b["text"][:60]}" visible={b["visible"]} cls={b.get("cls","")[:80]}')
# Data attrs
print('\n=== DATA ATTRS ===')
for attr in qi.get('dataAttrs', []):
print(f' {attr}')
# Recursive search for nodes by text
def find_nodes_by_text(node, target, path='', results=None, depth=0):
if results is None: results = []
if not isinstance(node, dict): return results
tag = node.get('tag','')
cls = node.get('cls','')
text = node.get('text','')
children = node.get('children', [])
cur = f'{path}/{tag}'
if target.lower() in text.lower():
results.append({'path': cur, 'depth': depth, 'cls': cls[:80], 'text': text[:80], 'children': len(children)})
for c in children:
find_nodes_by_text(c, target, cur, results, depth+1)
return results
print('\n=== NODES containing "Always run" ===')
matches = find_nodes_by_text(body, 'Always run')
for m in matches:
print(f' depth={m["depth"]} cls="{m["cls"]}" text="{m["text"]}" children={m["children"]}')
print('\n=== NODES containing "Always" ===')
matches = find_nodes_by_text(body, 'Always')
for m in matches:
print(f' depth={m["depth"]} cls="{m["cls"]}" text="{m["text"]}" children={m["children"]}')
# Find ALL text nodes with > 30 chars
def find_all_text(node, results=None, depth=0, path=''):
if results is None: results = []
if not isinstance(node, dict): return results
tag = node.get('tag','')
cls = node.get('cls','')
text = node.get('text','')
children = node.get('children', [])
if text and len(text) > 30:
results.append({'depth': depth, 'tag': tag, 'cls': cls[:80], 'text': text[:100], 'path': f'{path}/{tag}'})
for c in children:
find_all_text(c, results, depth+1, f'{path}/{tag}')
return results
print('\n=== LONG TEXT NODES (>30 chars) ===')
texts = find_all_text(body)
texts.sort(key=lambda x: len(x['text']), reverse=True)
for t in texts[:25]:
print(f' d={t["depth"]} [{t["tag"]}] cls="{t["cls"][:50]}" len={len(t["text"])} "{t["text"][:80]}"')
# Find nodes with many children (structural containers)
def find_containers(node, results=None, depth=0, path=''):
if results is None: results = []
if not isinstance(node, dict): return results
tag = node.get('tag','')
cls = node.get('cls','')
children = node.get('children', [])
if len(children) > 5:
results.append({'depth': depth, 'tag': tag, 'cls': cls[:100], 'children': len(children), 'path': f'{path}/{tag}'})
for c in children:
find_containers(c, results, depth+1, f'{path}/{tag}')
return results
print('\n=== CONTAINERS (>5 children) ===')
conts = find_containers(body)
conts.sort(key=lambda x: x['children'], reverse=True)
for c in conts[:20]:
print(f' d={c["depth"]} [{c["tag"]}] children={c["children"]} cls="{c["cls"][:70]}"')

View File

@@ -0,0 +1,109 @@
"""Trace the DOM path from body to AI response container."""
import json, os
fpath = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html_5.json')
with open(fpath, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
body = data.get('body', data.get('bodyTree', {}))
def find_path_to_class(node, target_cls, path=None, depth=0):
"""Find the DOM path down to a node with a matching class."""
if path is None: path = []
if not isinstance(node, dict): return []
tag = node.get('tag', '')
cls = node.get('cls', '')
children = node.get('children', [])
text = node.get('text', '')
attrs = node.get('attrs', {})
entry = {
'depth': depth,
'tag': tag,
'cls': cls[:120],
'children': len(children),
'text': text[:60] if text else '',
'attrs': {k:v[:40] for k,v in attrs.items() if k not in ('style',)}
}
if target_cls.lower() in cls.lower():
return path + [entry]
for i, child in enumerate(children):
result = find_path_to_class(child, target_cls, path + [entry], depth+1)
if result:
return result
return []
# Find path to the AI response container
print("=== PATH TO 'leading-relaxed select-text' ===")
path = find_path_to_class(body, 'leading-relaxed select-text')
for p in path:
indent = ' ' * p['depth']
print(f'{indent}[{p["tag"]}] cls="{p["cls"]}" children={p["children"]} {p["attrs"]}')
if p['text']:
print(f'{indent} text: "{p["text"]}"')
# Now get the full subtree of the AI response container
def get_subtree(node, target_cls, depth=0):
if not isinstance(node, dict): return None
cls = node.get('cls', '')
if target_cls.lower() in cls.lower():
return node
for child in node.get('children', []):
result = get_subtree(child, target_cls, depth+1)
if result:
return result
return None
print("\n=== AI RESPONSE CONTAINER SUBTREE ===")
container = get_subtree(body, 'leading-relaxed select-text')
if container:
def print_tree(node, depth=0, max_depth=4):
if not isinstance(node, dict) or depth > max_depth: return
tag = node.get('tag','')
cls = node.get('cls','')[:80]
text = node.get('text','')
children = node.get('children', [])
indent = ' ' * depth
line = f'{indent}[{tag}]'
if cls: line += f' cls="{cls}"'
line += f' children={len(children)}'
if text: line += f' text="{text[:60]}"'
print(line)
for c in children:
print_tree(c, depth+1, max_depth)
print_tree(container, 0, 3)
# Also search for the chat panel container - what wraps the entire conversation
print("\n=== SEARCH FOR CHAT PANEL WRAPPERS ===")
chat_patterns = ['chat', 'antigravity', 'gemini', 'panel', 'agentview', 'sidebar', 'conversation']
for pat in chat_patterns:
path = find_path_to_class(body, pat)
if path:
last = path[-1]
print(f' Pattern "{pat}" found at depth={last["depth"]} [{last["tag"]}] cls="{last["cls"]}" children={last["children"]}')
# Find the parent chain from body to the container - look by scanning ALL class names
print("\n=== ALL UNIQUE CLASS NAMES (depth <= 12) ===")
all_classes = set()
def collect_classes(node, depth=0, max_depth=12):
if not isinstance(node, dict) or depth > max_depth: return
cls = node.get('cls', '')
if cls:
for c in cls.split():
if len(c) > 3 and not c.startswith('{') and 'mtk' not in c:
all_classes.add(c)
for child in node.get('children', []):
collect_classes(child, depth+1, max_depth)
collect_classes(body)
# Print classes sorted, grouped by potential relevance
relevant = sorted([c for c in all_classes if any(k in c.lower() for k in
['chat', 'message', 'response', 'agent', 'gemini', 'turn', 'model', 'user', 'bot', 'conversation', 'markdown', 'prose', 'text-', 'content'])])
print("Relevant classes:")
for c in relevant:
print(f' {c}')

View File

@@ -0,0 +1,217 @@
"""Vikunja safe task updater — preserves existing fields when updating tasks.
Usage:
python vikunja_helper.py done 75 # Mark task #75 as done
python vikunja_helper.py done 71 77 78 # Mark multiple tasks done
python vikunja_helper.py undone 75 # Mark task #75 as not done
python vikunja_helper.py comment 75 "text" # Add comment to task #75
python vikunja_helper.py desc 75 "text" # Set description (appends if exists)
python vikunja_helper.py create "title" "desc" --labels Backend,Priority:High
python vikunja_helper.py create "title" "desc" --done --labels Frontend,Priority:Mid
python vikunja_helper.py label 75 Backend Priority:High # Add labels to task
python vikunja_helper.py list # List all tasks
python vikunja_helper.py list todo # List TODO only
python vikunja_helper.py list done # List DONE only
"""
import sys
import json
import urllib.request
import urllib.error
import io
# Fix Windows console encoding (cp949 → utf-8)
if sys.stdout.encoding != "utf-8":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
# ============================================================
# ⚙️ CONFIGURATION — PROJECT_ID만 프로젝트별로 변경하세요
# ============================================================
API_BASE = "https://plan.variet.net/api/v1"
TOKEN = "tk_070f8e0b715e818bb7178c3815ed5389040eddca"
PROJECT_ID = 8 # gravity_control project
# ============================================================
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
# Label name → Vikunja label ID mapping
# Customize for your project's labels
LABEL_MAP = {
"Backend": 1, "Frontend": 2, "Engine": 3, "Infra": 4, "Test": 5,
"Priority:High": 6, "Priority:Mid": 7, "Priority:Low": 8,
"Agent": 17, "Tool": 18, "AI/LLM": 19,
}
def api_get(path: str):
req = urllib.request.Request(f"{API_BASE}{path}", headers=HEADERS)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))
def api_post(path: str, data: dict):
body = json.dumps(data).encode("utf-8")
req = urllib.request.Request(f"{API_BASE}{path}", data=body, headers=HEADERS, method="POST")
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))
def api_put(path: str, data: dict):
body = json.dumps(data).encode("utf-8")
req = urllib.request.Request(f"{API_BASE}{path}", data=body, headers=HEADERS, method="PUT")
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))
def get_task(task_id: int) -> dict:
return api_get(f"/tasks/{task_id}")
def safe_update_task(task_id: int, updates: dict) -> dict:
task = get_task(task_id)
safe_body = {
"title": task.get("title", ""),
"description": task.get("description", ""),
"priority": task.get("priority", 0),
"done": task.get("done", False),
}
safe_body.update(updates)
return api_post(f"/tasks/{task_id}", safe_body)
def mark_done(task_ids: list):
for tid in task_ids:
result = safe_update_task(tid, {"done": True})
title = result.get("title", "?")
print(f" ✅ #{tid} → done=True [{title}]")
def mark_undone(task_ids: list):
for tid in task_ids:
result = safe_update_task(tid, {"done": False})
title = result.get("title", "?")
print(f" ⬜ #{tid} → done=False [{title}]")
def add_comment(task_id: int, comment: str):
result = api_put(f"/tasks/{task_id}/comments", {"comment": comment})
print(f" 💬 #{task_id} comment added (id={result.get('id', '?')})")
def set_description(task_id: int, desc: str, append: bool = True):
task = get_task(task_id)
existing = task.get("description", "") or ""
if append and existing:
new_desc = existing.rstrip() + "\n\n" + desc
else:
new_desc = desc
result = safe_update_task(task_id, {"description": new_desc})
print(f" 📝 #{task_id} description updated [{result.get('title', '?')}]")
def list_tasks(filter_: str = "all"):
all_tasks = []
page = 1
while True:
batch = api_get(f"/projects/{PROJECT_ID}/tasks?per_page=50&page={page}")
if not batch:
break
all_tasks.extend(batch)
if len(batch) < 50:
break
page += 1
if filter_ == "todo":
all_tasks = [t for t in all_tasks if not t["done"]]
elif filter_ == "done":
all_tasks = [t for t in all_tasks if t["done"]]
all_tasks.sort(key=lambda t: t["id"])
for t in all_tasks:
status = "" if t["done"] else ""
desc = (t.get("description") or "")[:50].replace("\n", " ")
labels = ", ".join(l["title"] for l in (t.get("labels") or []))
print(f" {status} #{t['id']:3d} {t['title'][:40]:<40} [{labels}] {desc}")
print(f"\n Total: {len(all_tasks)} tasks")
def add_labels(task_id: int, label_names: list):
for name in label_names:
label_id = LABEL_MAP.get(name)
if not label_id:
print(f" ⚠️ Unknown label '{name}'. Valid: {', '.join(LABEL_MAP.keys())}")
continue
try:
api_put(f"/tasks/{task_id}/labels", {"label_id": label_id})
print(f" 🏷️ #{task_id} + {name} (id={label_id})")
except Exception as e:
if "already" in str(e).lower() or "409" in str(e):
print(f" 🏷️ #{task_id} already has {name}")
else:
print(f" ⚠️ #{task_id} label {name} failed: {e}")
def create_task(title: str, description: str = "", done: bool = False, labels: list = None):
payload = {"title": title, "description": description}
result = api_put(f"/projects/{PROJECT_ID}/tasks", payload)
task_id = result["id"]
print(f" ✨ #{task_id} created: {result.get('title', '?')}")
if labels:
add_labels(task_id, labels)
if done:
result = safe_update_task(task_id, {"done": True})
print(f" ✅ #{task_id} → done=True")
return result
def main():
if len(sys.argv) < 2:
print(__doc__)
return
cmd = sys.argv[1].lower()
if cmd == "done":
ids = [int(x) for x in sys.argv[2:]]
mark_done(ids)
elif cmd == "undone":
ids = [int(x) for x in sys.argv[2:]]
mark_undone(ids)
elif cmd == "comment":
add_comment(int(sys.argv[2]), sys.argv[3])
elif cmd == "desc":
set_description(int(sys.argv[2]), sys.argv[3])
elif cmd == "list":
f = sys.argv[2] if len(sys.argv) > 2 else "all"
list_tasks(f)
elif cmd == "label":
if len(sys.argv) < 4:
print("Usage: vikunja_helper.py label TASK_ID Label1 Label2 ...")
return
add_labels(int(sys.argv[2]), sys.argv[3:])
elif cmd == "create":
title = sys.argv[2] if len(sys.argv) > 2 else ""
desc = sys.argv[3] if len(sys.argv) > 3 and not sys.argv[3].startswith("--") else ""
is_done = "--done" in sys.argv
labels = None
for i, arg in enumerate(sys.argv):
if arg == "--labels" and i + 1 < len(sys.argv):
labels = sys.argv[i + 1].split(",")
break
if not title:
print("Error: title is required")
return
create_task(title, desc, done=is_done, labels=labels)
else:
print(f"Unknown command: {cmd}")
print(__doc__)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,100 @@
"""Gitea Wiki helper: list, read, create, update wiki pages.
Usage:
wiki_helper.py list — list all pages
wiki_helper.py read <title> — read a page
wiki_helper.py create <title> <file> — create a page from file
wiki_helper.py update <title> <file> — update a page from file
"""
import sys, io, json, base64, urllib.request, urllib.error
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# ============================================================
# ⚙️ CONFIGURATION — GITEA_REPO만 프로젝트별로 변경하세요
# ============================================================
GITEA_BASE_URL = "https://git.variet.net"
GITEA_OWNER = "Variet"
GITEA_REPO = "gravity_control" # ← 프로젝트별 변경 필요
GITEA_TOKEN = "3a01b4b15a39921572e64c413353e870d4d2161b"
# ============================================================
BASE = f"{GITEA_BASE_URL}/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}/wiki"
HEADERS = {"Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json"}
def _req(method, path, data=None):
url = f"{BASE}{path}"
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(url, data=body, headers=HEADERS, method=method)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
err = e.read().decode()
print(f" ⚠️ HTTP {e.code}: {err}")
return None
def _find_sub_url(title):
pages = _req("GET", "/pages")
if pages:
for p in pages:
if p.get("title", "").lower() == title.lower():
return p.get("sub_url", title)
return title
def list_pages():
pages = _req("GET", "/pages")
if pages:
print(f"=== {len(pages)} Wiki Pages ===")
for p in pages:
print(f" {p.get('title', '?')}")
return pages
def read_page(title):
sub = _find_sub_url(title)
page = _req("GET", f"/page/{sub}")
if page and page.get("content_base64"):
content = base64.b64decode(page["content_base64"]).decode("utf-8")
return content
return None
def create_page(title, content):
data = {
"title": title,
"content_base64": base64.b64encode(content.encode()).decode(),
}
result = _req("POST", "/new", data)
if result:
print(f" ✅ Created wiki page: {title}")
return result
def update_page(title, content):
sub = _find_sub_url(title)
data = {
"title": title,
"content_base64": base64.b64encode(content.encode()).decode(),
}
result = _req("PATCH", f"/page/{sub}", data)
if result:
print(f" ✅ Updated wiki page: {title}")
return result
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
if cmd == "list":
list_pages()
elif cmd == "read" and len(sys.argv) > 2:
content = read_page(sys.argv[2])
if content:
print(content[:5000])
else:
print(f" Page '{sys.argv[2]}' not found")
elif cmd == "create" and len(sys.argv) > 3:
with open(sys.argv[3], "r", encoding="utf-8") as f:
create_page(sys.argv[2], f.read())
elif cmd == "update" and len(sys.argv) > 3:
with open(sys.argv[3], "r", encoding="utf-8") as f:
update_page(sys.argv[2], f.read())
else:
print("Usage: wiki_helper.py list|read <title>|create <title> <file>|update <title> <file>")

View File

@@ -0,0 +1,39 @@
---
description: 모든 구현 작업 전 실행하는 사전 점검 체크리스트 (pre-task, 준비, 시작 전, 계획, 구현)
---
# Pre-Task Checklist
> [!IMPORTANT]
> 코딩을 시작하기 전에 반드시 이 체크리스트를 순서대로 완료하세요.
> 체크리스트를 건너뛸 경우 불필요한 시행착오가 발생합니다.
## 1단계: 요구사항 정리
- [ ] 유저 요청을 구체적 작업 항목으로 분해
- [ ] 변경 범위(scope)를 명확히 정의 (영향받는 파일/모듈)
- [ ] 성공 기준(acceptance criteria) 확인
## 2단계: 레퍼런스 확인 (추측 금지)
- [ ] `.agents/references/architecture.md` — 현재 아키텍처 확인
- [ ] `.agents/references/tech-stack.md` — 기술 스택 및 버전 확인
- [ ] `.agents/references/conventions.md` — 코딩 컨벤션 확인
- [ ] `.agents/references/known-issues.md` — 과거 실패 패턴 확인
- [ ] 관련 기존 코드 최소 3개 파일 읽기
> [!CAUTION]
> 레퍼런스 문서가 존재하는 주제에 대해 추측하지 마세요.
> 문서가 없으면 유저에게 확인을 요청하세요.
## 3단계: 계획 수립
- [ ] 변경할 파일 목록 작성
- [ ] 의존성 순서 파악 (어떤 파일부터 수정해야 하는가?)
- [ ] 리스크 식별 (어디서 실패할 가능성이 높은가?)
- [ ] 테스트 방법 결정 (어떻게 검증할 것인가?)
## 4단계: 유저 확인
- [ ] 계획을 유저에게 보고하고 승인받기 (변경 파일 3개 이상인 경우)
- [ ] 작은 변경은 바로 실행하되, 변경 내용을 명확히 설명

View File

@@ -0,0 +1,128 @@
---
description: 프로젝트 연동 서비스 URL, API 키, 프로젝트 정보 참조
---
# 서비스 연동 정보
> [!CAUTION]
> 이 파일에는 API 토큰이 포함되어 있습니다. 프라이빗 레포에서만 사용하세요.
## 로컬 환경
| 항목 | 값 |
|------|-----|
| **Node.js** | 시스템 설치 (`node`, `npm`) |
| **Python** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` (**항상 이 경로 사용**) |
| **Shell** | PowerShell (`curl` = `Invoke-WebRequest` 별칭이므로 반드시 **`curl.exe`** 사용) |
## Gitea (Git Repository)
| 항목 | 값 |
|------|-----|
| **Base URL** | `https://git.variet.net` |
| **API Base** | `https://git.variet.net/api/v1` |
| **Repo** | `Variet/gravity_control` |
| **Token** | `3a01b4b15a39921572e64c413353e870d4d2161b` |
| **Auth Header** | `-H "Authorization: token 3a01b4b15a39921572e64c413353e870d4d2161b"` |
## Vikunja (Task Management)
| 항목 | 값 |
|------|-----|
| **Base URL** | `https://plan.variet.net` |
| **API Base** | `https://plan.variet.net/api/v1` |
| **Project ID** | `8` |
| **Token** | `tk_070f8e0b715e818bb7178c3815ed5389040eddca` |
| **Auth Header** | `-H "Authorization: Bearer tk_070f8e0b715e818bb7178c3815ed5389040eddca"` |
## Vikunja 태스크 조회
> [!TIP]
> 태스크 목록은 항상 라이브 조회를 사용합니다. 하드코딩된 매핑은 유지하지 않습니다.
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list todo
```
## 기타 서비스
| 서비스 | URL | 용도 |
|--------|-----|------|
| Uptime Kuma | `https://status.variet.net` | 서비스 모니터링 |
| Authentik | `https://auth.variet.net` | SSO 인증 |
## AI 작업 프로토콜
> [!IMPORTANT]
> 아래 규칙은 모든 작업에 자동 적용됩니다. 유저가 별도 지시하지 않아도 따릅니다.
### Vikunja = Single Source of Truth (SSOT)
- **Vikunja가 유일한 작업 현황 관리 도구**입니다.
- 로컬 `task.md`는 현재 대화 내 세부 체크리스트용으로만 사용합니다.
- 새 TODO 발견 시 → Vikunja에 태스크 생성 (로컬 파일에만 적는 것은 금지)
- 작업 완료 시 → Vikunja 태스크 완료 처리 (로컬 체크만 하는 것은 금지)
### Vikunja 태깅 규칙
태스크 생성 시 반드시 아래 라벨을 적절히 부여합니다:
**영역 라벨 (필수, 1개 이상):**
| ID | 라벨 | 적용 대상 |
|:--:|-------|-----------:|
| 1 | `Backend` | 서버, DB, API |
| 2 | `Frontend` | UI, 웹 프론트엔드 |
| 3 | `Engine` | 핵심 엔진/로직 |
| 4 | `Infra` | Docker, CI/CD, 모니터링 |
| 5 | `Test` | 테스트, E2E |
**우선순위 라벨 (필수, 1개):**
| ID | 라벨 | 기준 |
|:--:|-------|------:|
| 6 | `Priority:High` | 핵심 기능 미완성, 블로커 |
| 7 | `Priority:Mid` | 기능 개선, UX 향상, 리팩터링 |
| 8 | `Priority:Low` | nice-to-have, 문서, 코드 정리 |
**태스크 제목 규칙:**
- 한글 + 핵심 키워드 (예: `WebSocket 재연결 로직 구현`)
- 50자 이내
### 작업 시작 시
1. `git pull` 으로 최신 코드 동기화
2. Vikunja 태스크 조회 (`/check-vikunja`) → 관련 태스크 ID 확인
3. 관련 태스크가 있으면 Vikunja에서 진행중 표시
4. 관련 태스크가 없으면 Vikunja에 새 태스크 생성 (태깅 규칙 준수)
### 작업 중
5. 의미 있는 단위마다 자주 커밋 (대규모 변경을 한번에 커밋하지 않음)
6. 커밋 메시지 규칙:
- `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, `ci:` 접두사 사용
- 관련 Vikunja 태스크가 있으면 `#task-{ID}` 참조 포함
- 예: `feat(server): WebSocket 재연결 로직 #task-21`
### 작업 완료 시
7. 모든 변경사항 커밋 + `git push`
8. Vikunja 태스크 완료 처리 (**반드시 `vikunja_helper.py` 사용**):
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py done {TASK_ID}
```
> [!CAUTION]
> **직접 `Invoke-RestMethod -Body '{"done": true}'` 사용 금지!**
> Vikunja API는 POST 시 body에 없는 필드를 빈값으로 덮어씁니다.
9. 작업 중 발견된 새 TODO → Vikunja에 태스크 생성
### 멀티 AI 협업 시 추가 규칙
- 작업 전 `git pull` 필수 (다른 AI가 push한 변경 반영)
- 같은 파일을 동시에 수정하지 않음
- 공유 인터페이스 수정 시 즉시 commit + push
- 충돌 발생 시 유저에게 확인 요청
## PowerShell 주의사항
- `curl` → PowerShell에서 `Invoke-WebRequest`의 별칭. **반드시 `curl.exe`** 사용
- `npm` → PowerShell에서 실행 정책 문제 시 `cmd /c npm` 사용
- JSON 파이프 파싱 시 PowerShell 이스케이핑 문제 → `.py` 스크립트 파일로 만들어 실행 권장

View File

@@ -0,0 +1,66 @@
---
description: 세션 시작 시 프로젝트 맥락을 빠르게 복구하는 워크플로우 (시작, continue, 이어서, 작업 시작)
---
# 세션 시작 프로토콜
새 대화 시작, "continue", "이어서", "작업 시작" 등 요청 시 이 워크플로우를 실행합니다.
// turbo-all
## 절차
### 0. 에이전트 룰 & 맥락 로딩 (자동)
`.agents/AGENT.md`를 읽고 에이전트 행동 규칙을 로딩합니다.
`.agents/references/known-issues.md`를 읽어 최근 이슈를 파악합니다.
`.agents/workflows/services.md`**로컬 환경** 섹션을 읽고 Python 경로 등 환경 설정을 확인합니다.
### 1. Devlog 맥락 복구
오늘 + 어제 devlog index를 읽고 최근 작업 흐름을 파악합니다.
```powershell
$today = Get-Date -Format "yyyy-MM-dd"
$yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd")
if (Test-Path "docs\devlog\$today.md") {
Write-Host "=== Devlog: $today ==="
Get-Content "docs\devlog\$today.md"
} elseif (Test-Path "docs\devlog\$yesterday.md") {
Write-Host "=== Devlog: $yesterday (no entry for today yet) ==="
Get-Content "docs\devlog\$yesterday.md"
} else {
Write-Host "=== No recent devlog found ==="
}
```
미완료(🔧) 항목이 있으면 해당 entry 파일을 읽어 이어받기 맥락을 확보합니다:
- Entry 경로: `docs/devlog/entries/YYYYMMDD-NNN.md`
### 2. Git 상태 확인
```powershell
git status --short
```
```powershell
git log --oneline -5
```
### 3. Vikunja TODO 태스크
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe .agents\workflows\helpers\vikunja_helper.py list todo
```
### 4. 종합 보고
결과를 종합하여 사용자에게 보고:
- 마지막 작업 맥락 + 미완료 항목 (devlog 🔧 기반)
- TODO 태스크 목록 (라벨 + 우선순위)
- 다음 작업 제안
**우선순위 판단 기준** (라벨만으로 판단 금지):
- P0: 최근 커밋에서 스키마/모델/인터페이스 변경 → 연쇄 영향 점검
- P1: 서버 기동/API 응답 장애
- P2: 기능 미완성/UX 개선
- P3: 정확도 향상, 신규 기능, CI/CD, 문서 정리

1
.deps_installed Normal file
View File

@@ -0,0 +1 @@

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
__pycache__/
*.pyc
.env
*.vsix
extension/node_modules/
extension/out/
.deps_installed
gravity_control.log
*.tar.gz

View File

@@ -1,17 +1,31 @@
# Discord Bot Token # Discord Bot Token (필수)
DISCORD_TOKEN=your_discord_bot_token_here DISCORD_TOKEN=your_discord_bot_token_here
# Discord Guild (서버) ID — 봇이 채널을 생성할 서버 # Discord Guild (서버) ID (필수) — 봇이 채널을 생성할 서버
DISCORD_GUILD_ID= DISCORD_GUILD_ID=
# Antigravity Brain Path # Bridge 디렉토리 (기본값: ~/.gemini/antigravity/bridge)
BRAIN_PATH=C:\Users\Certes\.gemini\antigravity\brain # 보통 수정 불필요 — Extension과 동일 경로 사용
BRIDGE_PATH=
# Antigravity Brain Path (Watcher용)
BRAIN_PATH=
# 세션 활성 판단: 마지막 파일 변경으로부터 이 시간(초) 이내면 활성 # 세션 활성 판단: 마지막 파일 변경으로부터 이 시간(초) 이내면 활성
ACTIVE_TIMEOUT_SECONDS=300 ACTIVE_TIMEOUT_SECONDS=300
# Project name (used for Discord channel: AG-{PROJECT_NAME})
PROJECT_NAME=gravity_control
# Watcher Settings # Watcher Settings
DEBOUNCE_SECONDS=2 DEBOUNCE_SECONDS=2
# Bot mode: 'local' (default, file-based) or 'gateway' (서버 Docker + WS Hub)
BOT_MODE=local
# Gateway API Key (보안)
# 서버와 Collector에 동일한 키를 설정하세요
# 생성: python -c "import secrets; print(secrets.token_urlsafe(32))"
GATEWAY_API_KEY=
# Hub WebSocket 인증 (선택 — 미설정 시 인증 생략)
# 생성: python -c "import secrets; print(secrets.token_hex(32))"
GRAVITY_HUB_SECRET=
GRAVITY_REGISTRATION_CODE=

3
.gitignore vendored
View File

@@ -11,8 +11,6 @@ build/
.venv/ .venv/
venv/ venv/
# Agents (contains tokens)
.agents/
# IDE # IDE
.vscode/ .vscode/
@@ -28,3 +26,4 @@ Thumbs.db
# Node # Node
node_modules/ node_modules/
extension/out/ extension/out/
*.vsix

BIN
.gitlog.txt Normal file

Binary file not shown.

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt aiohttp>=3.9.0
# Copy application code (all Python modules)
COPY *.py ./
# Default environment (can be overridden via docker-compose)
ENV BOT_MODE=gateway
ENV GATEWAY_PORT=8585
EXPOSE 8585
CMD ["python", "main.py"]

View File

@@ -0,0 +1,2 @@
custom:
- https://github.com/Kanezal/antigravity-sdk#support

View File

@@ -0,0 +1,49 @@
name: Deploy TypeDoc to GitHub Pages
on:
push:
branches: [main]
paths:
- 'src/**'
- 'package.json'
- 'tsconfig.json'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npx typedoc --out docs-site src/index.ts --tsconfig tsconfig.json
- uses: actions/upload-pages-artifact@v3
with:
path: docs-site
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4

15
antigravity-sdk-main/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
node_modules/
dist/
docs-site/
*.tsbuildinfo
.DS_Store
# Internal reference — not for public repo
GEMINI.md
docs/implementation-plan.md
docs/internals.md
scripts/
# Extensions (separate repo)
example-extension/
xray-extension/

View File

@@ -0,0 +1,68 @@
# Legal Notice
## Disclaimer
This project is an unofficial, community-maintained SDK for building extensions
for [Antigravity IDE](https://antigravity.dev). It is **not affiliated with,
endorsed by, or sponsored by Google LLC or any of its subsidiaries.**
## Nature of the Project
Antigravity SDK provides a **TypeScript library** for VS Code extension
developers who want to build tools that work within Antigravity IDE.
The SDK interacts with Antigravity exclusively through:
- **VS Code Extension API** — the standard, documented `vscode.*` namespace
that all extensions use
- **Registered commands** — commands exposed by Antigravity through the
standard `vscode.commands` interface
- **Local state files** — reading (not writing) locally stored settings
## Compliance
- This SDK **does not access** Google's backend servers, gRPC endpoints,
or authentication systems directly.
- This SDK **does not extract** AI models, training data, weights, or
proprietary algorithms.
- This SDK **does not bypass** security features, licensing, rate limits,
or usage restrictions.
- This SDK **does not proxy** or relay requests to Google's infrastructure.
- All communication goes through Antigravity's own extension host — the same
mechanism used by any VS Code extension.
## Interoperability
This SDK is developed to enable interoperability between Antigravity IDE
and third-party extensions, as provided by:
- **EU Software Directive** (Directive 2009/24/EC), Article 6 — permits
analysis of software for the purpose of achieving interoperability
- **UK Copyright, Designs and Patents Act 1988**, Section 50B
- Similar provisions in other jurisdictions
The API interfaces documented in this project were derived through observation
of Antigravity's public extension API surface — the same surface available to
any VS Code extension running inside Antigravity.
## User Responsibility
Users and extension developers are responsible for ensuring their use of
this SDK and any extensions built with it comply with applicable terms of
service and local laws.
Extension developers should:
1. Not use the SDK to access Google's backend directly
2. Not use the SDK to extract or replicate AI model behavior
3. Not use the SDK to bypass security or licensing restrictions
4. Follow Antigravity's extension guidelines where applicable
## Takedown
If Google or the Antigravity team requests removal of this project, we will
comply promptly. Contact: [open a GitHub issue](https://github.com/Kanezal/antigravity-sdk/issues).
## License
This project is released under the [GNU Affero General Public License v3.0](LICENSE).

View File

@@ -0,0 +1,644 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they receive
widespread use, become available for other developers to incorporate.
Many developers of free software are heartened and encouraged by the
resulting cooperation. However, in the case of software used on network
servers, this result may fail to come about. The GNU General Public
License permits making a modified version and letting the public access
it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding Source
of the work are being offered to the general public at no charge under
subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied by
the Installation Information. But this requirement does not apply if
neither you nor any third party retains the ability to install modified
object code on the User Product (for example, the work has been
installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in source
code form), and must require no special password or key for unpacking,
reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR
DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR
A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Antigravity SDK
Copyright (C) 2026 Kanezal
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,348 @@
<div align="center">
# Antigravity SDK
**Community SDK for building extensions for [Antigravity IDE](https://antigravity.dev)**
[![npm](https://img.shields.io/npm/v/antigravity-sdk)](https://www.npmjs.com/package/antigravity-sdk)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-Support%20this%20project-ff69b4?logo=githubsponsors&logoColor=white)](https://github.com/Kanezal/antigravity-sdk#support)
*Build powerful extensions that work alongside Antigravity's AI agent.*
</div>
---
## What is this?
A TypeScript SDK for building **VS Code extensions** that extend Antigravity IDE. It gives you programmatic access to the agent's conversations, preferences, step control, real-time activity monitoring, and a declarative API for integrating custom UI directly into the Agent View — all through Antigravity's own extension protocols.
> [!IMPORTANT]
> This SDK is designed **exclusively** for building Antigravity extensions. It is **not** a tool for integrating Antigravity with third-party applications, extracting data, or proxying requests. See [Compliance](#compliance).
---
## Quick Start
```bash
npm install antigravity-sdk
```
```typescript
import { AntigravitySDK } from 'antigravity-sdk';
export async function activate(context: vscode.ExtensionContext) {
const sdk = new AntigravitySDK(context);
await sdk.initialize();
// List conversations with real titles
const sessions = await sdk.cascade.getSessions();
console.log(`${sessions.length} conversations`);
// Read all 16 agent preferences
const prefs = await sdk.cascade.getPreferences();
console.log('Terminal policy:', prefs.terminalExecutionPolicy);
// Monitor agent activity in real time
sdk.monitor.onStepCountChanged((e) => {
console.log(`${e.title}: +${e.delta} steps`);
});
sdk.monitor.onActiveSessionChanged((e) => {
console.log(`Switched to: ${e.title}`);
});
sdk.monitor.start();
// Accept/reject agent steps programmatically
await sdk.cascade.acceptStep();
await sdk.cascade.acceptTerminalCommand();
context.subscriptions.push(sdk);
}
```
---
## Features
### Agent View UI Integration
The SDK provides **9 integration points** in the Agent View panel — add buttons, metadata, badges, menu items, and interactive elements with a fluent, declarative API. Everything is theme-aware and survives Antigravity updates via auto-repair.
```typescript
import { IntegrationManager, IntegrationPoint } from 'antigravity-sdk';
const ui = new IntegrationManager();
// Fluent API — chain calls
ui.addTopBarButton('stats', '📊', 'Show Stats', {
title: 'Session Stats',
rows: [{ key: 'Steps:', value: '42' }],
})
.addInputButton('tokens', '🔢', 'Token Counter')
.addTurnMetadata('meta', ['turnNumber', 'userCharCount', 'aiCharCount', 'codeBlocks'])
.addUserBadges('badges', 'charCount')
.addBotAction('inspect', '🔍', 'Inspect Response')
.addDropdownItem('export', 'Export Chat', '📋')
.addTitleInteraction('title', 'dblclick', 'Double-click to bookmark');
await ui.install();
ui.enableAutoRepair(); // Survives Antigravity updates
```
| Integration Point | Location | Use Cases |
|-------------------|----------|-----------|
| `TOP_BAR` | Header icon bar | Session overview, navigation |
| `TOP_RIGHT` | Before close button | Status indicators, quick toggle |
| `INPUT_AREA` | Next to send button | Token counter, prompt templates |
| `BOTTOM_ICONS` | Bottom icon row | Mode switches, quick actions |
| `TURN_METADATA` | Inside each turn | Character count, code block stats, turn numbers |
| `USER_BADGE` | User message bubble | Message length indicator |
| `BOT_ACTION` | Next to Good/Bad | Response analysis, copy actions |
| `DROPDOWN_MENU` | 3-dot overflow menu | Export, settings, debug tools |
| `CHAT_TITLE` | Conversation title | Rename, bookmark on interaction |
> [!NOTE]
> The integration script runs in the renderer process, independent of the extension. The SDK uses a **heartbeat mechanism** to prevent orphaned integrations: `sdk.initialize()` refreshes a timestamp marker, and the script silently exits if the marker is stale (48h). Disabling your extension will automatically stop the integration on the next IDE restart after the grace period.
### Conversation Management
Full control over Cascade conversations — list, create, switch, send messages, and manage agent steps.
```typescript
// List sessions with titles, step counts, timestamps
const sessions = await sdk.cascade.getSessions();
// Switch to a conversation
await sdk.cascade.focusSession(sessions[0].id);
// Send a message to the active chat
await sdk.cascade.sendPrompt('Analyze this file');
// Create a background conversation
const id = await sdk.cascade.createBackgroundSession('Run tests quietly');
```
### Real-Time Event Monitoring
Watch for state changes as they happen — new conversations, step progress, session switches, preference updates.
```typescript
// Agent made progress (added steps)
sdk.monitor.onStepCountChanged((e) => {
statusBar.text = `${e.title}: step ${e.newCount}`;
});
// User switched to a different conversation
sdk.monitor.onActiveSessionChanged((e) => {
console.log(`Now viewing: ${e.title}`);
});
// New conversation created
sdk.monitor.onNewConversation(() => {
console.log('New conversation detected');
});
// Any USS state changed (preferences, settings, etc.)
sdk.monitor.onStateChanged((e) => {
console.log(`${e.key}: ${e.previousSize}${e.newSize} bytes`);
});
sdk.monitor.start(3000, 5000); // USS poll: 3s, trajectory poll: 5s
```
### Agent Step Control
Programmatically accept, reject, or run agent actions — build approval workflows, auto-accept policies, or custom review UIs.
```typescript
await sdk.cascade.acceptStep(); // Accept code edit
await sdk.cascade.rejectStep(); // Reject code edit
await sdk.cascade.acceptTerminalCommand(); // Accept terminal command
await sdk.cascade.rejectTerminalCommand(); // Reject terminal command
await sdk.cascade.runTerminalCommand(); // Run pending command
await sdk.cascade.acceptCommand(); // Accept non-terminal action
```
### State & Preferences
Read the agent's current settings — terminal policies, secure mode, sandbox config, and more. Decoded directly from protobuf sentinel values.
```typescript
const prefs = await sdk.cascade.getPreferences();
prefs.terminalExecutionPolicy // OFF | AUTO | EAGER
prefs.artifactReviewPolicy // ALWAYS | TURBO | AUTO
prefs.secureModeEnabled // boolean
prefs.terminalSandboxEnabled // boolean
prefs.shellIntegrationEnabled // boolean
prefs.allowNonWorkspaceFiles // boolean
// ... 16 preferences total
```
### IDE Diagnostics
Access system information, extension logs, and recent conversation metadata.
```typescript
const diag = await sdk.cascade.getDiagnostics();
console.log(diag.systemInfo.operatingSystem);
console.log(diag.systemInfo.userName);
console.log(diag.isRemote); // SSH?
// MCP URL, browser port, git status
const mcpUrl = await sdk.cascade.getMcpUrl();
const browserPort = await sdk.cascade.getBrowserPort();
const ignored = await sdk.cascade.isFileGitIgnored('secret.env');
```
### Headless Cascade (LSBridge)
Create and manage conversations programmatically through the Language Server — no UI flicker, no panel switching.
```typescript
import { Models } from 'antigravity-sdk';
// Create a headless cascade with model selection
const cascadeId = await sdk.ls.createCascade({
text: 'Analyze test coverage in this project',
model: Models.GEMINI_FLASH,
});
// Send follow-up messages
await sdk.ls.sendMessage({
cascadeId,
text: 'Now fix the failing tests',
model: Models.GEMINI_PRO_HIGH,
});
// Focus in UI when ready
await sdk.ls.focusCascade(cascadeId);
// Or make raw RPC calls to any of the 68 verified LS methods
const status = await sdk.ls.getUserStatus();
const cascades = await sdk.ls.listCascades();
```
> [!NOTE]
> LSBridge auto-discovers the Language Server port and CSRF token from the running LS process. If auto-discovery fails (sandboxed environments), use `sdk.ls.setConnection(port, csrfToken)` manually.
---
## Architecture
```
Your Extension
┌──────────────────────────────────────────┐
│ antigravity-sdk │
│ │
│ sdk.cascade ← CascadeManager │
│ Sessions, preferences, step control │
│ │
│ sdk.monitor ← EventMonitor │
│ USS polling, trajectory tracking │
│ │
│ sdk.integration ← IntegrationManager │
│ Declarative UI for Agent View │
│ │
│ sdk.commands ← CommandBridge │
│ 60+ verified Antigravity commands │
│ │
│ sdk.state ← StateBridge │
│ Read-only access to USS preferences │
│ │
│ sdk.ls ← LSBridge │
│ Local LS communication (advanced) │
│ │
└────────────────────────────────────────-─┘
vscode.commands.executeCommand()
+ read-only state.vscdb (sql.js)
```
> [!NOTE]
> The SDK uses `sql.js` (pure JS/WASM SQLite) instead of `better-sqlite3` because Antigravity's Electron ABI (v140 / Node v22.21.1) is incompatible with native modules. This was verified in runtime.
---
## Compliance
> [!CAUTION]
> **Token extraction is a violation of Google's Terms of Service.**
>
> The SDK **actively blocks** access to authentication tokens (`oauthToken`, `agentManagerInitState`, and other sensitive keys). Any attempt to read these keys will throw an error.
>
> Extracting, storing, forwarding, or reusing Antigravity OAuth tokens — directly or through third-party tools — violates Google's TOS and may result in account termination.
### What this SDK is for
- Building **VS Code extensions** that run inside Antigravity IDE
- Extending Antigravity's functionality for your own workflows
- Adding custom UI elements to the Agent View
- Monitoring and automating agent step approval
- Reading preferences and conversation metadata
### What this SDK is NOT for
- Integrating Antigravity with external applications or services
- Proxying or relaying requests to Google's infrastructure
- Extracting AI model outputs for training other models
- Accessing Google's backend servers, gRPC endpoints, or auth systems
- Building alternative clients or wrappers around Antigravity
### How it works
All SDK communication goes through three safe, local channels:
1. **`vscode.commands.executeCommand()`** — the standard VS Code Extension API that all extensions use. Antigravity decides what to execute.
2. **Read-only local state** — the SDK reads `state.vscdb` for preferences and metadata, never writes.
3. **Local Language Server** — the SDK communicates with the LS process on `127.0.0.1` using the same ConnectRPC protocol that Antigravity itself uses. Authentication is via an ephemeral per-session CSRF token (not the user's OAuth token). No data leaves the local machine through this channel.
The SDK includes a `SENSITIVE_KEYS` blocklist that prevents extension developers from accidentally (or intentionally) accessing authentication data.
---
## Documentation
- **[GEMINI.md](GEMINI.md)** — Full internal architecture docs, verified DOM selectors, protobuf schemas
- **[LEGAL.md](LEGAL.md)** — Legal notice, interoperability rights, compliance details
- **[API Reference](https://kanezal.github.io/antigravity-sdk)** — TypeDoc (coming soon)
---
## Contributing
This is a community project. PRs welcome!
1. Fork the repo
2. Create a feature branch
3. Follow the existing code style
4. Add JSDoc comments for all public methods
5. Submit a PR
---
## Disclaimer
> [!WARNING]
> This project is not affiliated with Google or the Antigravity team. The SDK interacts with Antigravity through its existing extension API and local state files. Use at your own risk and in compliance with applicable terms of service.
---
## ❤️ Support
If you find this project useful and want to support its development, you can send **USDT** to:
| Network | Address |
|---------|---------|
| **TON** | `UQCjVh3C3mZc44GjT2IDsS4pmeOoUgRNxWMcb85NS5Bz_v1d` |
| **TRON (TRC20)** | `TH3JKGjNrSDCsjkkSuneaSMZoJYF7CNTXD` |
---
## License
[AGPL-3.0-or-later](LICENSE)

3106
antigravity-sdk-main/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
{
"name": "antigravity-sdk",
"version": "1.6.0",
"description": "Community SDK for building extensions for Antigravity IDE",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint src/",
"docs": "typedoc --out docs-site src/index.ts",
"prepublishOnly": "npm run build"
},
"keywords": [
"antigravity",
"antigravity-ide",
"google-antigravity",
"sdk",
"cascade",
"ai-agent",
"vscode-extension"
],
"author": "Kanezal",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "git+https://github.com/Kanezal/antigravity-sdk.git"
},
"homepage": "https://kanezal.github.io/antigravity-sdk",
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@types/vscode": "^1.85.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.85.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.0.0",
"tsup": "^8.0.0",
"typedoc": "^0.27.0",
"typescript": "^5.0.0"
},
"dependencies": {
"sql.js": "^1.14.0"
}
}

View File

@@ -0,0 +1,527 @@
/**
* Cascade Manager — Session listing, creation, and monitoring.
*
* Provides high-level API to interact with Cascade conversations
* using verified transport layer (CommandBridge + StateBridge).
*
* VERIFIED 2026-02-28: getDiagnostics.recentTrajectories returns clean JSON
* with { googleAgentId, trajectoryId, summary, lastStepIndex, lastModifiedTime }.
*
* @module cascade/cascade-manager
*/
import { IDisposable, DisposableStore } from '../core/disposable';
import { EventEmitter, Event } from '../core/events';
import { Logger } from '../core/logger';
import type {
ITrajectoryEntry,
IAgentPreferences,
IDiagnosticsInfo,
ICreateSessionOptions,
} from '../core/types';
import { CommandBridge, AntigravityCommands } from '../transport/command-bridge';
import { StateBridge } from '../transport/state-bridge';
const log = new Logger('CascadeManager');
/**
* Manages Cascade conversations.
*
* Primary data source: `antigravity.getDiagnostics` → `recentTrajectories`
* Fallback: `antigravityUnifiedStateSync.trajectorySummaries` protobuf parsing
*
* @example
* ```typescript
* const manager = new CascadeManager(commands, state);
* await manager.initialize();
*
* // List sessions (real titles from getDiagnostics)
* const sessions = await manager.getSessions();
* sessions.forEach(s => console.log(`${s.title} (step ${s.stepCount})`));
*
* // Read preferences (all 16 sentinel values)
* const prefs = await manager.getPreferences();
*
* // Create & send
* await manager.createSession({ task: 'Analyze coverage', background: true });
* ```
*/
export class CascadeManager implements IDisposable {
private readonly _disposables = new DisposableStore();
private _sessions: ITrajectoryEntry[] = [];
private _initialized = false;
// Events
private readonly _onSessionsChanged = this._disposables.add(new EventEmitter<ITrajectoryEntry[]>());
/** Fires when the session list changes */
public readonly onSessionsChanged: Event<ITrajectoryEntry[]> = this._onSessionsChanged.event;
constructor(
private readonly _commands: CommandBridge,
private readonly _state: StateBridge,
) { }
/**
* Initialize the cascade manager.
* Loads the initial session list from getDiagnostics.
*/
async initialize(): Promise<void> {
if (this._initialized) return;
await this._loadSessions();
this._initialized = true;
log.info(`Initialized with ${this._sessions.length} sessions`);
}
// ─── Read API ───────────────────────────────────────────────────────────
/**
* Get all known Cascade sessions.
*
* Uses `getDiagnostics.recentTrajectories` (clean JSON with titles).
*
* @returns List of trajectory entries sorted by recency
*/
async getSessions(): Promise<ITrajectoryEntry[]> {
if (!this._initialized) {
await this._loadSessions();
}
return [...this._sessions];
}
/**
* Refresh the session list.
*
* @returns Updated session list
*/
async refreshSessions(): Promise<ITrajectoryEntry[]> {
await this._loadSessions();
this._onSessionsChanged.fire(this._sessions);
return [...this._sessions];
}
/**
* Get agent preferences (all 16 sentinel values).
*/
async getPreferences(): Promise<IAgentPreferences> {
return this._state.getAgentPreferences();
}
/**
* Get IDE diagnostics (176KB JSON with system info, logs, trajectories).
*
* Structure (verified):
* - isRemote, systemInfo (OS, user, email)
* - extensionLogs (Array[375])
* - rendererLogs, mainThreadLogs, agentWindowConsoleLogs
* - languageServerLogs
* - recentTrajectories (Array[10])
*
* @returns Parsed diagnostics information
*/
async getDiagnostics(): Promise<IDiagnosticsInfo> {
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
if (!raw || typeof raw !== 'string') {
throw new Error('getDiagnostics returned unexpected type');
}
const parsed = JSON.parse(raw);
return {
isRemote: parsed.isRemote ?? false,
systemInfo: {
operatingSystem: parsed.systemInfo?.operatingSystem ?? 'unknown',
timestamp: parsed.systemInfo?.timestamp ?? '',
userEmail: parsed.systemInfo?.userEmail ?? '',
userName: parsed.systemInfo?.userName ?? '',
},
raw: parsed,
};
}
/**
* Get the Chrome DevTools MCP URL.
*
* Verified: returns `http://127.0.0.1:{port}/mcp`
*
* @returns MCP URL string
*/
async getMcpUrl(): Promise<string> {
const result = await this._commands.execute<string>('antigravity.getChromeDevtoolsMcpUrl');
return result ?? '';
}
/**
* Check if a file is gitignored.
*
* @param filePath - Relative or absolute file path
* @returns true if gitignored, false/null otherwise
*/
async isFileGitIgnored(filePath: string): Promise<boolean> {
const result = await this._commands.execute<boolean | null>('antigravity.isFileGitIgnored', filePath);
return result === true;
}
// ─── Write API ──────────────────────────────────────────────────────────
//
// Two-layer architecture (VERIFIED 2026-02-28):
//
// Layer 1 -- HEADLESS LS API (RECOMMENDED):
// Access: sdk.ls (LSBridge from antigravity-sdk)
// Method: Preact VNode tree -> component.props.lsClient -> 148 LS methods
// Creates cascade WITHOUT opening panel or switching UI.
// Usage: await sdk.ls.createCascade({ text: 'prompt' })
//
// Layer 2 — COMMAND API (FALLBACK, this file):
// Access: vscode.commands.executeCommand (extension host)
// Method: startNewConversation → sendPromptToAgentPanel → restore
// PROBLEM: Always switches UI, causes flickering, race conditions.
// Use only when renderer integration is not available.
//
// ────────────────────────────────────────────────────────────────────────
/**
* Create a new Cascade conversation via VS Code commands.
*
* ⚠️ **FALLBACK APPROACH** — causes UI flickering.
* For true headless creation, use `sdk.ls.createCascade()`
* from the SDK's LS bridge (see LSBridge module).
*
* VERIFIED 2026-02-28:
* - `startNewConversation` ✅ creates new chat (but switches UI)
* - `prioritized.chat.openNewConversation` ❌ does NOT create new
* - `sendPromptToAgentPanel` ✅ sends to currently visible chat (always opens panel)
* - `sendTextToChat` ❌ does not visibly work
*
* @param options - Session creation options
* @returns Session ID (googleAgentId) or empty string if not detected
*/
async createSession(options: ICreateSessionOptions): Promise<string> {
log.info(`Creating session (command fallback): "${options.task.substring(0, 50)}..."`);
// Snapshot current sessions to detect the new one
const beforeIds = new Set(this._sessions.map(s => s.id));
// Remember current active session (for background restore)
let previousActiveId = '';
if (options.background) {
try {
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
if (raw && typeof raw === 'string') {
const diag = JSON.parse(raw);
if (Array.isArray(diag.recentTrajectories) && diag.recentTrajectories.length > 0) {
previousActiveId = diag.recentTrajectories[0].googleAgentId ?? '';
}
}
} catch { }
}
// Create new conversation (VERIFIED: startNewConversation works)
await this._commands.execute(AntigravityCommands.START_NEW_CONVERSATION);
await this._delay(1500); // Wait for UI to initialize
// Send initial prompt
if (options.task) {
await this._commands.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, options.task);
}
// Mark as background if requested
if (options.background) {
await this._commands.execute(AntigravityCommands.TRACK_BACKGROUND_CONVERSATION);
}
// Wait for new session to appear in getDiagnostics
const newId = await this._waitForNewSession(beforeIds, 8000);
// If background: switch back to original conversation
if (options.background && previousActiveId) {
await this._delay(500);
await this._commands.execute(AntigravityCommands.SET_VISIBLE_CONVERSATION, previousActiveId);
log.info(`Background session created, restored to ${previousActiveId}`);
}
if (newId) {
log.info(`Session created: ${newId}`);
} else {
log.warn('Session created but ID not detected within timeout');
}
return newId;
}
/**
* Create a background Cascade conversation via commands.
*
* ⚠️ **FALLBACK** — Uses quick-switch approach (UI flickers briefly).
* For true headless background sessions, use the SDK's LS bridge:
* ```typescript
* // Using LSBridge:
* const cascadeId = await sdk.ls.createCascade({ text: 'task', modelId: 1018 });
* ```
*
* @param task - Initial task/prompt to send
* @returns Session ID or empty string
*/
async createBackgroundSession(task: string): Promise<string> {
return this.createSession({ task, background: true });
}
/**
* Send a message to the active Cascade conversation.
*
* Uses `antigravity.sendTextToChat` — the primary text sending command.
*/
async sendMessage(text: string): Promise<void> {
await this._commands.execute(AntigravityCommands.SEND_TEXT_TO_CHAT, text);
}
/**
* Send a prompt directly to the agent panel.
*
* Uses `antigravity.sendPromptToAgentPanel` — focuses the agent panel.
*/
async sendPrompt(text: string): Promise<void> {
await this._commands.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, text);
}
/**
* Send a chat action message (e.g., typing indicator, feedback).
*
* Uses `antigravity.sendChatActionMessage`.
*/
async sendChatAction(action: string): Promise<void> {
await this._commands.execute(AntigravityCommands.SEND_CHAT_ACTION, action);
}
/**
* Switch to a specific conversation.
*
* @param sessionId - Conversation UUID (googleAgentId)
*/
async focusSession(sessionId: string): Promise<void> {
await this._commands.execute(AntigravityCommands.SET_VISIBLE_CONVERSATION, sessionId);
}
/**
* Open a new conversation in the agent panel (prioritized command).
*
* Uses `antigravity.prioritized.chat.openNewConversation` which both
* opens the panel AND creates a fresh conversation.
*/
async openNewConversation(): Promise<void> {
await this._commands.execute(AntigravityCommands.OPEN_NEW_CONVERSATION);
}
/**
* Execute a Cascade action.
*
* Uses `antigravity.executeCascadeAction`.
*
* @param action - Action data to execute
*/
async executeCascadeAction(action: unknown): Promise<void> {
await this._commands.execute(AntigravityCommands.EXECUTE_CASCADE_ACTION, action);
}
// ─── Step Control ───────────────────────────────────────────────────────
/**
* Accept the current agent step (code edit, file write, etc.).
*
* Uses `antigravity.agent.acceptAgentStep`.
*/
async acceptStep(): Promise<void> {
await this._commands.execute(AntigravityCommands.ACCEPT_AGENT_STEP);
}
/** Reject the current agent step. */
async rejectStep(): Promise<void> {
await this._commands.execute(AntigravityCommands.REJECT_AGENT_STEP);
}
/**
* Accept a pending command (non-terminal, e.g. file edit confirmation).
*
* Uses `antigravity.command.accept`.
* This is DIFFERENT from terminalCommand.accept.
*/
async acceptCommand(): Promise<void> {
await this._commands.execute(AntigravityCommands.COMMAND_ACCEPT);
}
/** Reject a pending command (non-terminal). */
async rejectCommand(): Promise<void> {
await this._commands.execute(AntigravityCommands.COMMAND_REJECT);
}
// ─── Terminal Control ───────────────────────────────────────────────────
/**
* Accept a pending terminal command.
*
* Uses `antigravity.terminalCommand.accept`.
*/
async acceptTerminalCommand(): Promise<void> {
await this._commands.execute(AntigravityCommands.TERMINAL_ACCEPT);
}
/** Reject a pending terminal command. */
async rejectTerminalCommand(): Promise<void> {
await this._commands.execute(AntigravityCommands.TERMINAL_REJECT);
}
/** Run a pending terminal command. */
async runTerminalCommand(): Promise<void> {
await this._commands.execute(AntigravityCommands.TERMINAL_RUN);
}
// ─── Panel Control ──────────────────────────────────────────────────────
/** Open the Cascade agent panel */
async openPanel(): Promise<void> {
await this._commands.execute(AntigravityCommands.OPEN_AGENT_PANEL);
}
/** Focus the Cascade agent panel */
async focusPanel(): Promise<void> {
await this._commands.execute(AntigravityCommands.FOCUS_AGENT_PANEL);
}
/** Open the agent side panel */
async openSidePanel(): Promise<void> {
await this._commands.execute(AntigravityCommands.OPEN_AGENT_SIDE_PANEL);
}
/** Focus the agent side panel */
async focusSidePanel(): Promise<void> {
await this._commands.execute(AntigravityCommands.FOCUS_AGENT_SIDE_PANEL);
}
/**
* Get the browser integration port (e.g., 57401).
*/
async getBrowserPort(): Promise<number> {
return this._commands.execute<number>(AntigravityCommands.GET_BROWSER_PORT);
}
// ─── Private ────────────────────────────────────────────────────────────
/**
* Load sessions from getDiagnostics.recentTrajectories (clean JSON).
*
* VERIFIED structure per entry:
* {
* googleAgentId: "uuid", ← conversation ID
* trajectoryId: "uuid", ← internal trajectory ID
* summary: "title", ← human-readable title
* lastStepIndex: 992, ← step count
* lastModifiedTime: "ISO" ← last activity
* }
*/
private async _loadSessions(): Promise<void> {
try {
// Primary: getDiagnostics.recentTrajectories (10 most recent, with titles)
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
if (raw && typeof raw === 'string') {
const diag = JSON.parse(raw);
if (Array.isArray(diag.recentTrajectories)) {
this._sessions = diag.recentTrajectories.map((entry: any) => ({
id: entry.googleAgentId ?? '',
title: entry.summary ?? 'Untitled',
stepCount: entry.lastStepIndex ?? 0,
workspaceUri: '',
lastModifiedTime: entry.lastModifiedTime ?? '',
trajectoryId: entry.trajectoryId ?? '',
}));
log.debug(`Loaded ${this._sessions.length} sessions from getDiagnostics`);
return;
}
}
} catch (error) {
log.warn('getDiagnostics failed, falling back to USS', error);
}
// Fallback: parse trajectory summaries protobuf
try {
await this._loadSessionsFromUSS();
} catch (error) {
log.error('Failed to load sessions from USS', error);
this._sessions = [];
}
}
/**
* Fallback: extract sessions from USS trajectory summaries protobuf.
*/
private async _loadSessionsFromUSS(): Promise<void> {
const raw = await this._state.getRawValue('antigravityUnifiedStateSync.trajectorySummaries');
if (!raw) {
this._sessions = [];
return;
}
const buffer = Buffer.from(raw, 'base64');
const text = buffer.toString('utf8');
// Extract UUIDs
const uuids = [...new Set(text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g) || [])];
this._sessions = uuids.map((id, i) => ({
id,
title: `Conversation ${i + 1}`,
stepCount: 0,
workspaceUri: '',
}));
log.debug(`Loaded ${this._sessions.length} sessions from USS (fallback)`);
}
/**
* Wait for a new session to appear in getDiagnostics.
* Polls every 500ms up to timeoutMs.
*
* @returns New session ID or empty string if timeout
*/
private async _waitForNewSession(beforeIds: Set<string>, timeoutMs: number): Promise<string> {
const deadline = Date.now() + timeoutMs;
const pollInterval = 500;
while (Date.now() < deadline) {
await this._delay(pollInterval);
try {
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
if (!raw || typeof raw !== 'string') continue;
const diag = JSON.parse(raw);
if (!Array.isArray(diag.recentTrajectories)) continue;
for (const entry of diag.recentTrajectories) {
const id = entry.googleAgentId;
if (id && !beforeIds.has(id)) {
// Update local session list
await this._loadSessions();
return id;
}
}
} catch {
// ignore, retry
}
}
return '';
}
/**
* Simple delay utility.
*/
private _delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
dispose(): void {
this._disposables.dispose();
}
}

View File

@@ -0,0 +1,6 @@
/**
* Cascade module re-exports.
* @module cascade
*/
export { CascadeManager } from './cascade-manager';

View File

@@ -0,0 +1,73 @@
/**
* Disposable pattern for resource cleanup.
*
* @module disposable
*/
/**
* An object that can release resources when no longer needed.
*/
export interface IDisposable {
dispose(): void;
}
/**
* Collects multiple disposables and disposes them all at once.
*
* @example
* ```typescript
* const store = new DisposableStore();
* store.add(someEventSub);
* store.add(anotherSub);
* // Later:
* store.dispose(); // cleans up everything
* ```
*/
export class DisposableStore implements IDisposable {
private readonly _disposables: IDisposable[] = [];
private _disposed = false;
/**
* Add a disposable to the store.
*
* @param disposable - The disposable to track
* @returns The same disposable (for chaining)
*/
add<T extends IDisposable>(disposable: T): T {
if (this._disposed) {
disposable.dispose();
console.warn('[AntigravitySDK] Adding disposable to already disposed store');
} else {
this._disposables.push(disposable);
}
return disposable;
}
/**
* Dispose all tracked disposables.
*/
dispose(): void {
if (this._disposed) {
return;
}
this._disposed = true;
for (const d of this._disposables) {
try {
d.dispose();
} catch (error) {
console.error('[AntigravitySDK] Dispose error:', error);
}
}
this._disposables.length = 0;
}
}
/**
* Creates a disposable from a cleanup function.
*
* @param fn - Cleanup function to call on dispose
*/
export function toDisposable(fn: () => void): IDisposable {
return { dispose: fn };
}

View File

@@ -0,0 +1,61 @@
/**
* SDK-specific error classes.
*
* @module errors
*/
/**
* Base error for all Antigravity SDK errors.
*/
export class AntigravitySDKError extends Error {
constructor(message: string) {
super(`[AntigravitySDK] ${message}`);
this.name = 'AntigravitySDKError';
}
}
/**
* Thrown when Antigravity IDE is not detected or not running.
*/
export class AntigravityNotFoundError extends AntigravitySDKError {
constructor() {
super('Antigravity IDE not detected. Make sure this extension is running inside Antigravity.');
this.name = 'AntigravityNotFoundError';
}
}
/**
* Thrown when a command fails to execute.
*/
export class CommandExecutionError extends AntigravitySDKError {
constructor(
public readonly command: string,
public readonly reason: string,
) {
super(`Command "${command}" failed: ${reason}`);
this.name = 'CommandExecutionError';
}
}
/**
* Thrown when the state database cannot be read.
*/
export class StateReadError extends AntigravitySDKError {
constructor(
public readonly key: string,
public readonly reason: string,
) {
super(`Failed to read state key "${key}": ${reason}`);
this.name = 'StateReadError';
}
}
/**
* Thrown when a session/conversation is not found.
*/
export class SessionNotFoundError extends AntigravitySDKError {
constructor(public readonly sessionId: string) {
super(`Session "${sessionId}" not found`);
this.name = 'SessionNotFoundError';
}
}

View File

@@ -0,0 +1,99 @@
/**
* Lightweight event system for SDK.
*
* Follows VS Code's `Event<T>` / `EventEmitter<T>` pattern.
* Supports subscription, disposal, and one-shot listeners.
*
* @module events
*/
import type { IDisposable } from './disposable';
/**
* A function that represents a subscription to an event.
* Call the returned disposable to unsubscribe.
*/
export type Event<T> = (listener: (e: T) => void) => IDisposable;
/**
* Emits events to registered listeners.
*
* @example
* ```typescript
* const emitter = new EventEmitter<string>();
*
* const sub = emitter.event((msg) => console.log(msg));
* emitter.fire('hello'); // logs: hello
* sub.dispose();
* emitter.fire('world'); // nothing happens
* ```
*/
export class EventEmitter<T> implements IDisposable {
private _listeners: Set<(e: T) => void> = new Set();
private _disposed = false;
/**
* The event that listeners can subscribe to.
*/
readonly event: Event<T> = (listener: (e: T) => void): IDisposable => {
if (this._disposed) {
throw new Error('EventEmitter has been disposed');
}
this._listeners.add(listener);
return {
dispose: () => {
this._listeners.delete(listener);
},
};
};
/**
* Fire the event, notifying all listeners.
*
* @param data - The event data to send to listeners
*/
fire(data: T): void {
if (this._disposed) {
return;
}
for (const listener of this._listeners) {
try {
listener(data);
} catch (error) {
console.error('[AntigravitySDK] Event listener error:', error);
}
}
}
/**
* Subscribe to the event, but only fire once.
*
* @param listener - Callback to invoke once
* @returns Disposable to cancel before the event fires
*/
once(listener: (e: T) => void): IDisposable {
const sub = this.event((data) => {
sub.dispose();
listener(data);
});
return sub;
}
/**
* Get the current number of listeners.
*/
get listenerCount(): number {
return this._listeners.size;
}
/**
* Dispose of the emitter and all listeners.
*/
dispose(): void {
this._disposed = true;
this._listeners.clear();
}
}

View File

@@ -0,0 +1,11 @@
/**
* Core module — types, events, disposables, errors, logging.
*
* @module core
*/
export * from './types';
export * from './events';
export * from './disposable';
export * from './errors';
export { Logger, LogLevel } from './logger';

View File

@@ -0,0 +1,84 @@
/**
* Debug logger for SDK internals.
*
* Respects the `antigravitySDK.debug` setting.
*
* @module logger
*/
/**
* Log levels for SDK logging.
*/
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Off = 4,
}
/**
* SDK logger with level-based filtering.
*
* @example
* ```typescript
* const log = new Logger('CascadeManager');
* log.debug('Loading sessions...');
* log.info('Found 5 sessions');
* log.error('Failed to load', err);
* ```
*/
export class Logger {
private static _globalLevel: LogLevel = LogLevel.Warn;
/**
* Set the global log level for all SDK loggers.
*
* @param level - Minimum level to output
*/
static setLevel(level: LogLevel): void {
Logger._globalLevel = level;
}
/**
* Create a logger for a specific module.
*
* @param module - Module name (shown in log prefix)
*/
constructor(private readonly module: string) { }
/** Log a debug message. */
debug(message: string, ...args: unknown[]): void {
this._log(LogLevel.Debug, message, args);
}
/** Log an informational message. */
info(message: string, ...args: unknown[]): void {
this._log(LogLevel.Info, message, args);
}
/** Log a warning. */
warn(message: string, ...args: unknown[]): void {
this._log(LogLevel.Warn, message, args);
}
/** Log an error. */
error(message: string, ...args: unknown[]): void {
this._log(LogLevel.Error, message, args);
}
private _log(level: LogLevel, message: string, args: unknown[]): void {
if (level < Logger._globalLevel) {
return;
}
const prefix = `[AntigravitySDK:${this.module}]`;
const fn =
level === LogLevel.Error ? console.error
: level === LogLevel.Warn ? console.warn
: level === LogLevel.Info ? console.info
: console.debug;
fn(prefix, message, ...args);
}
}

View File

@@ -0,0 +1,381 @@
/**
* Core type definitions for Antigravity SDK.
*
* These types mirror the internal protobuf schemas used by Antigravity's
* Language Server, extracted via reverse engineering of the minified source.
*
* @module types
*/
// ─── Enums ──────────────────────────────────────────────────────────────────
/**
* Terminal command auto-execution policy.
*
* Controls how terminal commands are handled when the agent requests execution.
*/
export enum TerminalExecutionPolicy {
/** Always ask user before running */
OFF = 1,
/** Auto-run safe commands, ask for potentially dangerous ones */
AUTO = 2,
/** Always auto-run without asking */
EAGER = 3,
}
/**
* Artifact review policy for code changes.
*/
export enum ArtifactReviewPolicy {
/** Always show diff review */
ALWAYS = 1,
/** Skip review for simple changes */
TURBO = 2,
/** Automatically decide based on change complexity */
AUTO = 3,
}
/**
* Type of a Cortex step (tool call) in a trajectory.
*/
export enum CortexStepType {
RunCommand = 'RunCommand',
WriteToFile = 'WriteToFile',
ViewFile = 'ViewFile',
ViewFileOutline = 'ViewFileOutline',
ViewCodeItem = 'ViewCodeItem',
SearchWeb = 'SearchWeb',
ReadUrlContent = 'ReadUrlContent',
OpenBrowserUrl = 'OpenBrowserUrl',
ReadBrowserPage = 'ReadBrowserPage',
ListBrowserPages = 'ListBrowserPages',
ListDirectory = 'ListDirectory',
FindByName = 'FindByName',
CodebaseSearch = 'CodebaseSearch',
GrepSearch = 'GrepSearch',
SendCommandInput = 'SendCommandInput',
ReadTerminal = 'ReadTerminal',
ShellExec = 'ShellExec',
McpTool = 'McpTool',
InvokeSubagent = 'InvokeSubagent',
Memory = 'Memory',
KnowledgeGeneration = 'KnowledgeGeneration',
UserInput = 'UserInput',
SystemMessage = 'SystemMessage',
PlannerResponse = 'PlannerResponse',
Wait = 'Wait',
ProposeCode = 'ProposeCode',
WriteCascadeEdit = 'WriteCascadeEdit',
}
/**
* Status of a Cortex step.
*/
export enum StepStatus {
/** Step is being processed */
Running = 'running',
/** Step completed successfully */
Completed = 'completed',
/** Step failed */
Failed = 'failed',
/** Step is waiting for user interaction */
WaitingForUser = 'waiting_for_user',
/** Step was cancelled */
Cancelled = 'cancelled',
}
/**
* Type of trajectory (conversation).
*/
export enum TrajectoryType {
/** Standard chat conversation */
Chat = 'chat',
/** Agent mode (Cascade) */
Cascade = 'cascade',
}
// ─── Interfaces ─────────────────────────────────────────────────────────────
/**
* A single step (tool call) in a Cascade trajectory.
*/
export interface ICortexStep {
/** Unique step identifier */
readonly id: string;
/** Step index within the trajectory */
readonly index: number;
/** Type of tool call */
readonly type: CortexStepType;
/** Current status */
readonly status: StepStatus;
/** Human-readable summary of what this step does */
readonly summary: string;
/** Step-specific data (command line, file path, etc.) */
readonly data: Record<string, unknown>;
/** Internal metadata not shown in UI */
readonly metadata: IStepMetadata;
/** Timestamp when step was created */
readonly createdAt: Date;
/** Timestamp when step completed (if completed) */
readonly completedAt?: Date;
}
/**
* Internal metadata attached to each step.
*/
export interface IStepMetadata {
/** Raw protobuf fields from the server response */
readonly rawFields: Record<string, unknown>;
/** Token count for this step's input */
readonly inputTokens?: number;
/** Token count for this step's output */
readonly outputTokens?: number;
/** Model used for this step */
readonly model?: string;
/** Whether this step was auto-approved */
readonly autoApproved?: boolean;
}
/**
* A chat message in a conversation.
*/
export interface IChatMessage {
/** Message role */
readonly role: 'user' | 'assistant' | 'system';
/** Message content */
readonly content: string;
/** Message ID */
readonly id: string;
/** Timestamp */
readonly createdAt: Date;
/** Hidden metadata */
readonly metadata: Record<string, unknown>;
}
/**
* Information about the current context window usage.
*/
export interface IContextInfo {
/** Total tokens currently in context */
readonly totalTokens: number;
/** Maximum context window size */
readonly maxTokens: number;
/** Usage as percentage (0-100) */
readonly usagePercent: number;
/** Token breakdown by category */
readonly breakdown: ITokenBreakdown;
}
/**
* Token usage breakdown.
*/
export interface ITokenBreakdown {
/** System prompt tokens */
readonly system: number;
/** User message tokens */
readonly userMessages: number;
/** Assistant response tokens */
readonly assistantMessages: number;
/** Tool call input tokens */
readonly toolCalls: number;
/** Tool result tokens */
readonly toolResults: number;
}
/**
* A Cascade session (conversation/trajectory).
*/
export interface ISessionInfo {
/** Unique session/cascade ID */
readonly id: string;
/** Session title (auto-generated or user-set) */
readonly title: string;
/** When the session was created */
readonly createdAt: Date;
/** When the session was last active */
readonly lastActiveAt: Date;
/** Type of trajectory */
readonly type: TrajectoryType;
/** Whether the session is currently active */
readonly isActive: boolean;
/** Tags applied to this session */
readonly tags: string[];
}
/**
* Agent preferences from USS (Unified State Sync).
*
* All 16 sentinel keys verified from live state.vscdb on 2026-02-28.
*/
export interface IAgentPreferences {
/** Terminal command auto-execution policy (terminalAutoExecutionPolicySentinelKey) */
readonly terminalExecutionPolicy: TerminalExecutionPolicy;
/** Code change review policy (artifactReviewPolicySentinelKey) */
readonly artifactReviewPolicy: ArtifactReviewPolicy;
/** Planning mode (planningModeSentinelKey) */
readonly planningMode: number;
/** Whether strict/secure mode is enabled (secureModeSentinelKey) */
readonly secureModeEnabled: boolean;
/** Whether terminal sandbox is enabled (enableTerminalSandboxSentinelKey) */
readonly terminalSandboxEnabled: boolean;
/** Whether sandbox allows network access (sandboxAllowNetworkSentinelKey) */
readonly sandboxAllowNetwork: boolean;
/** Whether shell integration is enabled (enableShellIntegrationSentinelKey) */
readonly shellIntegrationEnabled: boolean;
/** Allow agent to access files outside workspace (allowAgentAccessNonWorkspaceFilesSentinelKey) */
readonly allowNonWorkspaceFiles: boolean;
/** Allow Cascade to read .gitignore files (allowCascadeAccessGitignoreFilesSentinelKey) */
readonly allowGitignoreAccess: boolean;
/** Explain and fix in current conversation (explainAndFixInCurrentConversationSentinelKey) */
readonly explainFixInCurrentConvo: boolean;
/** Auto-continue on max generator invocations (autoContinueOnMaxGeneratorInvocationsSentinelKey) */
readonly autoContinueOnMax: number;
/** Disable auto-open of edited files (disableAutoOpenEditedFilesSentinelKey) */
readonly disableAutoOpenEdited: boolean;
/** Enable sounds for special events (enableSoundsForSpecialEventsSentinelKey) */
readonly enableSounds: boolean;
/** Disable Cascade auto-fix for lint errors (disableCascadeAutoFixLintsSentinelKey) */
readonly disableAutoFixLints: boolean;
/** Explicitly allowed terminal commands (terminalAllowedCommandsSentinelKey) */
readonly allowedCommands: string[];
/** Explicitly denied terminal commands (terminalDeniedCommandsSentinelKey) */
readonly deniedCommands: string[];
}
/**
* Model configuration.
*/
export interface IModelConfig {
/** Model identifier */
readonly id: string;
/** Human-readable model name */
readonly name: string;
/** Whether this model is currently selected */
readonly isActive: boolean;
/** Maximum context window size in tokens */
readonly maxContextTokens: number;
}
/**
* Options for creating a new Cascade session.
*/
export interface ICreateSessionOptions {
/** Initial task/message to send */
readonly task: string;
/** Whether to run in background (don't focus the panel) */
readonly background?: boolean;
/** Model to use (defaults to current) */
readonly model?: string;
}
/**
* Agent state from the Agent Manager.
*/
export interface IAgentState {
/** Whether the agent manager is enabled */
readonly isEnabled: boolean;
/** Whether the agent is currently processing */
readonly isProcessing: boolean;
/** Active cascade/conversation ID */
readonly activeCascadeId: string | null;
/** Current model in use */
readonly currentModel: string;
}
/**
* Trajectory entry from getDiagnostics.recentTrajectories.
*
* VERIFIED 2026-02-28: getDiagnostics returns clean JSON array with:
* { googleAgentId, trajectoryId, summary, lastStepIndex, lastModifiedTime }
*/
export interface ITrajectoryEntry {
/** Conversation UUID = googleAgentId */
readonly id: string;
/** Human-readable title = summary field */
readonly title: string;
/** Current step index in this conversation */
readonly stepCount: number;
/** Workspace URI (from USS protobuf fallback) */
readonly workspaceUri: string;
/** Internal trajectory UUID (from getDiagnostics) */
readonly trajectoryId?: string;
/** ISO timestamp of last modification (from getDiagnostics) */
readonly lastModifiedTime?: string;
}
/**
* Diagnostics info from `antigravity.getDiagnostics`.
*
* VERIFIED: returns 176KB JSON string with 8 top-level keys:
* isRemote, systemInfo, extensionLogs, rendererLogs,
* mainThreadLogs, agentWindowConsoleLogs, languageServerLogs,
* recentTrajectories.
*/
export interface IDiagnosticsInfo {
/** Whether IDE is running remotely (SSH) */
readonly isRemote: boolean;
/** System info */
readonly systemInfo: {
readonly operatingSystem: string;
readonly timestamp: string;
readonly userEmail: string;
readonly userName: string;
};
/** Raw JSON for fields not yet typed */
readonly raw: Record<string, unknown>;
}

View File

@@ -0,0 +1,87 @@
/**
* Antigravity SDK — Community SDK for Antigravity IDE.
*
* @packageDocumentation
*
* @example
* ```typescript
* import { AntigravitySDK } from 'antigravity-sdk';
*
* export function activate(context: vscode.ExtensionContext) {
* const sdk = new AntigravitySDK(context);
* await sdk.initialize();
*
* // Read preferences
* const prefs = await sdk.cascade.getPreferences();
* console.log('Terminal policy:', prefs.terminalExecutionPolicy);
*
* // List sessions
* const sessions = await sdk.cascade.getSessions();
* console.log(`${sessions.length} conversations`);
*
* // Get diagnostics
* const diag = await sdk.cascade.getDiagnostics();
* console.log(`User: ${diag.systemInfo.userName}`);
* }
* ```
*/
// Core
export {
// Types
TerminalExecutionPolicy,
ArtifactReviewPolicy,
CortexStepType,
StepStatus,
TrajectoryType,
// Interfaces
type ICortexStep,
type IStepMetadata,
type IChatMessage,
type IContextInfo,
type ITokenBreakdown,
type ISessionInfo,
type IAgentPreferences,
type IModelConfig,
type ICreateSessionOptions,
type IAgentState,
type ITrajectoryEntry,
type IDiagnosticsInfo,
} from './core/types';
export { Event, EventEmitter } from './core/events';
export { IDisposable, DisposableStore, toDisposable } from './core/disposable';
export {
AntigravitySDKError,
AntigravityNotFoundError,
CommandExecutionError,
StateReadError,
SessionNotFoundError,
} from './core/errors';
export { Logger, LogLevel } from './core/logger';
// Transport
export { CommandBridge, AntigravityCommands } from './transport/command-bridge';
export { StateBridge, USSKeys } from './transport/state-bridge';
export { EventMonitor, type IStateChange, type IStepCountChange, type IActiveSessionChange } from './transport/event-monitor';
export { LSBridge, Models, type ModelId, type IHeadlessCascadeOptions, type ISendMessageOptions, type IConversationAnnotations } from './transport/ls-bridge';
// Cascade
export { CascadeManager } from './cascade/cascade-manager';
// Integration
export { IntegrationManager, IntegrityManager, TitleManager, IntegrationPoint } from './integration';
export type {
IntegrationConfig,
IButtonIntegration,
ITurnMetaIntegration,
IUserBadgeIntegration,
IBotActionIntegration,
IDropdownIntegration,
ITitleIntegration,
IToastConfig,
TurnMetric,
} from './integration';
// SDK
export { AntigravitySDK, type ISDKOptions } from './sdk';

View File

@@ -0,0 +1,21 @@
/**
* Integration module — re-exports.
* @module integration
*/
export { IntegrationManager } from './integration-manager';
export { IntegrityManager } from './integrity-manager';
export { TitleManager } from './title-manager';
export { IntegrationPoint } from './types';
export type {
IntegrationConfig,
IIntegrationManager,
IButtonIntegration,
ITurnMetaIntegration,
IUserBadgeIntegration,
IBotActionIntegration,
IDropdownIntegration,
ITitleIntegration,
IToastConfig,
IToastRow,
TurnMetric,
} from './types';

View File

@@ -0,0 +1,704 @@
/**
* Integration Manager — Public API for UI integration into Agent View.
*
* Orchestrates ScriptGenerator and WorkbenchPatcher to provide
* a clean, developer-friendly API.
*
* @module integration/integration-manager
*
* @example
* ```typescript
* import { IntegrationManager, IntegrationPoint } from 'antigravity-sdk';
*
* const integrator = new IntegrationManager();
*
* integrator.register({
* id: 'myStats',
* point: IntegrationPoint.TOP_BAR,
* icon: '📊',
* tooltip: 'Show Stats',
* toast: {
* title: 'My Extension Stats',
* rows: [{ key: 'turns:', value: 'Dynamic data here' }],
* },
* });
*
* integrator.register({
* id: 'turnInfo',
* point: IntegrationPoint.TURN_METADATA,
* metrics: ['turnNumber', 'userCharCount', 'separator', 'aiCharCount', 'codeBlocks'],
* });
*
* await integrator.install();
* // Restart Antigravity to see changes
* ```
*/
import * as fs from 'fs';
import { IDisposable } from '../core/disposable';
import { Logger } from '../core/logger';
import {
IntegrationConfig,
IntegrationPoint,
IIntegrationManager,
IButtonIntegration,
ITurnMetaIntegration,
IUserBadgeIntegration,
IBotActionIntegration,
IDropdownIntegration,
ITitleIntegration,
IToastConfig,
} from './types';
import { ScriptGenerator } from './script-generator';
import { WorkbenchPatcher } from './workbench-patcher';
import { IntegrityManager } from './integrity-manager';
import { TitleManager } from './title-manager';
import { generateTitleProxyCode } from './title-proxy';
const log = new Logger('IntegrationManager');
/**
* Manages UI integrations into the Antigravity Agent View.
*
* Provides a declarative API to register integration points,
* generates a self-contained JavaScript file, and installs it
* into Antigravity's workbench.
*
* Features:
* - **Theme-aware**: Adapts to dark/light mode automatically
* - **Auto-repair**: Watches workbench.html and re-patches after updates
* - **Dynamic update**: Re-generate script without re-patching workbench.html
*/
export class IntegrationManager implements IIntegrationManager, IDisposable {
private readonly _configs: Map<string, IntegrationConfig> = new Map();
private readonly _generator = new ScriptGenerator();
private readonly _patcher: WorkbenchPatcher;
private readonly _integrity: IntegrityManager;
private readonly _titles = new TitleManager();
private readonly _namespace: string;
private _watcher: fs.FSWatcher | null = null;
private _autoRepairDebounce: ReturnType<typeof setTimeout> | null = null;
private _titleProxyEnabled = false;
/**
* @param namespace - Unique slug that isolates this extension's files.
* Derived automatically from `context.extension.id` when using AntigravitySDK.
* Multiple SDK-based extensions can coexist without conflicts.
*/
constructor(namespace: string = 'default') {
this._namespace = namespace;
this._patcher = new WorkbenchPatcher(namespace);
this._integrity = new IntegrityManager(
this._patcher.getWorkbenchDir(),
namespace,
);
}
// ─── Registration ──────────────────────────────────────────────────
/**
* Register a single integration point.
*
* @throws If an integration with the same ID already exists
*/
register(config: IntegrationConfig): void {
if (this._configs.has(config.id)) {
throw new Error(`Integration '${config.id}' is already registered`);
}
this._configs.set(config.id, config);
log.debug(`Registered integration: ${config.id} (${config.point})`);
}
/**
* Register multiple integration points at once.
*/
registerMany(configs: IntegrationConfig[]): void {
for (const c of configs) {
this.register(c);
}
}
/**
* Remove a registered integration by ID.
*/
unregister(id: string): void {
this._configs.delete(id);
log.debug(`Unregistered integration: ${id}`);
}
/**
* Get all registered integrations.
*/
getRegistered(): ReadonlyArray<IntegrationConfig> {
return Array.from(this._configs.values());
}
// ─── Convenience methods (fluent API) ──────────────────────────────
/**
* Add a button to the top bar (near +, refresh icons).
*/
addTopBarButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
this.register({
id,
point: IntegrationPoint.TOP_BAR,
icon,
tooltip,
toast,
} as IButtonIntegration);
return this;
}
/**
* Add a button to the top-right corner (before X).
*/
addTopRightButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
this.register({
id,
point: IntegrationPoint.TOP_RIGHT,
icon,
tooltip,
toast,
} as IButtonIntegration);
return this;
}
/**
* Add a button next to the send/voice buttons.
*/
addInputButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
this.register({
id,
point: IntegrationPoint.INPUT_AREA,
icon,
tooltip,
toast,
} as IButtonIntegration);
return this;
}
/**
* Add an icon to the bottom icon row (file, terminal, etc.).
*/
addBottomIcon(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
this.register({
id,
point: IntegrationPoint.BOTTOM_ICONS,
icon,
tooltip,
toast,
} as IButtonIntegration);
return this;
}
/**
* Enable per-turn metadata display.
*/
addTurnMetadata(id: string, metrics: ITurnMetaIntegration['metrics'], clickable = true): this {
this.register({
id,
point: IntegrationPoint.TURN_METADATA,
metrics,
clickable,
} as ITurnMetaIntegration);
return this;
}
/**
* Add character count badges to user messages.
*/
addUserBadges(id: string, display: IUserBadgeIntegration['display'] = 'charCount'): this {
this.register({
id,
point: IntegrationPoint.USER_BADGE,
display,
} as IUserBadgeIntegration);
return this;
}
/**
* Add an action button next to Good/Bad feedback.
*/
addBotAction(id: string, icon: string, label: string, toast?: IToastConfig): this {
this.register({
id,
point: IntegrationPoint.BOT_ACTION,
icon,
label,
toast,
} as IBotActionIntegration);
return this;
}
/**
* Add item(s) to the 3-dot dropdown menu.
*/
addDropdownItem(id: string, label: string, icon?: string, toast?: IToastConfig, separator = false): this {
this.register({
id,
point: IntegrationPoint.DROPDOWN_MENU,
label,
icon,
toast,
separator,
} as IDropdownIntegration);
return this;
}
/**
* Enable chat title interaction.
*/
addTitleInteraction(id: string, interaction: ITitleIntegration['interaction'] = 'dblclick', hint?: string, toast?: IToastConfig): this {
this.register({
id,
point: IntegrationPoint.CHAT_TITLE,
interaction,
hint,
toast,
} as ITitleIntegration);
return this;
}
// ─── Title Proxy ─────────────────────────────────────────────────
/**
* Enable the title proxy feature.
*
* Adds renderer-side code that intercepts the summaries provider
* and injects custom chat titles. Uses structural matching to find
* the provider (obfuscation-safe).
*
* After enabling, call `install()` or `updateScript()` to apply.
*
* @example
* ```typescript
* const sdk = new AntigravitySDK(context);
* await sdk.initialize();
*
* sdk.integration.enableTitleProxy();
* await sdk.integration.install();
*
* // Now rename from extension host:
* sdk.integration.titles.rename(cascadeId, 'My Custom Title');
* ```
*/
enableTitleProxy(): this {
this._titleProxyEnabled = true;
if (this._patcher.isAvailable()) {
this._titles.initialize(this._patcher.getWorkbenchDir(), this._namespace);
}
log.info('Title proxy enabled');
return this;
}
/**
* Access the title manager for programmatic title control.
*
* Requires `enableTitleProxy()` to be called first.
*
* @example
* ```typescript
* sdk.integration.titles.rename(cascadeId, 'My Title');
* sdk.integration.titles.remove(cascadeId);
* const all = sdk.integration.titles.getAll();
* ```
*/
get titles(): TitleManager {
if (!this._titleProxyEnabled) {
log.warn('Title proxy not enabled. Call enableTitleProxy() first.');
}
return this._titles;
}
// ─── Build & Install ───────────────────────────────────────────────
/**
* Generate the integration script from all registered configs.
*
* If title proxy is enabled, appends the title proxy renderer code.
*
* @returns Complete JavaScript code as a string
*/
build(): string {
const configs = Array.from(this._configs.values());
if (configs.length === 0 && !this._titleProxyEnabled) {
throw new Error('No integration points registered and title proxy not enabled');
}
let script = '';
if (configs.length > 0) {
log.info(`Building script for ${configs.length} integration(s)`);
script = this._generator.generate(configs);
}
if (this._titleProxyEnabled) {
log.info('Appending title proxy code');
script += '\n' + generateTitleProxyCode(this._namespace);
}
return script;
}
/**
* Install the generated script into workbench.html.
*
* For seamless hot-reload behavior, use `installSeamless()` instead.
*
* @returns true if the script content actually changed on disk
*/
async install(): Promise<boolean> {
if (!this._patcher.isAvailable()) {
throw new Error('Antigravity workbench not found. Is Antigravity installed?');
}
const script = this.build();
// Read existing script to detect changes
const scriptPath = this._patcher.getScriptPath();
let oldContent = '';
try {
if (fs.existsSync(scriptPath)) {
oldContent = fs.readFileSync(scriptPath, 'utf8');
}
} catch { /* ignore */ }
this._patcher.install(script);
this._integrity.suppressCheck();
this._patcher.writeHeartbeat();
const changed = oldContent !== script;
log.info(
`Installed integration (${this._configs.size} points, titleProxy: ${this._titleProxyEnabled}) -> ${scriptPath} [${changed ? 'CHANGED' : 'unchanged'}]`,
);
return changed;
}
/**
* Seamless install — handles everything automatically.
*
* This is the **recommended** install method for extension developers.
* It handles the entire lifecycle:
*
* 1. **First install:** Writes script + patches HTML + prompts user to reload
* 2. **Update:** Compares content, if changed → auto-reloads window (no prompt)
* 3. **No change:** Does nothing
*
* The developer never needs to think about reload.
*
* @param executeCommand - Function to execute VS Code commands
* (pass `vscode.commands.executeCommand` or equivalent)
*
* @example
* ```typescript
* const sdk = new AntigravitySDK(context);
* await sdk.initialize();
*
* sdk.integration.enableTitleProxy();
* // That's it. SDK handles install, reload, everything.
* await sdk.integration.installSeamless(
* (cmd) => vscode.commands.executeCommand(cmd),
* (msg, ...items) => vscode.window.showInformationMessage(msg, ...items),
* );
* ```
*/
async installSeamless(
executeCommand: (command: string) => Thenable<any>,
showMessage?: (message: string, ...items: string[]) => Thenable<string | undefined>,
): Promise<void> {
const wasInstalled = this._patcher.isInstalled();
// Snapshot old content before install
const scriptPath = this._patcher.getScriptPath();
let oldContent = '';
try {
if (fs.existsSync(scriptPath)) {
oldContent = fs.readFileSync(scriptPath, 'utf8');
}
} catch { /* ignore */ }
const changed = await this.install();
if (!wasInstalled) {
// First install: prompt user
log.info('First install. Prompting for reload.');
if (showMessage) {
const action = await showMessage(
'Better Antigravity installed. Reload to activate.',
'Reload Now',
);
if (action === 'Reload Now') {
await executeCommand('workbench.action.reloadWindow');
}
}
} else if (changed) {
// Update: auto-reload (no prompt)
log.info('Script changed on disk. Auto-reloading window...');
// Small delay to let extension finish activation
setTimeout(() => executeCommand('workbench.action.reloadWindow'), 500);
} else {
log.debug('Script unchanged. No reload needed.');
}
}
/**
* Remove the integration from workbench.html.
*
* ⚠️ Requires Antigravity restart to take effect.
*/
async uninstall(): Promise<void> {
this._patcher.uninstall();
this._integrity.releaseCheck();
this._patcher.removeHeartbeat();
this.disableAutoRepair();
log.info('Uninstalled integration. Restart Antigravity to apply.');
}
/**
* Check if an integration is currently installed.
*/
isInstalled(): boolean {
return this._patcher.isInstalled();
}
/**
* Signal that the extension is active.
*
* Call this in your extension's `activate()` function.
* The integration script checks for this heartbeat;
* if it's missing or stale (>48h), the script won't start.
*
* This prevents orphaned integrations from running after
* an extension is disabled or uninstalled.
*
* @example
* ```typescript
* export function activate(context: vscode.ExtensionContext) {
* const sdk = new AntigravitySDK(context);
* sdk.integration.signalActive();
* // ...
* }
* ```
*/
signalActive(): void {
this._patcher.writeHeartbeat();
log.debug('Heartbeat refreshed');
}
// ─── Dynamic Update ─────────────────────────────────────────────────
/**
* Re-generate and overwrite the integration script without re-patching workbench.html.
*
* Use this after registering/unregistering integration points at runtime.
* The script file is updated in-place; the next Antigravity restart
* will pick up the changes. workbench.html <script> tag is unchanged.
*
* @returns true if script was updated
*/
updateScript(): boolean {
if (!this._patcher.isInstalled()) {
log.warn('Cannot update script — integration is not installed');
return false;
}
try {
const script = this.build();
fs.writeFileSync(this._patcher.getScriptPath(), script, 'utf8');
log.info(`Script updated (${this._configs.size} points)`);
return true;
} catch (err) {
log.error('Failed to update script', err);
return false;
}
}
// ─── Auto-Repair ────────────────────────────────────────────────────
/**
* Enable auto-repair: watches workbench.html for changes
* and automatically re-applies the integration patch.
*
* This handles Antigravity updates that overwrite workbench.html.
* The watcher detects when the file changes and re-patches it
* if the integration marker is missing.
*
* @example
* ```typescript
* const integrator = new IntegrationManager();
* integrator.useDemoPreset();
* await integrator.install();
* integrator.enableAutoRepair(); // Survive Antigravity updates
* ```
*/
enableAutoRepair(): void {
if (this._watcher) return;
const htmlPath = this._patcher.getWorkbenchDir() + '\\workbench.html';
if (!fs.existsSync(htmlPath)) {
log.warn('Cannot enable auto-repair — workbench.html not found');
return;
}
try {
this._watcher = fs.watch(htmlPath, (eventType) => {
if (eventType !== 'change') return;
// Debounce — Antigravity may write multiple times
if (this._autoRepairDebounce) clearTimeout(this._autoRepairDebounce);
this._autoRepairDebounce = setTimeout(() => {
this._tryRepair();
}, 2000);
});
log.info('Auto-repair enabled — watching workbench.html');
} catch (err) {
log.error('Failed to enable auto-repair', err);
}
}
/**
* Disable auto-repair watcher.
*/
disableAutoRepair(): void {
if (this._watcher) {
this._watcher.close();
this._watcher = null;
log.info('Auto-repair disabled');
}
if (this._autoRepairDebounce) {
clearTimeout(this._autoRepairDebounce);
this._autoRepairDebounce = null;
}
}
/**
* Whether auto-repair is active.
*/
get isAutoRepairEnabled(): boolean {
return this._watcher !== null;
}
private _tryRepair(): void {
try {
if (this._patcher.isInstalled()) {
log.debug('Auto-repair: integration still present, no action needed');
return;
}
if (this._configs.size === 0) {
log.debug('Auto-repair: no configs registered, skipping');
return;
}
log.info('Auto-repair: integration lost (Antigravity update?), re-patching...');
const script = this.build();
this._patcher.install(script);
this._integrity.repair();
log.info('Auto-repair: re-patched successfully. Restart Antigravity.');
} catch (err) {
log.error('Auto-repair failed', err);
}
}
// ─── Preset ────────────────────────────────────────────────────────
/**
* Register the Demo preset — a complete demo of all 9 integration points.
* Useful for testing and as a reference implementation.
*/
useDemoPreset(): this {
this.addTopBarButton('demo_overview', '\u{1F4E1}', 'SDK: Session Overview', {
title: 'Session Overview',
badge: { text: 'TOP_BAR', bgColor: 'rgba(79,195,247,.2)', textColor: '#4fc3f7' },
rows: [
{ key: 'location:', value: 'Header icon bar' },
{ key: 'use case:', value: 'Session overview, navigation' },
],
});
this.addTopRightButton('demo_perf', '\u26A1', 'SDK: Performance', {
title: 'Performance',
badge: { text: 'TOP_RIGHT', bgColor: 'rgba(255,193,7,.2)', textColor: '#ffd54f' },
rows: [
{ key: 'location:', value: 'Top right, before close' },
{ key: 'use case:', value: 'Status indicator' },
],
});
this.addInputButton('demo_stats', '\u{1F4CA}', 'SDK: Stats', {
title: 'Input Stats',
badge: { text: 'INPUT_AREA', bgColor: 'rgba(76,175,80,.2)', textColor: '#81c784' },
rows: [
{ key: 'location:', value: 'Next to send button' },
{ key: 'use case:', value: 'Token counter, analytics' },
],
});
this.addBottomIcon('demo_actions', '\u2630', 'SDK: Quick Actions', {
title: 'Quick Actions',
badge: { text: 'BOTTOM_ICONS', bgColor: 'rgba(255,152,0,.2)', textColor: '#ffb74d' },
rows: [
{ key: 'location:', value: 'Bottom icon row' },
{ key: 'use case:', value: 'Mode switches, quick actions' },
],
});
this.addTurnMetadata('demo_turns', [
'turnNumber',
'userCharCount',
'separator',
'aiCharCount',
'codeBlocks',
'thinkingIndicator',
]);
this.addUserBadges('demo_ubadge', 'charCount');
this.addBotAction('demo_inspect', '\u{1F50D}', 'inspect', {
title: 'Response Inspector',
badge: { text: 'BOT_ACTION', bgColor: 'rgba(156,39,176,.2)', textColor: '#ce93d8' },
rows: [
{ key: 'location:', value: 'Next to Good/Bad' },
{ key: 'use case:', value: 'Response analysis' },
],
});
this.addDropdownItem('demo_menu_stats', 'SDK Stats', '\u{1F4CA}', {
title: 'Extended Stats',
badge: { text: 'DROPDOWN', bgColor: 'rgba(233,30,99,.2)', textColor: '#f48fb1' },
rows: [
{ key: 'location:', value: '3-dot dropdown menu' },
{ key: 'use case:', value: 'Extended actions' },
],
}, true);
this.addDropdownItem('demo_menu_debug', 'SDK Debug', '\u{1F9EA}', {
title: 'Debug Info',
badge: { text: 'DEBUG', bgColor: 'rgba(255,87,34,.2)', textColor: '#ff8a65' },
rows: [
{ key: 'location:', value: '3-dot dropdown menu' },
{ key: 'use case:', value: 'Debug, diagnostics' },
],
});
this.addTitleInteraction('demo_title', 'dblclick', 'dblclick', {
title: 'Chat Title',
badge: { text: 'TITLE', bgColor: 'rgba(0,150,136,.2)', textColor: '#80cbc4' },
rows: [
{ key: 'location:', value: 'Conversation title' },
{ key: 'use case:', value: 'Rename, bookmark' },
],
});
return this;
}
// ─── Dispose ───────────────────────────────────────────────────────
dispose(): void {
this.disableAutoRepair();
this._configs.clear();
this._titles.dispose();
}
}

View File

@@ -0,0 +1,270 @@
/**
* Integrity Manager — Suppress Antigravity's "corrupt installation" warnings.
*
* When the SDK patches workbench files, Antigravity's IntegrityService detects
* checksum mismatches and shows two warnings:
* 1. Console WARN ("Installation has been modified on disk")
* 2. UI Notification ("Your Antigravity installation appears to be corrupt")
*
* This class updates ALL mismatched SHA256 hashes in product.json, so
* IntegrityService sees isPure=true and produces no warnings at all.
*
* Handles not just workbench.html but also workbench.desktop.main.js (auto-run fix),
* workbench-jetski-agent.html (agent manager patching), and any other modified files.
*
* Multi-extension coordination: a registry file (.ag-sdk-integrity.json)
* in the workbench directory tracks active SDK namespaces and the original
* hashes, so the last extension to uninstall restores the original state.
*
* @module integration/integrity-manager
*
* @internal
*/
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { Logger } from '../core/logger';
const log = new Logger('IntegrityManager');
/** Coordination registry stored in the workbench directory. */
interface IIntegrityRegistry {
/** Active SDK namespace slugs. */
namespaces: string[];
/** Original product.json hashes for ALL checksummed files (before any patching). */
originalHashes: Record<string, string>;
}
/** Registry filename — lives next to workbench.html. */
const REGISTRY_FILENAME = '.ag-sdk-integrity.json';
/**
* Manages integrity check suppression for Antigravity's IntegrityService.
*
* Call `suppressCheck()` after any file patching (workbench.html, main.js, etc.).
* It scans ALL files listed in product.json checksums, recomputes hashes for
* any that have changed, and updates product.json. IntegrityService will see
* `isPure = true` on next restart, producing zero warnings.
*/
export class IntegrityManager {
private readonly _productJsonPath: string;
private readonly _appOutDir: string;
private readonly _registryPath: string;
private readonly _namespace: string;
/**
* @param workbenchDir — Absolute path to the workbench directory
* (e.g. `%LOCALAPPDATA%/Programs/Antigravity/resources/app/out/vs/code/electron-browser/workbench/`)
* @param namespace — Unique slug for this extension (e.g. 'kanezal-better-antigravity')
*/
constructor(workbenchDir: string, namespace: string) {
this._namespace = namespace;
this._registryPath = path.join(workbenchDir, REGISTRY_FILENAME);
// product.json is at resources/app/product.json
// workbenchDir is resources/app/out/vs/code/electron-browser/workbench/
const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
this._productJsonPath = path.join(appDir, 'product.json');
this._appOutDir = path.join(appDir, 'out');
}
/**
* Suppress the integrity check by updating ALL mismatched hashes in product.json.
*
* Scans every file listed in product.json checksums, recomputes SHA256 for each,
* and updates any that have changed. This handles not just workbench.html but also
* workbench.desktop.main.js (auto-run fix), jetskiAgent files, etc.
*
* Call this after any file patching. Safe to call multiple times.
*/
suppressCheck(): void {
try {
if (!fs.existsSync(this._productJsonPath)) {
log.warn(`product.json not found at ${this._productJsonPath}`);
return;
}
const productJson = JSON.parse(fs.readFileSync(this._productJsonPath, 'utf8'));
if (!productJson.checksums) {
log.debug('No checksums in product.json — nothing to update');
return;
}
// 1. Load or create registry, register this namespace
const registry = this._readRegistry();
if (!registry.namespaces.includes(this._namespace)) {
registry.namespaces.push(this._namespace);
}
// 2. Scan ALL checksummed files, save originals & update mismatches
let updatedCount = 0;
for (const [relPath, storedHash] of Object.entries(productJson.checksums) as [string, string][]) {
const filePath = path.join(this._appOutDir, relPath);
let actualHash: string;
try {
const content = fs.readFileSync(filePath);
actualHash = this._computeHash(content);
} catch {
// File not found — skip (don't break other checks)
continue;
}
if (actualHash !== storedHash) {
// Save original hash if we haven't already
if (!(relPath in registry.originalHashes)) {
registry.originalHashes[relPath] = storedHash;
log.debug(`Saved original hash for ${relPath}`);
}
productJson.checksums[relPath] = actualHash;
updatedCount++;
log.info(`Updated hash: ${relPath} (${storedHash.substring(0, 8)}... -> ${actualHash.substring(0, 8)}...)`);
}
}
// 3. Write registry
this._writeRegistry(registry);
// 4. Write product.json if anything changed
if (updatedCount > 0) {
fs.writeFileSync(this._productJsonPath, JSON.stringify(productJson, null, '\t'), 'utf8');
log.info(`Updated ${updatedCount} hash(es) in product.json`);
} else {
log.debug('All hashes already match — no update needed');
}
} catch (err) {
log.error('Failed to suppress integrity check', err);
}
}
/**
* Release the integrity check suppression.
*
* Call this when uninstalling the integration. If no other SDK namespaces
* remain active, restores all original hashes in product.json.
*/
releaseCheck(): void {
try {
const registry = this._readRegistry();
// Remove this namespace
registry.namespaces = registry.namespaces.filter(ns => ns !== this._namespace);
this._writeRegistry(registry);
if (registry.namespaces.length > 0) {
// Other SDK extensions still active — recompute all hashes
log.debug(`${registry.namespaces.length} other namespace(s) still active, recomputing hashes`);
this.suppressCheck();
return;
}
// Last extension uninstalling — restore ALL original hashes
if (Object.keys(registry.originalHashes).length > 0) {
this._restoreOriginalHashes(registry.originalHashes);
log.info(`Restored ${Object.keys(registry.originalHashes).length} original hash(es)`);
}
// Clean up registry file
this._deleteRegistry();
} catch (err) {
log.error('Failed to release integrity check', err);
}
}
/**
* Re-apply integrity suppression after auto-repair.
*
* Call this after auto-repair has re-patched files
* (e.g. after an AG update that overwrote workbench files).
*/
repair(): void {
log.info('Repairing integrity check suppression...');
this.suppressCheck();
}
// ── Private helpers ─────────────────────────────────────────────
/**
* Compute SHA256 hash matching Antigravity's ChecksumService format:
* base64 WITHOUT trailing '=' padding.
*/
private _computeHash(content: Buffer): string {
return crypto.createHash('sha256')
.update(content)
.digest('base64')
.replace(/=+$/, '');
}
/**
* Restore all original hashes in product.json.
*/
private _restoreOriginalHashes(originalHashes: Record<string, string>): void {
if (!fs.existsSync(this._productJsonPath)) return;
const productJson = JSON.parse(fs.readFileSync(this._productJsonPath, 'utf8'));
if (!productJson.checksums) return;
for (const [relPath, hash] of Object.entries(originalHashes)) {
if (relPath in productJson.checksums) {
productJson.checksums[relPath] = hash;
}
}
fs.writeFileSync(this._productJsonPath, JSON.stringify(productJson, null, '\t'), 'utf8');
}
/**
* Read the coordination registry from disk.
*/
private _readRegistry(): IIntegrityRegistry {
try {
if (fs.existsSync(this._registryPath)) {
const raw = fs.readFileSync(this._registryPath, 'utf8');
const data = JSON.parse(raw);
// Migrate from old format (single originalHash) to new (originalHashes map)
let originalHashes: Record<string, string> = {};
if (data.originalHashes && typeof data.originalHashes === 'object') {
originalHashes = data.originalHashes;
} else if (typeof data.originalHash === 'string') {
// Legacy v1.5.0 format: single hash for workbench.html
originalHashes['vs/code/electron-browser/workbench/workbench.html'] = data.originalHash;
}
return {
namespaces: Array.isArray(data.namespaces) ? data.namespaces : [],
originalHashes,
};
}
} catch {
// Corrupt or inaccessible — start fresh
}
return { namespaces: [], originalHashes: {} };
}
/**
* Write the coordination registry to disk.
*/
private _writeRegistry(registry: IIntegrityRegistry): void {
try {
fs.writeFileSync(this._registryPath, JSON.stringify(registry, null, 2), 'utf8');
} catch (err) {
log.error('Failed to write integrity registry', err);
}
}
/**
* Delete the coordination registry file.
*/
private _deleteRegistry(): void {
try {
if (fs.existsSync(this._registryPath)) {
fs.unlinkSync(this._registryPath);
}
} catch {
// Ignore
}
}
}

View File

@@ -0,0 +1,554 @@
/**
* Script Generator — Builds self-contained JS from integration configs.
*
* Generates a Trusted Types-safe integration script that:
* - Uses ONLY createElement/textContent (no innerHTML)
* - Uses MutationObserver for dynamic content
* - Is fully self-contained (runs in renderer, no Node.js APIs)
*
* @module integration/script-generator
*
* @internal
*/
import { Selectors, AG_PREFIX, AG_DATA_ATTR } from './selectors';
import {
IntegrationConfig,
IntegrationPoint,
IToastConfig,
IToastRow,
TurnMetric,
IButtonIntegration,
ITurnMetaIntegration,
IUserBadgeIntegration,
IBotActionIntegration,
IDropdownIntegration,
ITitleIntegration,
} from './types';
/**
* Generates a self-contained JavaScript integration script
* from an array of IntegrationConfig objects.
*/
export class ScriptGenerator {
/**
* Generate the complete integration script.
*
* @param configs — Registered integration configurations
* @returns — Complete JS code as a string
*/
generate(configs: IntegrationConfig[]): string {
const parts: string[] = [];
parts.push(this._header());
parts.push(this._css(configs));
parts.push(this._helpers());
parts.push(this._toast());
parts.push(this._stats());
// Generate code for each integration point
const grouped = this._groupByPoint(configs);
for (const [point, cfgs] of Object.entries(grouped)) {
parts.push(this._generatePoint(point as IntegrationPoint, cfgs));
}
parts.push(this._mainLoop(Object.keys(grouped) as IntegrationPoint[]));
parts.push(this._footer());
return parts.join('\n');
}
// ─── Grouping ──────────────────────────────────────────────────────
private _groupByPoint(configs: IntegrationConfig[]): Record<string, IntegrationConfig[]> {
const groups: Record<string, IntegrationConfig[]> = {};
for (const c of configs) {
if (c.enabled === false) continue;
if (!groups[c.point]) groups[c.point] = [];
groups[c.point].push(c);
}
return groups;
}
// ─── Code Sections ────────────────────────────────────────────────
private _header(): string {
return `(function agSDK(){
'use strict';
if(window.__agSDK)return;
window.__agSDK=true;
// ─── Theme Detection ───
var _isDark=document.body.classList.contains('vscode-dark')||document.body.classList.contains('vscode-high-contrast');
var _theme={
bg:_isDark?'rgba(25,25,30,.95)':'rgba(245,245,250,.95)',
fg:_isDark?'#ccc':'#333',
fgDim:_isDark?'rgba(200,200,200,.45)':'rgba(80,80,80,.5)',
fgHover:_isDark?'rgba(200,200,200,.8)':'rgba(40,40,40,.9)',
accent:_isDark?'#4fc3f7':'#0288d1',
accentBg:_isDark?'rgba(79,195,247,.12)':'rgba(2,136,209,.08)',
success:_isDark?'#81c784':'#388e3c',
successBg:_isDark?'rgba(76,175,80,.1)':'rgba(56,142,60,.06)',
warn:_isDark?'#ffb74d':'#e65100',
border:_isDark?'rgba(79,195,247,.06)':'rgba(0,0,0,.06)',
borderHover:_isDark?'rgba(79,195,247,.2)':'rgba(2,136,209,.15)',
sep:_isDark?'rgba(255,255,255,.06)':'rgba(0,0,0,.06)',
shadow:_isDark?'rgba(0,0,0,.5)':'rgba(0,0,0,.15)',
metaBg:_isDark?'linear-gradient(135deg,rgba(79,195,247,.03),rgba(156,39,176,.02))':'linear-gradient(135deg,rgba(2,136,209,.03),rgba(123,31,162,.02))',
metaBgHover:_isDark?'linear-gradient(135deg,rgba(79,195,247,.07),rgba(156,39,176,.05))':'linear-gradient(135deg,rgba(2,136,209,.07),rgba(123,31,162,.05))'
};
// Watch for theme changes (VS Code toggles body classes)
new MutationObserver(function(){var newDark=document.body.classList.contains('vscode-dark');if(newDark!==_isDark){location.reload();}}).observe(document.body,{attributes:true,attributeFilter:['class']});
`;
}
private _footer(): string {
// The heartbeat file is in the same directory as the script.
// We use sync XHR (allowed in renderer since we're in a script tag,
// not a module) to check the file before starting.
// Max age: 48 hours (172800000ms) — enough to survive normal restarts
// but catches disabled extensions reliably.
return `
var _heartbeatMaxAge=172800000;
function checkHeartbeat(){
try{
var xhr=new XMLHttpRequest();
xhr.open('GET','./ag-sdk-heartbeat?t='+Date.now(),false);
xhr.send();
if(xhr.status!==200)return false;
var ts=parseInt(xhr.responseText,10);
if(isNaN(ts))return false;
return(Date.now()-ts)<_heartbeatMaxAge;
}catch(e){return false;}
}
function boot(){
if(!checkHeartbeat()){
console.log('[AG SDK] Heartbeat missing or stale — extension disabled? Skipping.');
return;
}
if(document.readyState==='complete')setTimeout(start,3000);
else window.addEventListener('load',function(){setTimeout(start,3000);});
}
boot();
})();`;
}
private _css(configs: IntegrationConfig[]): string {
// Only include CSS for points that are actually used
const points = new Set(configs.map(c => c.point));
// All colors now use _theme variables for light/dark mode support
// CSS is generated as a JS template that reads _theme at runtime
return `
// ─── Theme-Aware CSS ───
var _cssRules=[
'.${AG_PREFIX}meta{padding:3px 8px;background:'+_theme.metaBg+';border-top:1px solid '+_theme.border+';font-family:"Cascadia Code","Fira Code",monospace;font-size:9px;color:'+_theme.fgDim+';display:flex;align-items:center;gap:5px;flex-wrap:wrap;transition:all .2s;cursor:default;user-select:none;margin-top:2px;border-radius:0 0 6px 6px}',
'.${AG_PREFIX}meta:hover{background:'+_theme.metaBgHover+';color:'+_theme.fgHover+'}',
'.${AG_PREFIX}t{padding:1px 4px;border-radius:3px;font-size:8px;font-weight:700;letter-spacing:.3px}',
'.${AG_PREFIX}u{background:'+_theme.successBg+';color:'+_theme.success+'}',
'.${AG_PREFIX}b{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
'.${AG_PREFIX}k{color:'+_theme.fgDim+';font-size:8px}',
'.${AG_PREFIX}v{color:'+_theme.fg+';font-size:8px;opacity:.55}',
'.${AG_PREFIX}hi{color:'+_theme.accent+'}',
'.${AG_PREFIX}w{color:'+_theme.warn+'}',
'.${AG_PREFIX}s{color:'+_theme.sep+'}',
// Toast
'.${AG_PREFIX}toast{position:fixed;bottom:80px;right:20px;background:'+_theme.bg+';border:1px solid '+_theme.borderHover+';border-radius:8px;padding:10px 14px;font-family:"Cascadia Code",monospace;font-size:10px;color:'+_theme.fg+';z-index:99999;max-width:320px;backdrop-filter:blur(10px);box-shadow:0 4px 24px '+_theme.shadow+';animation:${AG_PREFIX}fade .25s ease}',
'@keyframes ${AG_PREFIX}fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}',
'.${AG_PREFIX}toast-t{color:'+_theme.accent+';font-weight:700;margin-bottom:5px;font-size:11px;display:flex;align-items:center;gap:6px}',
'.${AG_PREFIX}toast-r{display:flex;gap:8px;margin:1px 0}',
'.${AG_PREFIX}toast-k{color:'+_theme.fgDim+';min-width:70px}',
'.${AG_PREFIX}toast-v{color:'+_theme.fg+'}',
'.${AG_PREFIX}toast-badge{font-size:8px;padding:1px 5px;border-radius:3px;font-weight:700}',
// Buttons
'.${AG_PREFIX}hdr{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:4px;cursor:pointer;color:'+_theme.fgDim+';font-size:9px;font-family:"Cascadia Code",monospace;transition:all .15s;user-select:none}',
'.${AG_PREFIX}hdr:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
'.${AG_PREFIX}inp{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;border-radius:4px;cursor:pointer;color:'+_theme.fgDim+';font-size:11px;transition:all .15s;flex-shrink:0;padding:0 4px;font-family:"Cascadia Code",monospace}',
'.${AG_PREFIX}inp:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
'.${AG_PREFIX}menu{padding:4px 8px;cursor:pointer;font-size:11px;color:'+_theme.fg+';opacity:.7;transition:all .12s;display:flex;align-items:center;gap:6px;white-space:nowrap}',
'.${AG_PREFIX}menu:hover{background:'+_theme.accentBg+';color:'+_theme.accent+';opacity:1}',
'.${AG_PREFIX}vote{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:3px;cursor:pointer;color:'+_theme.fgDim+';font-size:9px;font-family:"Cascadia Code",monospace;transition:all .15s;margin-left:4px}',
'.${AG_PREFIX}vote:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
'.${AG_PREFIX}ubadge{display:inline-flex;align-items:center;gap:2px;padding:1px 5px;border-radius:3px;background:'+_theme.successBg+';cursor:pointer;color:'+_theme.success+';opacity:.4;font-size:8px;font-family:"Cascadia Code",monospace;transition:all .15s;margin-left:3px}',
'.${AG_PREFIX}ubadge:hover{background:'+_theme.successBg+';color:'+_theme.success+';opacity:1}',
'.${AG_PREFIX}title-hint{position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:8px;color:'+_theme.accent+';opacity:.3;pointer-events:none;font-family:"Cascadia Code",monospace;transition:opacity .2s}',
'.${AG_PREFIX}title-wrap:hover .${AG_PREFIX}title-hint{opacity:1}'
];
var css=document.createElement('style');
css.textContent=_cssRules.join('\\n');
document.head.appendChild(css);
`;
}
private _helpers(): string {
return `
function mk(tag,cls,txt){var e=document.createElement(tag);if(cls)e.className=cls;if(txt!==undefined)e.textContent=txt;return e;}
function fmt(n){return n>=1000?(n/1000).toFixed(1)+'k':''+n;}
`;
}
private _toast(): string {
return `
var _toastT=0;
function toast(title,badge,rows){
var old=document.querySelector('.${AG_PREFIX}toast');if(old)old.remove();
var t=mk('div','${AG_PREFIX}toast');
var hdr=mk('div','${AG_PREFIX}toast-t');
hdr.appendChild(document.createTextNode(title));
if(badge){var b=mk('span','${AG_PREFIX}toast-badge');b.textContent=badge[0];b.style.background=badge[1];b.style.color=badge[2];hdr.appendChild(b);}
t.appendChild(hdr);
rows.forEach(function(r){var row=mk('div','${AG_PREFIX}toast-r');row.appendChild(mk('span','${AG_PREFIX}toast-k',r[0]));row.appendChild(mk('span','${AG_PREFIX}toast-v',r[1]));t.appendChild(row);});
document.body.appendChild(t);
clearTimeout(_toastT);_toastT=setTimeout(function(){if(t.parentNode)t.remove();},6000);
t.addEventListener('click',function(){t.remove();});
}
`;
}
private _stats(): string {
return `
function getStats(){
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});
if(!c)return null;
var turns=0,uC=0,bC=0,code=0;
Array.from(c.children).forEach(function(ch){
if(ch.getAttribute('${AG_DATA_ATTR}')||ch.children.length<1)return;
turns++;
uC+=(ch.children[0]?.textContent?.trim()||'').length;
bC+=(ch.children[1]?.textContent?.trim()||'').length;
code+=(ch.children[1]?.querySelectorAll('pre')?.length||0);
});
return{turns:turns,u:uC,b:bC,code:code};
}
`;
}
// ─── Point generators ─────────────────────────────────────────────
private _generatePoint(point: IntegrationPoint, configs: IntegrationConfig[]): string {
switch (point) {
case IntegrationPoint.TOP_BAR:
return this._genTopBar(configs as IButtonIntegration[]);
case IntegrationPoint.TOP_RIGHT:
return this._genTopRight(configs as IButtonIntegration[]);
case IntegrationPoint.INPUT_AREA:
return this._genInputArea(configs as IButtonIntegration[]);
case IntegrationPoint.BOTTOM_ICONS:
return this._genBottomIcons(configs as IButtonIntegration[]);
case IntegrationPoint.TURN_METADATA:
return this._genTurnMeta(configs as ITurnMetaIntegration[]);
case IntegrationPoint.USER_BADGE:
return this._genUserBadge(configs as IUserBadgeIntegration[]);
case IntegrationPoint.BOT_ACTION:
return this._genBotAction(configs as IBotActionIntegration[]);
case IntegrationPoint.DROPDOWN_MENU:
return this._genDropdown(configs as IDropdownIntegration[]);
case IntegrationPoint.CHAT_TITLE:
return this._genTitle(configs as ITitleIntegration[]);
default:
return `// Unknown point: ${point}`;
}
}
private _genToastCall(toast?: IToastConfig): string {
if (!toast) return '';
const badge = toast.badge
? `[${JSON.stringify(toast.badge.text)},${JSON.stringify(toast.badge.bgColor)},${JSON.stringify(toast.badge.textColor)}]`
: 'null';
const rows = toast.rows
.map(r => {
if (r.dynamic) {
return `[${JSON.stringify(r.key)},${r.value}]`;
}
return `[${JSON.stringify(r.key)},${JSON.stringify(r.value)}]`;
})
.join(',');
return `toast(${JSON.stringify(toast.title)},${badge},[${rows}]);`;
}
private _genTopBar(configs: IButtonIntegration[]): string {
const buttons = configs.map(c => {
const toastCall = this._genToastCall(c.toast);
return ` var btn_${c.id}=mk('a','${AG_PREFIX}hdr ${AG_PREFIX}${c.id}');
btn_${c.id}.textContent=${JSON.stringify(c.icon)};
btn_${c.id}.title=${JSON.stringify(c.tooltip || '')};
btn_${c.id}.addEventListener('click',function(){${toastCall}});
iconsArea.insertBefore(btn_${c.id},iconsArea.children[1]);`;
});
return `
function integrateTopBar(){
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
var topBar=p.querySelector(${JSON.stringify(Selectors.TOP_BAR)});if(!topBar)return;
var iconsArea=topBar.querySelector(${JSON.stringify(Selectors.TOP_ICONS)});
if(!iconsArea||iconsArea.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
${buttons.join('\n')}
}
`;
}
private _genTopRight(configs: IButtonIntegration[]): string {
const buttons = configs.map(c => {
const toastCall = this._genToastCall(c.toast);
return ` var btn_${c.id}=mk('a','${AG_PREFIX}hdr ${AG_PREFIX}${c.id}');
btn_${c.id}.textContent=${JSON.stringify(c.icon)};
btn_${c.id}.title=${JSON.stringify(c.tooltip || '')};
btn_${c.id}.addEventListener('click',function(){${toastCall}});
iconsArea.insertBefore(btn_${c.id},iconsArea.lastElementChild);`;
});
return `
function integrateTopRight(){
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
var topBar=p.querySelector(${JSON.stringify(Selectors.TOP_BAR)});if(!topBar)return;
var iconsArea=topBar.querySelector(${JSON.stringify(Selectors.TOP_ICONS)});
if(!iconsArea||iconsArea.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
${buttons.join('\n')}
}
`;
}
private _genInputArea(configs: IButtonIntegration[]): string {
const buttons = configs.map(c => {
const toastCall = this._genToastCall(c.toast);
return ` var btn=mk('div','${AG_PREFIX}inp ${AG_PREFIX}${c.id}');
btn.textContent=${JSON.stringify(c.icon)};
btn.title=${JSON.stringify(c.tooltip || '')};
btn.addEventListener('click',function(){${toastCall}});
btnRow.insertBefore(btn,btnRow.firstChild);`;
});
return `
function integrateInputArea(){
var ib=document.querySelector(${JSON.stringify(Selectors.INPUT_BOX)});
if(!ib||ib.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
var allBtns=ib.querySelectorAll('button,[role="button"]');
if(allBtns.length===0)return;
var btnRow=allBtns[allBtns.length-1].parentElement;if(!btnRow)return;
${buttons.join('\n')}
}
`;
}
private _genBottomIcons(configs: IButtonIntegration[]): string {
const buttons = configs.map(c => {
const toastCall = this._genToastCall(c.toast);
return ` var btn=mk('div','${AG_PREFIX}inp ${AG_PREFIX}${c.id}');
btn.textContent=${JSON.stringify(c.icon)};
btn.title=${JSON.stringify(c.tooltip || '')};
btn.addEventListener('click',function(){${toastCall}});
row.appendChild(btn);`;
});
return `
function integrateBottomIcons(){
var ib=document.querySelector(${JSON.stringify(Selectors.INPUT_BOX)});
if(!ib||ib.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
var rows=ib.querySelectorAll('.flex.items-center');
var row=null;
for(var i=0;i<rows.length;i++){if(rows[i].querySelectorAll('svg').length>=2){row=rows[i];}}
if(!row)return;
${buttons.join('\n')}
}
`;
}
private _genTurnMeta(configs: ITurnMetaIntegration[]): string {
// Take first config for metrics (single turn metadata style)
const cfg = configs[0];
const metricParts: string[] = [];
for (const m of cfg.metrics) {
switch (m) {
case 'turnNumber':
metricParts.push(`meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}b','T'+tI));`);
break;
case 'userCharCount':
metricParts.push(`if(uL>0){meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}u','USER'));meta.appendChild(mk('span','${AG_PREFIX}k',fmt(uL)));}`);
break;
case 'separator':
metricParts.push(`if(uL>0&&bL>0)meta.appendChild(mk('span','${AG_PREFIX}s','\\u2502'));`);
break;
case 'aiCharCount':
metricParts.push(`if(bL>0){meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}b','AI'));meta.appendChild(mk('span','${AG_PREFIX}k',fmt(bL)));}`);
break;
case 'codeBlocks':
metricParts.push(`if(codes>0){meta.appendChild(mk('span','${AG_PREFIX}k','code:'));meta.appendChild(mk('span','${AG_PREFIX}v ${AG_PREFIX}w',''+codes));}`);
break;
case 'thinkingIndicator':
metricParts.push(`if(brain)meta.appendChild(mk('span','${AG_PREFIX}v','\\u{1F9E0}'));`);
break;
case 'ratio':
metricParts.push(`if(uL>0&&bL>0){meta.appendChild(mk('span','${AG_PREFIX}k',(bL/uL).toFixed(1)+'x'));}`);
break;
}
}
const clickHandler = cfg.clickable !== false
? `meta.addEventListener('click',function(){toast('Turn '+tI,null,[['user:',fmt(uL)],['AI:',fmt(bL)],['ratio:',uL>0?(bL/uL).toFixed(1)+'x':'\\u2014']]);});`
: '';
return `
function integrateTurnMeta(){
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
var tI=0;
Array.from(c.children).forEach(function(turn){
if(turn.getAttribute('${AG_DATA_ATTR}')||turn.children.length<1)return;
turn.setAttribute('${AG_DATA_ATTR}','1');
tI++;var uL=(turn.children[0]?.textContent?.trim()||'').length;
var bL=(turn.children[1]?.textContent?.trim()||'').length;
if(uL===0&&bL===0)return;
var codes=turn.children[1]?.querySelectorAll('pre')?.length||0;
var brain=(turn.children[1]?.textContent||'').includes('Thought');
var meta=mk('div','${AG_PREFIX}meta');
${metricParts.join('\n ')}
${clickHandler}
turn.appendChild(meta);
});
}
`;
}
private _genUserBadge(configs: IUserBadgeIntegration[]): string {
const cfg = configs[0];
let displayExpr = 'fmt(uLen)+" ch"';
if (cfg.display === 'wordCount') {
displayExpr = '(txt.split(/\\\\s+/).length)+" w"';
} else if (cfg.display === 'custom' && cfg.customFormat) {
displayExpr = cfg.customFormat;
}
return `
function integrateUserBadges(){
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
Array.from(c.children).forEach(function(turn,i){
if(turn.getAttribute('${AG_DATA_ATTR}u')||turn.children.length<1)return;
var bubble=turn.children[0]?.querySelector(${JSON.stringify(Selectors.USER_BUBBLE)});
if(!bubble)return;
var txt=turn.children[0]?.textContent?.trim()||'';
var uLen=txt.length;if(uLen<5)return;
turn.setAttribute('${AG_DATA_ATTR}u','1');
var row=turn.children[0]?.querySelector('.flex.w-full,.flex.flex-row')||turn.children[0];
var badge=mk('span','${AG_PREFIX}ubadge');
badge.textContent=${displayExpr};
badge.title='SDK: User message';
row.appendChild(badge);
});
}
`;
}
private _genBotAction(configs: IBotActionIntegration[]): string {
const items = configs.map(c => {
const toastCall = this._genToastCall(c.toast);
return `var b=mk('span','${AG_PREFIX}vote');b.textContent=${JSON.stringify(c.icon + ' ' + c.label)};
b.addEventListener('click',function(ev){ev.stopPropagation();${toastCall}});
row.appendChild(b);`;
});
return `
function integrateBotAction(){
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
c.querySelectorAll('span,button,a,div').forEach(function(el){
if(el.getAttribute('${AG_DATA_ATTR}v'))return;
var txt=el.textContent?.trim();
if(txt==='Good'||txt==='Bad'){
var row=el.parentElement;if(!row||row.querySelector('.${AG_PREFIX}vote'))return;
el.setAttribute('${AG_DATA_ATTR}v','1');
${items.join('\n ')}
}
});
}
`;
}
private _genDropdown(configs: IDropdownIntegration[]): string {
const markers = JSON.stringify(Selectors.DROPDOWN_MARKER_TEXT);
const items = configs.map(c => {
const toastCall = this._genToastCall(c.toast);
const sep = c.separator
? `var sep=mk('div','');sep.style.cssText='height:1px;background:rgba(255,255,255,.06);margin:4px 8px';dd.appendChild(sep);`
: '';
return `${sep}
var mi=mk('div','${AG_PREFIX}menu');
${c.icon ? `mi.appendChild(mk('span','',${JSON.stringify(c.icon)}));` : ''}
mi.appendChild(document.createTextNode(${JSON.stringify(c.label)}));
mi.addEventListener('click',function(){${toastCall}});
dd.appendChild(mi);`;
});
return `
function integrateDropdown(){
var dds=document.querySelectorAll('.rounded-bg.py-1,.rounded-lg.py-1');
dds.forEach(function(dd){
if(dd.getAttribute('${AG_DATA_ATTR}m'))return;
var items=dd.querySelectorAll(${JSON.stringify(Selectors.DROPDOWN_ITEM)});
var markers=${markers};
var found=false;
items.forEach(function(it){markers.forEach(function(m){if((it.textContent||'').includes(m))found=true;});});
if(!found)return;
dd.setAttribute('${AG_DATA_ATTR}m','1');
${items.join('\n ')}
});
}
`;
}
private _genTitle(configs: ITitleIntegration[]): string {
const cfg = configs[0];
const toastCall = this._genToastCall(cfg.toast);
const event = cfg.interaction || 'dblclick';
return `
function integrateTitle(){
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
var el=p.querySelector(${JSON.stringify(Selectors.TITLE)});
if(!el||el.getAttribute('${AG_DATA_ATTR}t'))return;
el.setAttribute('${AG_DATA_ATTR}t','1');
el.style.cursor='pointer';
el.classList.add('${AG_PREFIX}title-wrap');
el.style.position='relative';
${cfg.hint ? `var hint=mk('span','${AG_PREFIX}title-hint',${JSON.stringify(cfg.hint)});el.appendChild(hint);` : ''}
el.addEventListener(${JSON.stringify(event)},function(){
var title=el.textContent?.replace(${JSON.stringify(cfg.hint || '')},'')?.trim()||'';
${toastCall || `toast('Chat',null,[['title:',title],['chars:',''+title.length]]);`}
});
}
`;
}
// ─── Main loop ────────────────────────────────────────────────────
private _mainLoop(points: IntegrationPoint[]): string {
const fnMap: Record<string, string> = {
[IntegrationPoint.TOP_BAR]: 'integrateTopBar',
[IntegrationPoint.TOP_RIGHT]: 'integrateTopRight',
[IntegrationPoint.INPUT_AREA]: 'integrateInputArea',
[IntegrationPoint.BOTTOM_ICONS]: 'integrateBottomIcons',
[IntegrationPoint.TURN_METADATA]: 'integrateTurnMeta',
[IntegrationPoint.USER_BADGE]: 'integrateUserBadges',
[IntegrationPoint.BOT_ACTION]: 'integrateBotAction',
[IntegrationPoint.DROPDOWN_MENU]: 'integrateDropdown',
[IntegrationPoint.CHAT_TITLE]: 'integrateTitle',
};
const calls = points.map(p => ` ${fnMap[p]}();`).join('\n');
return `
function fullScan(){
${calls}
}
var _timer=0;
function debounced(){clearTimeout(_timer);_timer=setTimeout(function(){requestAnimationFrame(fullScan);},400);}
function start(){
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});
if(!p){setTimeout(start,1000);return;}
fullScan();
new MutationObserver(debounced).observe(p,{childList:true,subtree:true});
setInterval(fullScan,8000);
console.log('[AG SDK] Active \\u2014 ${points.length} integration points');
}
`;
}
}

View File

@@ -0,0 +1,56 @@
/**
* DOM Selectors — Single source of truth for all Agent View selectors.
*
* Verified against Antigravity v1.107.0 DOM (2026-02-28).
* If Antigravity updates break selectors, only THIS file needs updating.
*
* @module integration/selectors
*
* @internal
*/
export const Selectors = {
/** The entire agent side panel container */
PANEL: '.antigravity-agent-side-panel',
/** Top bar with title and action icons */
TOP_BAR: '.flex.items-center.justify-between',
/** Icons area in top bar (contains +, refresh, ..., X) */
TOP_ICONS: '.flex.items-center.gap-2',
/** Chat title element */
TITLE: '.flex.min-w-0.items-center.overflow-hidden',
/** Main conversation scroll area */
CONVERSATION: '#conversation',
/** Message turns container (direct children are turns) */
TURNS_CONTAINER: '#conversation .gap-y-3',
/** User message bubble (inside turn) */
USER_BUBBLE: '.rounded-lg',
/** Input box container */
INPUT_BOX: '#antigravity\\.agentSidePanelInputBox',
/** 3-dot dropdown menu (appears dynamically) */
DROPDOWN_MARKER_TEXT: ['Customization', 'Export'],
/** Dropdown menu item class pattern */
DROPDOWN_ITEM: '.cursor-pointer',
/** Good/Bad feedback text markers */
FEEDBACK_MARKERS: ['Good', 'Bad'],
} as const;
/**
* CSS class prefixes used by SDK integrations.
* Used to identify and clean up integrated elements.
*/
export const AG_PREFIX = 'ag-';
/**
* Data attribute used to mark processed elements.
*/
export const AG_DATA_ATTR = 'data-ag-sdk';

View File

@@ -0,0 +1,171 @@
/**
* Title Manager — Extension-host API for managing chat titles.
*
* Allows extensions to programmatically rename conversations
* by writing to a data file that the renderer-side title proxy reads.
*
* Also provides a direct localStorage synchronization mechanism
* via the integration script's window.__agSDKTitles API.
*
* @module integration/title-manager
*
* @example
* ```typescript
* const sdk = new AntigravitySDK(context);
* await sdk.initialize();
*
* // Rename via extension host (writes data file, renderer picks up on next poll)
* sdk.titles.rename('cascade-uuid', 'My Custom Title');
*
* // Get all custom titles
* const titles = sdk.titles.getAll();
*
* // Remove a custom title (reverts to auto-generated summary)
* sdk.titles.remove('cascade-uuid');
* ```
*/
import * as fs from 'fs';
import * as path from 'path';
import { Logger } from '../core/logger';
import { IDisposable } from '../core/disposable';
import { getTitlesDataFile } from './title-proxy';
const log = new Logger('TitleManager');
/**
* Manages custom conversation titles from the extension host.
*
* Titles are persisted in a JSON file in the workbench directory.
* The renderer-side title proxy reads this file and merges with localStorage.
*/
export class TitleManager implements IDisposable {
private _titles: Record<string, string> = {};
private _dataPath: string = '';
private _initialized = false;
/**
* Initialize with the workbench directory path.
*
* @param workbenchDir - Path to workbench directory where data file is stored
* @param namespace - Extension namespace for file isolation
*/
initialize(workbenchDir: string, namespace: string = 'default'): void {
this._dataPath = path.join(workbenchDir, getTitlesDataFile(namespace));
this._load();
this._initialized = true;
log.info(`Initialized, ${Object.keys(this._titles).length} custom titles loaded`);
}
/**
* Check if the manager is initialized.
*/
get isInitialized(): boolean {
return this._initialized;
}
/**
* Set a custom title for a conversation.
*
* The title will be displayed in the Agent View title bar
* and conversation list instead of the auto-generated summary.
*
* @param cascadeId - The conversation's cascade ID (UUID)
* @param title - The custom title to display
*
* @example
* ```typescript
* // Rename the active conversation
* const id = sdk.titles.getActiveCascadeId();
* sdk.titles.rename(id, 'Project Alpha Discussion');
* ```
*/
rename(cascadeId: string, title: string): void {
if (!cascadeId) {
log.warn('rename: cascadeId is required');
return;
}
if (!title || !title.trim()) {
log.warn('rename: title cannot be empty');
return;
}
this._titles[cascadeId] = title.trim();
this._save();
log.debug(`Renamed ${cascadeId.substring(0, 8)}... -> "${title.trim()}"`);
}
/**
* Get the custom title for a conversation.
*
* @param cascadeId - The conversation's cascade ID
* @returns The custom title, or undefined if no custom title is set
*/
getTitle(cascadeId: string): string | undefined {
return this._titles[cascadeId];
}
/**
* Get all custom titles.
*
* @returns A copy of the titles map (cascadeId -> title)
*/
getAll(): Readonly<Record<string, string>> {
return { ...this._titles };
}
/**
* Remove a custom title, reverting to the auto-generated summary.
*
* @param cascadeId - The conversation's cascade ID
*/
remove(cascadeId: string): void {
if (this._titles[cascadeId]) {
delete this._titles[cascadeId];
this._save();
log.debug(`Removed title for ${cascadeId.substring(0, 8)}...`);
}
}
/**
* Remove all custom titles.
*/
clear(): void {
this._titles = {};
this._save();
log.debug('Cleared all custom titles');
}
/**
* Get the number of custom titles.
*/
get count(): number {
return Object.keys(this._titles).length;
}
/** Load titles from the data file */
private _load(): void {
try {
if (fs.existsSync(this._dataPath)) {
const content = fs.readFileSync(this._dataPath, 'utf8');
this._titles = JSON.parse(content) || {};
}
} catch (err) {
log.warn(`Failed to load titles: ${err}`);
this._titles = {};
}
}
/** Save titles to the data file */
private _save(): void {
if (!this._dataPath) return;
try {
fs.writeFileSync(this._dataPath, JSON.stringify(this._titles, null, 2), 'utf8');
} catch (err) {
log.warn(`Failed to save titles: ${err}`);
}
}
dispose(): void {
// Nothing to clean up - titles persist on disk
}
}

View File

@@ -0,0 +1,292 @@
/**
* Title Proxy — Renderer-side code for intercepting chat summaries.
*
* Generates JavaScript that runs in the workbench renderer process.
* Uses Preact VNode context walk to find the summaries provider,
* wraps getState() to inject custom titles from localStorage,
* and captures onDidChange listeners for forced re-renders.
*
* All identifiers used here are STRUCTURALLY MATCHED, not hardcoded
* minified variable names — this survives obfuscation changes.
*
* @module integration/title-proxy
* @internal
*/
/** localStorage key prefix for custom titles */
const TITLES_STORAGE_PREFIX = 'ag-sdk-titles';
/** Data file prefix for extension-host to set titles */
const TITLES_DATA_PREFIX = 'ag-sdk-titles';
/**
* Generate the renderer-side title proxy JavaScript.
*
* This code:
* 1. BFS walks the Preact VNode tree (limit 3000, arrays not counted)
* 2. Finds summaries provider via structural matching
* 3. Wraps provider.getState() to inject custom titles
* 4. Captures onDidChange listeners for forced re-renders
* 5. Reads custom titles from localStorage + data file
* 6. Exposes window.__agSDKTitles API for inline rename
*
* @param dataFilePath - Relative path to the JSON data file (for extension-host titles)
* @returns JavaScript source code
*/
export function generateTitleProxyCode(namespace: string = 'default'): string {
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
const storageKey = `${TITLES_STORAGE_PREFIX}-${slug}`;
const dataFile = `./${TITLES_DATA_PREFIX}-${slug}.json`;
return `
// ── AG SDK: Title Proxy ──────────────────────────────────────────
// Intercepts summaries provider to inject custom chat titles.
// Uses structural matching (obfuscation-safe).
(function initTitleProxy(){
var PANEL_SEL='.antigravity-agent-side-panel';
var TITLE_SEL='.flex.min-w-0.items-center.overflow-hidden';
var STORAGE_KEY='${storageKey}';
var DATA_FILE='${dataFile}';
var _provider=null;
var _origGetState=null;
var _listeners=[];
var _customTitles={};
var _searchTime=0;
// ── Load / Save ────────────────────────────────────────────────
function loadTitles(){
// Step 1: Load from localStorage (sync, fast)
try{_customTitles=JSON.parse(localStorage.getItem(STORAGE_KEY)||'{}');}catch(e){_customTitles={};}
// Step 2: Merge extension-host titles from data file (async fetch)
fetch(DATA_FILE).then(function(r){
if(!r.ok)return;
return r.text();
}).then(function(text){
if(!text)return;
try{
var extTitles=JSON.parse(text);
if(extTitles&&typeof extTitles==='object'){
for(var k in extTitles){_customTitles[k]=extTitles[k];}
saveTitles();
notifyListeners();
}
}catch(e){}
}).catch(function(){});
}
function saveTitles(){
try{localStorage.setItem(STORAGE_KEY,JSON.stringify(_customTitles));}catch(e){}
}
// ── Notify ─────────────────────────────────────────────────────
function notifyListeners(){
for(var i=0;i<_listeners.length;i++){try{_listeners[i]();}catch(e){}}
}
// ── Provider Wrapping ──────────────────────────────────────────
function wrapProvider(provider){
if(provider.__agSDKWrapped)return;
provider.__agSDKWrapped=true;
_provider=provider;
var origFn=provider.getState;
_origGetState=origFn;
// Wrap getState to inject custom titles
provider.getState=function(){
var state=origFn.call(provider);
if(!state||!state.summaries)return state;
var hasOverrides=false;
for(var cid in _customTitles){if(state.summaries[cid]){hasOverrides=true;break;}}
if(!hasOverrides)return state;
var ns={};
for(var k in state.summaries)ns[k]=state.summaries[k];
for(var cid in _customTitles){
if(ns[cid]){
var copy={};for(var p in ns[cid])copy[p]=ns[cid][p];
copy.summary=_customTitles[cid];
ns[cid]=copy;
}
}
var newState={};for(var sk in state)newState[sk]=state[sk];
newState.summaries=ns;
return newState;
};
// Intercept onDidChange to capture listeners
var origOnDidChange=provider.onDidChange;
provider.onDidChange=function(callback){
_listeners.push(callback);
var origDispose=origOnDidChange.call(this,callback);
return{dispose:function(){
var idx=_listeners.indexOf(callback);
if(idx>=0)_listeners.splice(idx,1);
origDispose.dispose();
}};
};
console.log('[AG SDK] Title proxy active, custom titles:', Object.keys(_customTitles).length);
// Force re-render so custom titles appear immediately
// (without waiting for next native summaries update)
setTimeout(function(){notifyListeners();},50);
}
// ── VNode BFS Walk ─────────────────────────────────────────────
function findProvider(){
if(_provider)return;
var panel=document.querySelector(PANEL_SEL);
if(!panel||!panel.__k)return;
// Throttle only AFTER confirming panel exists (don't block retries when panel isn't mounted)
var now=Date.now();
if(_searchTime&&now-_searchTime<30000)return;
_searchTime=now;
var queue=[panel.__k],visited=0;
while(queue.length>0&&visited<3000){
var node=queue.shift();
if(!node)continue;
if(Array.isArray(node)){
for(var ai=0;ai<node.length;ai++){if(node[ai])queue.push(node[ai]);}
continue;
}
visited++;
var comp=node.__c;
if(comp&&comp.context&&typeof comp.context==='object'){
for(var key in comp.context){
try{
var ctx=comp.context[key];
if(!ctx||!ctx.props||!ctx.props.value)continue;
var val=ctx.props.value;
// Structural match: {provider: {getState() -> {summaries}}}
if(val.provider&&typeof val.provider.getState==='function'){
var ts=val.provider.getState();
if(ts&&ts.summaries){wrapProvider(val.provider);return;}
}
// Structural match: {trajectorySummariesProvider: {getState() -> {summaries}}}
if(val.trajectorySummariesProvider&&typeof val.trajectorySummariesProvider.getState==='function'){
var ts2=val.trajectorySummariesProvider.getState();
if(ts2&&ts2.summaries){wrapProvider(val.trajectorySummariesProvider);return;}
}
}catch(e){}
}
}
// Direct props match
if(comp&&comp.props&&comp.props.trajectorySummariesProvider){
var tsp=comp.props.trajectorySummariesProvider;
if(typeof tsp.getState==='function'){
try{var ts3=tsp.getState();
if(ts3&&ts3.summaries){wrapProvider(tsp);return;}
}catch(e){}
}
}
if(node.__k){
if(Array.isArray(node.__k)){for(var ki=0;ki<node.__k.length;ki++){if(node.__k[ki])queue.push(node.__k[ki]);}}
else{queue.push(node.__k);}
}
}
}
// ── CascadeId Resolution ───────────────────────────────────────
function findCascadeIdByTitle(text){
if(!_origGetState)return '';
try{
var state=_origGetState.call(_provider);
if(!state||!state.summaries)return '';
// Reverse lookup custom titles first
for(var cid in _customTitles){if(_customTitles[cid]===text)return cid;}
// Match original summaries
var bestId='',bestTime=0;
for(var cid in state.summaries){
var e=state.summaries[cid];
if(e&&e.summary===text){
var t=0;try{t=new Date(e.lastModifiedTime).getTime();}catch(e){}
if(!bestId||t>bestTime){bestId=cid;bestTime=t;}
}
}
return bestId;
}catch(e){return '';}
}
// ── Public API ─────────────────────────────────────────────────
window.__agSDKTitles={
rename:function(cascadeId,title){
if(!cascadeId||!title)return false;
_customTitles[cascadeId]=title;
saveTitles();
notifyListeners();
return true;
},
renameByCurrentTitle:function(currentTitle,newTitle){
var cid=findCascadeIdByTitle(currentTitle);
if(!cid)return false;
return this.rename(cid,newTitle);
},
remove:function(cascadeId){
delete _customTitles[cascadeId];
saveTitles();
notifyListeners();
},
getTitle:function(cascadeId){return _customTitles[cascadeId]||null;},
getAll:function(){var copy={};for(var k in _customTitles)copy[k]=_customTitles[k];return copy;},
getActiveCascadeId:function(){
var panel=document.querySelector(PANEL_SEL);
if(!panel)return '';
var titleEl=panel.querySelector(TITLE_SEL);
if(!titleEl)return '';
var text='';
function findText(el){
for(var i=0;i<el.childNodes.length;i++){
var n=el.childNodes[i];
if(n.nodeType===3&&n.textContent.trim().length>0)return n.textContent.trim();
if(n.nodeType===1){var found=findText(n);if(found)return found;}
}
return '';
}
text=findText(titleEl);
return text?findCascadeIdByTitle(text):'';
},
isReady:function(){return !!_provider;},
reload:function(){loadTitles();notifyListeners();}
};
// ── Init ───────────────────────────────────────────────────────
loadTitles();
function poll(){
findProvider();
}
// Poll until provider found, then every 30s for recovery
var pollTimer=setInterval(function(){poll();},2000);
// Initial attempt after DOM is ready
if(document.querySelector(PANEL_SEL)){
poll();
}
})();
`;
}
/**
* Get the data file name for extension-host titles.
*/
export function getTitlesDataFile(namespace: string = 'default'): string {
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
return `${TITLES_DATA_PREFIX}-${slug}.json`;
}
/**
* Get the localStorage key used by the renderer.
*/
export function getTitlesStorageKey(namespace: string = 'default'): string {
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
return `${TITLES_STORAGE_PREFIX}-${slug}`;
}

View File

@@ -0,0 +1,213 @@
/**
* Integration module types — standardized UI integration points
* for the Antigravity Agent View.
*
* @module integration/types
*/
// ─── Integration Points ──────────────────────────────────────────────────
/**
* Standardized integration points in the Agent View UI.
*
* Each point corresponds to a specific DOM location in the
* Antigravity chat interface (verified 2026-02-28).
*/
export enum IntegrationPoint {
/** Top bar — next to +, refresh, ... icons */
TOP_BAR = 'topBar',
/** Top right corner — before the X (close) button */
TOP_RIGHT = 'topRight',
/** Input area — next to voice/send buttons */
INPUT_AREA = 'inputArea',
/** Bottom icon row — file, terminal, artifact, chrome icons */
BOTTOM_ICONS = 'bottomIcons',
/** Per-turn metadata — appended inside each conversation turn */
TURN_METADATA = 'turnMeta',
/** User message badge — small badge inside user message bubbles */
USER_BADGE = 'userBadge',
/** Bot response action — button next to Good/Bad feedback */
BOT_ACTION = 'botAction',
/** 3-dot dropdown menu — extra items in the overflow menu */
DROPDOWN_MENU = 'dropdownMenu',
/** Chat title bar — interaction on conversation title */
CHAT_TITLE = 'chatTitle',
}
// ─── Configuration Interfaces ──────────────────────────────────────────
/**
* Base configuration for all integration points.
*/
export interface IIntegrationBase {
/** Unique ID for this integration (prevents duplicates) */
id: string;
/** Which integration point to target */
point: IntegrationPoint;
/** Whether this integration is enabled (default: true) */
enabled?: boolean;
}
/**
* Configuration for button-type integrations (top bar, input area, etc.).
*/
export interface IButtonIntegration extends IIntegrationBase {
point:
| IntegrationPoint.TOP_BAR
| IntegrationPoint.TOP_RIGHT
| IntegrationPoint.INPUT_AREA
| IntegrationPoint.BOTTOM_ICONS;
/** Icon (emoji or text glyph) */
icon: string;
/** Tooltip text */
tooltip?: string;
/** Toast to show on click */
toast?: IToastConfig;
/** CSS class override */
className?: string;
}
/**
* Configuration for turn-level metadata integration.
*/
export interface ITurnMetaIntegration extends IIntegrationBase {
point: IntegrationPoint.TURN_METADATA;
/** Which metrics to display */
metrics: TurnMetric[];
/** Whether turns are clickable to show details toast */
clickable?: boolean;
}
/**
* Configuration for user message badges.
*/
export interface IUserBadgeIntegration extends IIntegrationBase {
point: IntegrationPoint.USER_BADGE;
/** What to show in the badge */
display: 'charCount' | 'wordCount' | 'custom';
/** Custom formatter function body (receives `textLength` as arg) */
customFormat?: string;
}
/**
* Configuration for bot response action buttons.
*/
export interface IBotActionIntegration extends IIntegrationBase {
point: IntegrationPoint.BOT_ACTION;
/** Icon */
icon: string;
/** Label text */
label: string;
/** Toast config on click */
toast?: IToastConfig;
}
/**
* Configuration for dropdown menu items.
*/
export interface IDropdownIntegration extends IIntegrationBase {
point: IntegrationPoint.DROPDOWN_MENU;
/** Menu item icon */
icon?: string;
/** Menu item label */
label: string;
/** Add separator before this item */
separator?: boolean;
/** Toast config on click */
toast?: IToastConfig;
}
/**
* Configuration for chat title interaction.
*/
export interface ITitleIntegration extends IIntegrationBase {
point: IntegrationPoint.CHAT_TITLE;
/** Interaction type */
interaction: 'click' | 'dblclick' | 'hover';
/** Hint text shown on hover */
hint?: string;
/** Toast config on interaction */
toast?: IToastConfig;
}
/**
* Toast popup configuration.
*/
export interface IToastConfig {
/** Toast title */
title: string;
/** Badge label and colors */
badge?: {
text: string;
bgColor: string;
textColor: string;
};
/** Key-value rows to display */
rows: IToastRow[];
/** Auto-dismiss after N milliseconds (default: 6000) */
duration?: number;
}
/**
* A row in a toast popup.
*/
export interface IToastRow {
/** Label (left side) */
key: string;
/**
* Value (right side).
* Can be a static string or a dynamic expression.
* Dynamic expressions are JS code that runs in the renderer,
* with access to `getStats()` which returns conversation stats.
*/
value: string;
/** If true, `value` is treated as a JS expression */
dynamic?: boolean;
}
/**
* Metrics available for turn metadata display.
*/
export type TurnMetric =
| 'turnNumber'
| 'userCharCount'
| 'aiCharCount'
| 'codeBlocks'
| 'thinkingIndicator'
| 'ratio'
| 'separator';
/**
* Union type of all integration configurations.
*/
export type IntegrationConfig =
| IButtonIntegration
| ITurnMetaIntegration
| IUserBadgeIntegration
| IBotActionIntegration
| IDropdownIntegration
| ITitleIntegration;
// ─── Manager Interface ────────────────────────────────────────────────
/**
* Public interface for the Integration Manager.
*/
export interface IIntegrationManager {
/** Register a single integration point */
register(config: IntegrationConfig): void;
/** Register multiple integration points at once */
registerMany(configs: IntegrationConfig[]): void;
/** Remove a registered integration by ID */
unregister(id: string): void;
/** Get all registered integrations */
getRegistered(): ReadonlyArray<IntegrationConfig>;
/** Generate the integration script from all registered configs */
build(): string;
/** Install the generated script into workbench.html. Returns true if content changed. */
install(): Promise<boolean>;
/** Remove the integration from workbench.html */
uninstall(): Promise<void>;
/** Check if an integration is currently installed */
isInstalled(): boolean;
}

View File

@@ -0,0 +1,257 @@
/**
* Workbench Patcher — Install/uninstall integration scripts into workbench.html.
*
* Handles the file-level modification of Antigravity's workbench.html
* to include/remove custom script tags.
*
* @module integration/workbench-patcher
*
* @internal
*/
import * as fs from 'fs';
import * as path from 'path';
/** Default prefix for generated files */
const FILE_PREFIX = 'ag-sdk';
/**
* Manages patching/unpatching of Antigravity's workbench.html.
*/
export class WorkbenchPatcher {
private readonly _workbenchDir: string;
private readonly _workbenchHtml: string;
private readonly _scriptPath: string;
private readonly _heartbeatPath: string;
private readonly _slug: string;
private readonly _markerStart: string;
private readonly _markerEnd: string;
/**
* @param namespace - Unique slug for this extension (e.g. 'kanezal-better-antigravity').
* Used to namespace all generated files and HTML markers so multiple
* SDK-based extensions can coexist without conflicts.
*/
constructor(namespace: string = 'default') {
// Resolve Antigravity install path
const appData = process.env.LOCALAPPDATA || '';
this._workbenchDir = path.join(
appData,
'Programs',
'Antigravity',
'resources',
'app',
'out',
'vs',
'code',
'electron-browser',
'workbench',
);
this._workbenchHtml = path.join(this._workbenchDir, 'workbench.html');
this._slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
this._scriptPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}.js`);
this._heartbeatPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}-heartbeat`);
this._markerStart = `<!-- AG SDK [${this._slug}] -->`;
this._markerEnd = `<!-- /AG SDK [${this._slug}] -->`;
}
/**
* Check if workbench.html exists and is accessible.
*/
isAvailable(): boolean {
return fs.existsSync(this._workbenchHtml);
}
/**
* Check if our integration is currently installed.
*/
isInstalled(): boolean {
if (!this.isAvailable()) return false;
try {
const content = fs.readFileSync(this._workbenchHtml, 'utf8');
return content.includes(this._markerStart);
} catch {
return false;
}
}
/**
* Install the integration script.
*
* 1. Writes the script file to the workbench directory
* 2. Patches workbench.html to include a <script> tag
*
* @param scriptContent — The generated JavaScript code
*/
install(scriptContent: string): void {
if (!this.isAvailable()) {
throw new Error(`Workbench not found at: ${this._workbenchDir}`);
}
// First uninstall any previous integration for THIS namespace
if (this.isInstalled()) {
this.uninstall();
}
// Clean up legacy files from previous versions (non-namespaced)
this._cleanupLegacyFiles();
// Write the script file
fs.writeFileSync(this._scriptPath, scriptContent, 'utf8');
// Patch workbench.html
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
// Insert before </html>
const scriptBasename = path.basename(this._scriptPath);
const scriptTag = [
this._markerStart,
`<script src="./${scriptBasename}"></script>`,
this._markerEnd,
].join('\n');
html = html.replace('</html>', `${scriptTag}\n</html>`);
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
// Create empty titles JSON if it doesn't exist (prevents console 404)
const titlesPath = path.join(this._workbenchDir, `ag-sdk-titles-${this._slug}.json`);
if (!fs.existsSync(titlesPath)) {
fs.writeFileSync(titlesPath, '{}', 'utf8');
}
}
/**
* Remove the integration.
*
* 1. Removes the <script> tag from workbench.html
* 2. Deletes the script file
*/
uninstall(): void {
if (!this.isAvailable()) return;
// Remove from workbench.html
try {
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
const regex = new RegExp(
`\\n?${escapeRegex(this._markerStart)}[\\s\\S]*?${escapeRegex(this._markerEnd)}\\n?`,
'g',
);
html = html.replace(regex, '');
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
} catch {
// Ignore errors during cleanup
}
// Remove script file
try {
if (fs.existsSync(this._scriptPath)) {
fs.unlinkSync(this._scriptPath);
}
} catch {
// Ignore
}
}
/**
* Write/refresh the heartbeat marker file.
*
* The generated script checks this file's modification time
* to determine if the extension is still active. If the file
* is missing or stale, the script will not start.
*/
writeHeartbeat(): void {
try {
fs.writeFileSync(this._heartbeatPath, Date.now().toString(), 'utf8');
} catch {
// Ignore — workbench dir may not be writable
}
}
/**
* Remove the heartbeat marker file.
*/
removeHeartbeat(): void {
try {
if (fs.existsSync(this._heartbeatPath)) {
fs.unlinkSync(this._heartbeatPath);
}
} catch {
// Ignore
}
}
/**
* Get the path to the heartbeat file.
*/
getHeartbeatPath(): string {
return this._heartbeatPath;
}
/**
* Get the path to the workbench directory.
*/
getWorkbenchDir(): string {
return this._workbenchDir;
}
/**
* Get the path to the script file.
*/
getScriptPath(): string {
return this._scriptPath;
}
/**
* Clean up legacy files from previous SDK versions.
*
* Removes non-namespaced files (from before namespace support)
* and files with wrong namespace (e.g. 'undefined').
*/
private _cleanupLegacyFiles(): void {
// Legacy file names that may exist from older versions
const legacyFiles = [
'ag-sdk-integrate.js',
'ag-sdk-heartbeat',
'ag-sdk-titles.json',
'ag-sdk-titles-undefined.json',
'ag-sdk-titles-default.json',
];
for (const name of legacyFiles) {
const p = path.join(this._workbenchDir, name);
try {
if (fs.existsSync(p)) fs.unlinkSync(p);
} catch { /* ignore */ }
}
// Remove legacy script tags from workbench.html
try {
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
let changed = false;
// Remove bare <script src="./ag-sdk-integrate.js"></script> lines
const legacyTagRegex = /<script src="\.\/ag-sdk-integrate\.js"><\/script>\n?/g;
if (legacyTagRegex.test(html)) {
html = html.replace(legacyTagRegex, '');
changed = true;
}
// Remove old X-Ray SDK markers with no namespace
const xrayRegex = /<!-- X-Ray SDK Integration -->\n?<script[^>]*ag-sdk-integrate[^>]*><\/script>\n?<!-- \/X-Ray SDK Integration -->\n?/g;
if (xrayRegex.test(html)) {
html = html.replace(xrayRegex, '');
changed = true;
}
if (changed) {
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
}
} catch { /* ignore */ }
}
}
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

View File

@@ -0,0 +1,229 @@
/**
* Main SDK entry point.
*
* Provides a unified interface to Antigravity's agent system
* via verified transport layer (CommandBridge + StateBridge + EventMonitor).
*
* @module AntigravitySDK
*
* @example
* ```typescript
* import { AntigravitySDK } from 'antigravity-sdk';
*
* export function activate(context: vscode.ExtensionContext) {
* const sdk = new AntigravitySDK(context);
* await sdk.initialize();
*
* // List conversations
* const sessions = await sdk.cascade.getSessions();
* console.log(`${sessions.length} conversations`);
*
* // Read preferences (all 16 sentinel values)
* const prefs = await sdk.cascade.getPreferences();
* console.log('Terminal policy:', prefs.terminalExecutionPolicy);
*
* // Monitor for new conversations
* sdk.monitor.onNewConversation(() => {
* console.log('New conversation detected!');
* });
* sdk.monitor.start(3000);
*
* // Clean up
* context.subscriptions.push(sdk);
* }
* ```
*/
import * as vscode from 'vscode';
import { DisposableStore, IDisposable } from './core/disposable';
import { Logger, LogLevel } from './core/logger';
import { AntigravityNotFoundError } from './core/errors';
import { CommandBridge } from './transport/command-bridge';
import { StateBridge } from './transport/state-bridge';
import { EventMonitor } from './transport/event-monitor';
import { LSBridge } from './transport/ls-bridge';
import { CascadeManager } from './cascade/cascade-manager';
import { IntegrationManager } from './integration/integration-manager';
const log = new Logger('SDK');
/**
* SDK initialization options.
*/
export interface ISDKOptions {
/** Enable debug logging */
debug?: boolean;
}
/**
* The main Antigravity SDK class.
*
* Provides access to:
* - `commands` — Execute Antigravity internal commands
* - `state` — Read agent preferences and state from USS
* - `cascade` — Manage Cascade conversations, send messages, read preferences
* - `monitor` — Watch for state changes (new conversations, preference updates)
*
* @example
* ```typescript
* const sdk = new AntigravitySDK(context);
* await sdk.initialize();
* const sessions = await sdk.cascade.getSessions();
* ```
*/
export class AntigravitySDK implements IDisposable {
private readonly _disposables = new DisposableStore();
private _initialized = false;
/** Command bridge for executing Antigravity commands */
public readonly commands: CommandBridge;
/** State bridge for reading USS data */
public readonly state: StateBridge;
/** Cascade manager for conversations, preferences, diagnostics */
public readonly cascade: CascadeManager;
/** Event monitor for watching state changes */
public readonly monitor: EventMonitor;
/** Integration manager for Agent View UI customization */
public readonly integration: IntegrationManager;
/**
* Language Server bridge for headless cascade operations.
* Use this for background cascade creation without UI switching.
*
* @example
* ```typescript
* const id = await sdk.ls.createCascade({ text: 'Analyze coverage' });
* await sdk.ls.sendMessage({ cascadeId: id, text: 'Focus on tests' });
* await sdk.ls.focusCascade(id); // Only when ready to show
* ```
*/
public readonly ls: LSBridge;
/**
* Create a new Antigravity SDK instance.
*
* @param context - VS Code extension context
* @param options - SDK options
*/
constructor(
private readonly _context: vscode.ExtensionContext,
options?: ISDKOptions,
) {
if (options?.debug) {
Logger.setLevel(LogLevel.Debug);
}
// Derive namespace from extension ID for file isolation
// e.g. 'kanezal.better-antigravity' -> 'kanezal-better-antigravity'
const namespace = this._context.extension.id.replace(/\./g, '-');
this.commands = this._disposables.add(new CommandBridge());
this.state = this._disposables.add(new StateBridge());
this.cascade = this._disposables.add(new CascadeManager(this.commands, this.state));
this.monitor = this._disposables.add(new EventMonitor(this.state));
this.integration = this._disposables.add(new IntegrationManager(namespace));
this.ls = new LSBridge(
<T = any>(cmd: string, ...args: any[]) => Promise.resolve(vscode.commands.executeCommand<T>(cmd, ...args))
);
log.info(`SDK created (namespace: ${namespace})`);
}
/**
* Initialize the SDK and verify Antigravity is running.
*
* Call this before using any SDK features.
*
* @throws {AntigravityNotFoundError} If Antigravity is not detected
*/
async initialize(): Promise<void> {
if (this._initialized) {
return;
}
log.info('Initializing SDK...');
// Verify we're running inside Antigravity
const isAntigravity = await this._detectAntigravity();
if (!isAntigravity) {
throw new AntigravityNotFoundError();
}
// Initialize state bridge (opens state.vscdb via sql.js)
await this.state.initialize();
// Initialize cascade manager (loads session list)
await this.cascade.initialize();
// Initialize LS bridge (discovers Language Server port + CSRF token)
const lsOk = await this.ls.initialize();
if (lsOk) {
log.info(`LS bridge ready on port ${this.ls.port} (csrf: ${this.ls.hasCsrfToken ? 'ok' : 'missing'})`);
} else {
log.warn('LS bridge not available — use sdk.ls.setConnection(port, csrfToken) or command fallback');
}
// Refresh integration heartbeat (so renderer script knows extension is active)
this.integration.signalActive();
this._initialized = true;
log.info('SDK initialized successfully');
}
/**
* Check if the SDK has been initialized.
*/
get isInitialized(): boolean {
return this._initialized;
}
/**
* Get the SDK version.
*/
get version(): string {
try {
return require('../package.json').version;
} catch {
return 'unknown';
}
}
/**
* Detect if we're running inside Antigravity IDE.
*/
private async _detectAntigravity(): Promise<boolean> {
try {
// Check for Antigravity-specific commands (VERIFIED naming)
const commands = await this.commands.getAntigravityCommands();
const hasAgentPanel = commands.includes('antigravity.agentPanel.open');
if (hasAgentPanel) {
log.debug(`Detected Antigravity (${commands.length} commands)`);
return true;
}
// Fallback: check env
const appName = vscode.env.appName;
if (appName?.toLowerCase().includes('antigravity')) {
log.debug(`Detected Antigravity via appName: ${appName}`);
return true;
}
return false;
} catch {
return false;
}
}
/**
* Dispose of the SDK and all its resources.
*/
dispose(): void {
log.info('Disposing SDK');
this._disposables.dispose();
}
}

View File

@@ -0,0 +1,342 @@
/**
* Command Bridge — executes Antigravity internal commands via VS Code API.
*
* All commands go through `vscode.commands.executeCommand()` which is the
* safe, official way to interact with Antigravity from extensions.
*
* VERIFIED: All commands listed below were confirmed to exist in
* Antigravity v1.107.0 workbench.desktop.main.js and extension.js
* on 2026-02-28.
*
* @module transport/command-bridge
*/
import * as vscode from 'vscode';
import { IDisposable } from '../core/disposable';
import { CommandExecutionError } from '../core/errors';
import { Logger } from '../core/logger';
const log = new Logger('CommandBridge');
/**
* All known Antigravity commands, organized by category.
*
* Sources: workbench.desktop.main.js (160+ commands) + extension.js (45 commands)
*/
export const AntigravityCommands = {
// ─── Agent Panel & UI (VERIFIED: .open/.focus suffix required) ────────
/** Open the Cascade agent panel */
OPEN_AGENT_PANEL: 'antigravity.agentPanel.open',
/** Focus the Cascade agent panel */
FOCUS_AGENT_PANEL: 'antigravity.agentPanel.focus',
/** Open the agent side panel */
OPEN_AGENT_SIDE_PANEL: 'antigravity.agentSidePanel.open',
/** Focus the agent side panel */
FOCUS_AGENT_SIDE_PANEL: 'antigravity.agentSidePanel.focus',
/** Toggle side panel visibility */
TOGGLE_SIDE_PANEL: 'antigravity.agentSidePanel.toggleVisibility',
/** Open agent (generic) */
OPEN_AGENT: 'antigravity.openAgent',
/** Toggle chat focus */
TOGGLE_CHAT_FOCUS: 'antigravity.toggleChatFocus',
/** Switch between workspace editor and agent view */
SWITCH_WORKSPACE_AGENT: 'antigravity.switchBetweenWorkspaceAndAgent',
// ─── Conversation Management (Critical for SDK) ──────────────────────
/** Start a new conversation */
START_NEW_CONVERSATION: 'antigravity.startNewConversation',
/** Send a prompt to the agent panel */
SEND_PROMPT_TO_AGENT: 'antigravity.sendPromptToAgentPanel',
/** Send text to chat */
SEND_TEXT_TO_CHAT: 'antigravity.sendTextToChat',
/** Send a chat action message */
SEND_CHAT_ACTION: 'antigravity.sendChatActionMessage',
/** Set which conversation is visible */
SET_VISIBLE_CONVERSATION: 'antigravity.setVisibleConversation',
/** Execute a cascade action */
EXECUTE_CASCADE_ACTION: 'antigravity.executeCascadeAction',
/** Broadcast conversation deletion to all windows */
BROADCAST_CONVERSATION_DELETION: 'antigravity.broadcastConversationDeletion',
/** Track that a background conversation was created */
TRACK_BACKGROUND_CONVERSATION: 'antigravity.trackBackgroundConversationCreated',
// ─── Agent Step Control (VERIFIED) ────────────────────────────────────
/** Accept the current agent step */
ACCEPT_AGENT_STEP: 'antigravity.agent.acceptAgentStep',
/** Reject the current agent step */
REJECT_AGENT_STEP: 'antigravity.agent.rejectAgentStep',
/** Accept a pending command */
COMMAND_ACCEPT: 'antigravity.command.accept',
/** Reject a pending command */
COMMAND_REJECT: 'antigravity.command.reject',
/** Accept a terminal command */
TERMINAL_ACCEPT: 'antigravity.terminalCommand.accept',
/** Reject a terminal command */
TERMINAL_REJECT: 'antigravity.terminalCommand.reject',
/** Run a terminal command */
TERMINAL_RUN: 'antigravity.terminalCommand.run',
/** Open new conversation (prioritized) */
OPEN_NEW_CONVERSATION: 'antigravity.prioritized.chat.openNewConversation',
// ─── Terminal Integration ─────────────────────────────────────────────
/** Notify terminal command started */
TERMINAL_COMMAND_START: 'antigravity.onManagerTerminalCommandStart',
/** Notify terminal command data */
TERMINAL_COMMAND_DATA: 'antigravity.onManagerTerminalCommandData',
/** Notify terminal command finished */
TERMINAL_COMMAND_FINISH: 'antigravity.onManagerTerminalCommandFinish',
/** Update last terminal command */
UPDATE_TERMINAL_LAST_COMMAND: 'antigravity.updateTerminalLastCommand',
/** Notify shell command completion */
ON_SHELL_COMPLETION: 'antigravity.onShellCommandCompletion',
/** Show managed terminal */
SHOW_MANAGED_TERMINAL: 'antigravity.showManagedTerminal',
/** Send terminal output to chat */
SEND_TERMINAL_TO_CHAT: 'antigravity.sendTerminalToChat',
/** Send terminal output to side panel */
SEND_TERMINAL_TO_SIDE_PANEL: 'antigravity.sendTerminalToSidePanel',
// ─── Agent & Mode ─────────────────────────────────────────────────────
/** Initialize the agent */
INITIALIZE_AGENT: 'antigravity.initializeAgent',
// ─── Conversation Picker & Workspace ──────────────────────────────────
/** Open conversation workspace picker */
OPEN_CONVERSATION_PICKER: 'antigravity.openConversationWorkspaceQuickPick',
/** Open conversation picker (alternative) */
OPEN_CONV_PICKER_ALT: 'antigravity.openConversationPicker',
/** Set working directories */
SET_WORKING_DIRS: 'antigravity.setWorkingDirectories',
// ─── Review & Diff ────────────────────────────────────────────────────
/** Open review changes view */
OPEN_REVIEW_CHANGES: 'antigravity.openReviewChanges',
/** Open diff view */
OPEN_DIFF_VIEW: 'antigravity.openDiffView',
/** Open diff zones */
OPEN_DIFF_ZONES: 'antigravity.openDiffZones',
/** Close all diff zones */
CLOSE_ALL_DIFF_ZONES: 'antigravity.closeAllDiffZones',
// ─── Rules & Workflows ────────────────────────────────────────────────
/** Create a new rule */
CREATE_RULE: 'antigravity.createRule',
/** Create a new workflow */
CREATE_WORKFLOW: 'antigravity.createWorkflow',
/** Create a global workflow */
CREATE_GLOBAL_WORKFLOW: 'antigravity.createGlobalWorkflow',
/** Open global rules */
OPEN_GLOBAL_RULES: 'antigravity.openGlobalRules',
/** Open workspace rules */
OPEN_WORKSPACE_RULES: 'antigravity.openWorkspaceRules',
// ─── Plugins & MCP ────────────────────────────────────────────────────
/** Open configure plugins page */
OPEN_CONFIGURE_PLUGINS: 'antigravity.openConfigurePluginsPage',
/** Get Cascade plugin template */
GET_PLUGIN_TEMPLATE: 'antigravity.getCascadePluginTemplate',
/** Poll MCP server states */
POLL_MCP_SERVERS: 'antigravity.pollMcpServerStates',
/** Open MCP config file */
OPEN_MCP_CONFIG: 'antigravity.openMcpConfigFile',
/** Open MCP docs page */
OPEN_MCP_DOCS: 'antigravity.openMcpDocsPage',
/** Update plugin installation count */
UPDATE_PLUGIN_COUNT: 'antigravity.updatePluginInstallationCount',
// ─── Autocomplete ─────────────────────────────────────────────────────
/** Enable autocomplete */
ENABLE_AUTOCOMPLETE: 'antigravity.enableAutocomplete',
/** Disable autocomplete */
DISABLE_AUTOCOMPLETE: 'antigravity.disableAutocomplete',
/** Accept completion */
ACCEPT_COMPLETION: 'antigravity.acceptCompletion',
/** Force supercomplete */
FORCE_SUPERCOMPLETE: 'antigravity.forceSupercomplete',
/** Snooze autocomplete temporarily */
SNOOZE_AUTOCOMPLETE: 'antigravity.snoozeAutocomplete',
/** Cancel snooze */
CANCEL_SNOOZE: 'antigravity.cancelSnoozeAutocomplete',
// ─── Auth & Account ───────────────────────────────────────────────────
/** Login to Antigravity */
LOGIN: 'antigravity.login',
/** Cancel login */
CANCEL_LOGIN: 'antigravity.cancelLogin',
/** Handle auth refresh */
HANDLE_AUTH_REFRESH: 'antigravity.handleAuthRefresh',
/** Sign in to Antigravity */
SIGN_IN: 'antigravity.SignInToAntigravity',
// ─── Diagnostics & Debug ──────────────────────────────────────────────
/** Get diagnostics info */
GET_DIAGNOSTICS: 'antigravity.getDiagnostics',
/** Download diagnostics bundle */
DOWNLOAD_DIAGNOSTICS: 'antigravity.downloadDiagnostics',
/** Capture traces */
CAPTURE_TRACES: 'antigravity.captureTraces',
/** Enable tracing */
ENABLE_TRACING: 'antigravity.enableTracing',
/** Clear and disable tracing */
CLEAR_TRACING: 'antigravity.clearAndDisableTracing',
/** Get manager trace */
GET_MANAGER_TRACE: 'antigravity.getManagerTrace',
/** Get workbench trace */
GET_WORKBENCH_TRACE: 'antigravity.getWorkbenchTrace',
/** Toggle debug info widget */
TOGGLE_DEBUG_INFO: 'antigravity.toggleDebugInfoWidget',
/** Open troubleshooting */
OPEN_TROUBLESHOOTING: 'antigravity.openTroubleshooting',
/** Open issue reporter */
OPEN_ISSUE_REPORTER: 'antigravity.openIssueReporter',
// ─── Language Server ──────────────────────────────────────────────────
/** Restart the language server */
RESTART_LANGUAGE_SERVER: 'antigravity.restartLanguageServer',
/** Kill language server and reload window */
KILL_LS_AND_RELOAD: 'antigravity.killLanguageServerAndReloadWindow',
// ─── Git & Commit ─────────────────────────────────────────────────────
/** Generate commit message via AI */
GENERATE_COMMIT_MESSAGE: 'antigravity.generateCommitMessage',
/** Cancel commit message generation */
CANCEL_COMMIT_MESSAGE: 'antigravity.cancelGenerateCommitMessage',
// ─── Browser ──────────────────────────────────────────────────────────
/** Open browser */
OPEN_BROWSER: 'antigravity.openBrowser',
/** Get browser onboarding port (returns number, e.g. 57401) */
GET_BROWSER_PORT: 'antigravity.getBrowserOnboardingPort',
// ─── Settings & Import ────────────────────────────────────────────────
/** Open quick settings panel */
OPEN_QUICK_SETTINGS: 'antigravity.openQuickSettingsPanel',
/** Open customizations tab */
OPEN_CUSTOMIZATIONS: 'antigravity.openCustomizationsTab',
/** Import VS Code settings */
IMPORT_VSCODE_SETTINGS: 'antigravity.importVSCodeSettings',
/** Import VS Code extensions */
IMPORT_VSCODE_EXTENSIONS: 'antigravity.importVSCodeExtensions',
/** Import Cursor settings */
IMPORT_CURSOR_SETTINGS: 'antigravity.importCursorSettings',
/** Import Cursor extensions */
IMPORT_CURSOR_EXTENSIONS: 'antigravity.importCursorExtensions',
// ─── Misc ─────────────────────────────────────────────────────────────
/** Reload window */
RELOAD_WINDOW: 'antigravity.reloadWindow',
/** Open documentation */
OPEN_DOCS: 'antigravity.openDocs',
/** Open changelog */
OPEN_CHANGELOG: 'antigravity.openChangeLog',
/** Explain and fix problem (from diagnostics) */
EXPLAIN_AND_FIX: 'antigravity.explainAndFixProblem',
/** Open a URL */
OPEN_URL: 'antigravity.openGenericUrl',
/** Editor mode settings */
EDITOR_MODE_SETTINGS: 'antigravity.editorModeSettings',
} as const;
/**
* Bridges between the SDK and Antigravity's command system.
*
* All interactions with Antigravity go through registered VS Code commands,
* ensuring we never bypass the official extension API.
*
* @example
* ```typescript
* const bridge = new CommandBridge();
*
* // Open the agent panel
* await bridge.execute(AntigravityCommands.OPEN_AGENT_PANEL);
*
* // Start a new conversation
* await bridge.execute(AntigravityCommands.START_NEW_CONVERSATION);
*
* // Send a prompt
* await bridge.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, 'Hello!');
* ```
*/
export class CommandBridge implements IDisposable {
private _disposed = false;
/**
* Execute an Antigravity command.
*
* @param command - The command ID to execute
* @param args - Arguments to pass to the command
* @returns The command's return value
* @throws {CommandExecutionError} If the command fails
*/
async execute<T = unknown>(command: string, ...args: unknown[]): Promise<T> {
if (this._disposed) {
throw new CommandExecutionError(command, 'CommandBridge has been disposed');
}
log.debug(`Executing: ${command}`, args.length > 0 ? args : '');
try {
const result = await vscode.commands.executeCommand<T>(command, ...args);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error(`Command failed: ${command}`, message);
throw new CommandExecutionError(command, message);
}
}
/**
* Check if a command is registered and available.
*
* @param command - Command ID to check
* @returns true if the command exists
*/
async isAvailable(command: string): Promise<boolean> {
const commands = await vscode.commands.getCommands(true);
return commands.includes(command);
}
/**
* Get all registered Antigravity commands.
*
* @returns List of command IDs starting with 'antigravity.'
*/
async getAntigravityCommands(): Promise<string[]> {
const commands = await vscode.commands.getCommands(true);
return commands.filter((cmd) => cmd.startsWith('antigravity.'));
}
/**
* Register a command handler.
*
* @param command - Command ID to register
* @param handler - Function to handle the command
* @returns Disposable to unregister the command
*/
register(command: string, handler: (...args: unknown[]) => unknown): IDisposable {
return vscode.commands.registerCommand(command, handler);
}
dispose(): void {
this._disposed = true;
}
}

View File

@@ -0,0 +1,307 @@
/**
* Event Monitor — polls state.vscdb and getDiagnostics for changes.
*
* Detects:
* - USS key changes (trajectory summaries, preferences, etc.)
* - Step count changes per session (via getDiagnostics.recentTrajectories)
* - Active session switches
* - New conversations
*
* @module transport/event-monitor
*/
import * as vscode from 'vscode';
import { IDisposable, DisposableStore } from '../core/disposable';
import { EventEmitter, Event } from '../core/events';
import { Logger } from '../core/logger';
import { StateBridge, USSKeys } from './state-bridge';
const log = new Logger('EventMonitor');
/**
* USS key change event.
*/
export interface IStateChange {
/** Which USS key changed */
readonly key: string;
/** New data size */
readonly newSize: number;
/** Previous data size */
readonly previousSize: number;
}
/**
* Step count change event — fired when the agent adds/processes steps.
*/
export interface IStepCountChange {
/** Conversation UUID (googleAgentId) */
readonly sessionId: string;
/** Conversation title */
readonly title: string;
/** Previous step count */
readonly previousCount: number;
/** New step count */
readonly newCount: number;
/** Number of new steps added */
readonly delta: number;
}
/**
* Active session change event.
*/
export interface IActiveSessionChange {
/** New active session ID */
readonly sessionId: string;
/** New active session title */
readonly title: string;
/** Previous active session ID (empty if first detection) */
readonly previousSessionId: string;
}
/**
* Snapshot of a trajectory from getDiagnostics.
*/
interface ITrajectorySnapshot {
id: string;
title: string;
stepCount: number;
lastModified: string;
}
/**
* Monitors Antigravity state for changes.
*
* Two polling modes:
* 1. **USS polling** — watches state.vscdb keys for size changes (lightweight)
* 2. **Trajectory polling** — watches getDiagnostics for step count changes (heavier, optional)
*
* @example
* ```typescript
* const monitor = new EventMonitor(stateBridge);
*
* // React to step changes (agent is working)
* monitor.onStepCountChanged((e) => {
* console.log(`${e.title}: +${e.delta} steps (now ${e.newCount})`);
* });
*
* // React to conversation switches
* monitor.onActiveSessionChanged((e) => {
* console.log(`Switched to: ${e.title}`);
* });
*
* monitor.start(3000);
* ```
*/
export class EventMonitor implements IDisposable {
private readonly _disposables = new DisposableStore();
private _ussTimer: ReturnType<typeof setInterval> | null = null;
private _trajTimer: ReturnType<typeof setInterval> | null = null;
private _ussSnapshots = new Map<string, number>();
private _trajSnapshots = new Map<string, ITrajectorySnapshot>();
private _activeSessionId = '';
private _running = false;
// ─── USS Events ─────────────────────────────────────────────────────
private readonly _onStateChanged = this._disposables.add(new EventEmitter<IStateChange>());
/** Fires when any monitored USS key changes size */
public readonly onStateChanged: Event<IStateChange> = this._onStateChanged.event;
private readonly _onNewConversation = this._disposables.add(new EventEmitter<void>());
/** Fires when trajectory summaries grow (new conversation likely) */
public readonly onNewConversation: Event<void> = this._onNewConversation.event;
// ─── Trajectory Events ──────────────────────────────────────────────
private readonly _onStepCountChanged = this._disposables.add(new EventEmitter<IStepCountChange>());
/** Fires when a session's step count changes (agent made progress) */
public readonly onStepCountChanged: Event<IStepCountChange> = this._onStepCountChanged.event;
private readonly _onActiveSessionChanged = this._disposables.add(new EventEmitter<IActiveSessionChange>());
/** Fires when the active (most recent) session changes */
public readonly onActiveSessionChanged: Event<IActiveSessionChange> = this._onActiveSessionChanged.event;
/** Keys we monitor for USS changes */
private readonly _watchedKeys = [
USSKeys.TRAJECTORY_SUMMARIES,
USSKeys.AGENT_PREFERENCES,
USSKeys.USER_STATUS,
];
constructor(private readonly _state: StateBridge) { }
/**
* Start polling for state changes.
*
* @param intervalMs - USS polling interval (default: 3000ms)
* @param trajectoryIntervalMs - Trajectory polling interval (default: 5000ms).
* Set to 0 to disable trajectory polling (saves CPU).
*/
start(intervalMs: number = 3000, trajectoryIntervalMs: number = 5000): void {
if (this._running) return;
this._running = true;
log.info(`Starting event monitor (USS: ${intervalMs}ms, Traj: ${trajectoryIntervalMs}ms)`);
// Initial USS snapshot
this._takeUSSSnapshot().catch(() => { });
// USS polling
this._ussTimer = setInterval(async () => {
try {
await this._pollUSS();
} catch (error) {
log.error('USS poll error', error);
}
}, intervalMs);
// Trajectory polling (optional, heavier)
if (trajectoryIntervalMs > 0) {
this._pollTrajectories().catch(() => { });
this._trajTimer = setInterval(async () => {
try {
await this._pollTrajectories();
} catch (error) {
log.error('Trajectory poll error', error);
}
}, trajectoryIntervalMs);
}
}
/**
* Stop polling.
*/
stop(): void {
if (this._ussTimer) {
clearInterval(this._ussTimer);
this._ussTimer = null;
}
if (this._trajTimer) {
clearInterval(this._trajTimer);
this._trajTimer = null;
}
this._running = false;
log.info('Event monitor stopped');
}
/** Check if the monitor is currently running. */
get isRunning(): boolean {
return this._running;
}
/** Get the currently active session ID. */
get activeSessionId(): string {
return this._activeSessionId;
}
// ─── USS Polling ────────────────────────────────────────────────────
private async _takeUSSSnapshot(): Promise<void> {
for (const key of this._watchedKeys) {
try {
const value = await this._state.getRawValue(key);
this._ussSnapshots.set(key, value ? value.length : 0);
} catch {
this._ussSnapshots.set(key, 0);
}
}
}
private async _pollUSS(): Promise<void> {
for (const key of this._watchedKeys) {
try {
const value = await this._state.getRawValue(key);
const newSize = value ? value.length : 0;
const previousSize = this._ussSnapshots.get(key) ?? 0;
if (newSize !== previousSize) {
log.debug(`USS change: ${key} (${previousSize} -> ${newSize})`);
this._ussSnapshots.set(key, newSize);
this._onStateChanged.fire({ key, newSize, previousSize });
if (key === USSKeys.TRAJECTORY_SUMMARIES && newSize > previousSize) {
this._onNewConversation.fire();
}
}
} catch {
// Skip errors during polling
}
}
}
// ─── Trajectory Polling ─────────────────────────────────────────────
private async _pollTrajectories(): Promise<void> {
let trajectories: Array<{
googleAgentId: string;
trajectoryId: string;
summary: string;
lastStepIndex: number;
lastModifiedTime: string;
}>;
try {
const raw = await vscode.commands.executeCommand<string>('antigravity.getDiagnostics');
if (!raw || typeof raw !== 'string') return;
const diag = JSON.parse(raw);
if (!Array.isArray(diag.recentTrajectories)) return;
trajectories = diag.recentTrajectories;
} catch {
return;
}
// Check for step count changes in each trajectory
for (const traj of trajectories) {
const id = traj.googleAgentId;
if (!id) continue;
const prev = this._trajSnapshots.get(id);
const newCount = traj.lastStepIndex ?? 0;
if (prev && prev.stepCount !== newCount) {
const delta = newCount - prev.stepCount;
log.debug(`Step change: "${traj.summary}" ${prev.stepCount} -> ${newCount} (+${delta})`);
this._onStepCountChanged.fire({
sessionId: id,
title: traj.summary ?? 'Untitled',
previousCount: prev.stepCount,
newCount,
delta,
});
}
this._trajSnapshots.set(id, {
id,
title: traj.summary ?? 'Untitled',
stepCount: newCount,
lastModified: traj.lastModifiedTime ?? '',
});
}
// Check for active session change (first entry = most recent)
if (trajectories.length > 0) {
const newActiveId = trajectories[0].googleAgentId;
if (newActiveId && newActiveId !== this._activeSessionId) {
const previousId = this._activeSessionId;
this._activeSessionId = newActiveId;
// Only fire event after initial snapshot (not on first detection)
if (previousId !== '') {
log.debug(`Active session changed: "${trajectories[0].summary}"`);
this._onActiveSessionChanged.fire({
sessionId: newActiveId,
title: trajectories[0].summary ?? 'Untitled',
previousSessionId: previousId,
});
}
}
}
}
dispose(): void {
this.stop();
this._disposables.dispose();
}
}

View File

@@ -0,0 +1,9 @@
/**
* Transport module re-exports.
* @module transport
*/
export { CommandBridge, AntigravityCommands } from './command-bridge';
export { StateBridge, USSKeys } from './state-bridge';
export { EventMonitor, type IStateChange } from './event-monitor';
export { LSBridge, Models, type ModelId, type IHeadlessCascadeOptions, type ISendMessageOptions } from './ls-bridge';

View File

@@ -0,0 +1,725 @@
/**
* Language Server Bridge — Direct ConnectRPC calls to the local LS.
*
* UPDATED 2026-03-01 (v1.3.0):
* Fixed CSRF token authentication (Issue #1).
* The LS binary is launched with --csrf_token as a CLI argument.
* Previous versions did not send this token, causing 401 "missing CSRF token".
*
* Discovery strategy (multi-layer):
* 1. Process CLI args — extract --port and --csrf_token from LS process
* 2. getDiagnostics console logs — fallback for port discovery
* 3. Manual override — setConnection(port, csrfToken)
*
* Service: exa.language_server_pb.LanguageServerService
* Protocol: HTTPS POST with JSON body + x-csrf-token header
*
* @module transport/ls-bridge
*/
import { Logger } from '../core/logger';
const log = new Logger('LSBridge');
/** Known model IDs (verified 2026-02-28) */
export const Models = {
GEMINI_FLASH: 1018,
GEMINI_PRO_LOW: 1164,
GEMINI_PRO_HIGH: 1165,
CLAUDE_SONNET: 1163,
CLAUDE_OPUS: 1154,
GPT_OSS: 342,
} as const;
export type ModelId = typeof Models[keyof typeof Models] | number;
/** Options for creating a headless cascade */
export interface IHeadlessCascadeOptions {
/** Text prompt to send */
text: string;
/** Model ID (default: Gemini 3 Flash = 1018) */
model?: ModelId;
/** Planner type: 'conversational' (default) or 'normal' */
plannerType?: 'conversational' | 'normal';
}
/** Options for sending a message to existing cascade */
export interface ISendMessageOptions {
/** Target cascade ID */
cascadeId: string;
/** Text to send */
text: string;
/** Model ID (default: Gemini 3 Flash = 1018) */
model?: ModelId;
}
/**
* Conversation annotation fields (from jetski_cortex.proto ConversationAnnotations).
*
* These are metadata annotations on a conversation that the user can set.
* The LS stores these natively and they persist across sessions.
*/
export interface IConversationAnnotations {
/** Custom user title -- overrides the auto-generated summary */
title?: string;
/** Tags/labels for organization */
tags?: string[];
/** Whether this conversation is archived */
archived?: boolean;
/** Whether this conversation is starred (pinned) */
starred?: boolean;
}
/**
* Direct bridge to the Language Server via ConnectRPC.
*
* Discovers the LS port and CSRF token from the LS process CLI args,
* then makes authenticated HTTPS POST calls to the LS endpoints.
*
* @example
* ```typescript
* const ls = new LSBridge(commandBridge);
* await ls.initialize();
*
* // Create a headless cascade
* const cascadeId = await ls.createCascade({
* text: 'Analyze test coverage',
* model: Models.GEMINI_FLASH,
* });
*
* // Send follow-up
* await ls.sendMessage({ cascadeId, text: 'Focus on edge cases' });
*
* // Switch UI to it
* await ls.focusCascade(cascadeId);
* ```
*/
export class LSBridge {
private _port: number | null = null;
private _csrfToken: string | null = null;
private _useTls: boolean = false;
private _executeCommand: <T = any>(command: string, ...args: any[]) => Promise<T>;
constructor(executeCommand: <T = any>(command: string, ...args: any[]) => Promise<T>) {
this._executeCommand = executeCommand;
}
/**
* Discover the Language Server port and CSRF token.
* Must be called before other methods.
*
* Discovery chain:
* 1. Parse LS process CLI arguments (--port, --csrf_token)
* 2. Fallback: getDiagnostics console logs (port only)
* 3. Manual: call setConnection() after initialize() returns false
*/
async initialize(): Promise<boolean> {
// Strategy 1: discover from LS process CLI args (port + CSRF)
const fromProcess = await this._discoverFromProcess();
if (fromProcess) {
this._port = fromProcess.port;
this._csrfToken = fromProcess.csrfToken;
this._useTls = fromProcess.useTls;
log.info(`LS discovered from process: port=${this._port}, tls=${this._useTls}, csrf=${this._csrfToken ? 'found' : 'missing'}`);
return true;
}
// Strategy 2: fallback to getDiagnostics logs (port only, no CSRF)
this._port = await this._discoverPortFromDiagnostics();
if (this._port) {
log.warn(`LS port from diagnostics: ${this._port}, but CSRF token not found — RPC calls may fail with 401`);
return true;
}
log.warn('Could not discover LS connection. Use setConnection(port, csrfToken) manually.');
return false;
}
/** Whether the bridge is ready (port discovered) */
get isReady(): boolean {
return this._port !== null;
}
/** The discovered LS port */
get port(): number | null {
return this._port;
}
/** Whether CSRF token is available */
get hasCsrfToken(): boolean {
return this._csrfToken !== null;
}
/**
* Manually set the LS connection parameters.
*
* Use this when auto-discovery fails (e.g., non-standard install,
* or you've discovered the port/token through other means like `lsof`).
*
* @param port - LS port number
* @param csrfToken - CSRF token from LS process CLI args
* @param useTls - Whether to use HTTPS (default: false, extension_server uses HTTP)
*
* @example
* ```typescript
* const ls = new LSBridge(commandBridge);
* const ok = await ls.initialize();
* if (!ok) {
* // Manual fallback: get port and csrf from your own discovery
* ls.setConnection(54321, 'abc123-csrf-token');
* }
* ```
*/
setConnection(port: number, csrfToken: string, useTls: boolean = false): void {
this._port = port;
this._csrfToken = csrfToken;
this._useTls = useTls;
log.info(`LS connection set manually: port=${port}, tls=${useTls}, csrf=${csrfToken ? 'provided' : 'empty'}`);
}
// ─── Headless Cascade API ────────────────────────────────────────
/**
* Create a new cascade and optionally send a message.
* Fully headless — no UI panel opened, no conversation switched.
*
* @returns cascadeId or null on failure
*/
async createCascade(options: IHeadlessCascadeOptions): Promise<string | null> {
this._ensureReady();
// Step 1: StartCascade
const startResp = await this._rpc('StartCascade', { source: 0 });
const cascadeId = startResp?.cascadeId;
if (!cascadeId) {
log.error('StartCascade returned no cascadeId');
return null;
}
log.info(`Cascade created: ${cascadeId}`);
// Step 2: SendUserCascadeMessage
if (options.text) {
await this._sendMessage(cascadeId, options.text, options.model, options.plannerType);
log.info(`Message sent to: ${cascadeId}`);
}
return cascadeId;
}
/**
* Send a message to an existing cascade.
*
* @returns true if sent successfully
*/
async sendMessage(options: ISendMessageOptions): Promise<boolean> {
this._ensureReady();
await this._sendMessage(options.cascadeId, options.text, options.model);
return true;
}
/**
* Switch the UI to show a specific cascade conversation.
*/
async focusCascade(cascadeId: string): Promise<void> {
this._ensureReady();
await this._rpc('SmartFocusConversation', { cascadeId });
}
/**
* Cancel a running cascade invocation.
*/
async cancelCascade(cascadeId: string): Promise<void> {
this._ensureReady();
await this._rpc('CancelCascadeInvocation', { cascadeId });
}
// ─── Conversation Annotations API ───────────────────────────────
/**
* Native conversation annotations (verified from jetski_cortex.proto).
*
* ConversationAnnotations protobuf fields:
* - title (string) — custom user title, overrides auto-summary
* - tags (string[]) — tags/labels
* - archived (bool) — archive status
* - starred (bool) — pinned/starred
* - last_user_view_time (Timestamp)
*
* @param cascadeId - Conversation ID
* @param annotations - Partial annotation fields to set
* @param merge - If true, merge with existing annotations (default: true)
*/
async updateAnnotations(
cascadeId: string,
annotations: IConversationAnnotations,
merge: boolean = true,
): Promise<void> {
this._ensureReady();
// Convert camelCase to snake_case for protobuf
const proto: Record<string, any> = {};
if (annotations.title !== undefined) proto.title = annotations.title;
if (annotations.starred !== undefined) proto.starred = annotations.starred;
if (annotations.archived !== undefined) proto.archived = annotations.archived;
if (annotations.tags !== undefined) proto.tags = annotations.tags;
await this._rpc('UpdateConversationAnnotations', {
cascadeId,
annotations: proto,
mergeAnnotations: merge,
});
log.info(`Annotations updated for ${cascadeId.substring(0, 8)}...`);
}
/**
* Set a custom title for a conversation.
*
* This sets the `title` field in ConversationAnnotations.
* When set, this title should be displayed instead of the
* auto-generated `summary` from the LLM.
*
* @param cascadeId - Conversation ID
* @param title - Custom title to set
*/
async setTitle(cascadeId: string, title: string): Promise<void> {
await this.updateAnnotations(cascadeId, { title });
}
/**
* Star (pin) or unstar a conversation.
*
* This sets the `starred` field in ConversationAnnotations.
*
* @param cascadeId - Conversation ID
* @param starred - true to star, false to unstar
*/
async setStar(cascadeId: string, starred: boolean): Promise<void> {
await this.updateAnnotations(cascadeId, { starred });
}
// ─── Conversation Read API ──────────────────────────────────────
/**
* Get details of a specific conversation.
*/
async getConversation(cascadeId: string): Promise<any> {
this._ensureReady();
return this._rpc('GetConversation', { cascadeId });
}
/**
* Get all cascade trajectories (conversation list).
*/
async listCascades(): Promise<any> {
this._ensureReady();
const resp = await this._rpc('GetAllCascadeTrajectories', {});
return resp?.trajectorySummaries ?? {};
}
/**
* Get trajectory descriptions (lighter than full trajectories).
* Returns { trajectories: [...] }.
*/
async getTrajectoryDescriptions(): Promise<any> {
this._ensureReady();
return this._rpc('GetUserTrajectoryDescriptions', {});
}
/**
* Get user status (tier, models, etc.)
*/
async getUserStatus(): Promise<any> {
this._ensureReady();
return this._rpc('GetUserStatus', {});
}
/**
* Make a raw RPC call to any LS method.
* @param method - RPC method name (e.g. 'StartCascade')
* @param payload - JSON payload
*/
async rawRPC(method: string, payload: any): Promise<any> {
this._ensureReady();
return this._rpc(method, payload);
}
// ─── Internal ────────────────────────────────────────────────────
private _ensureReady(): void {
if (!this._port) {
throw new Error('LSBridge not initialized. Call initialize() first.');
}
}
private async _sendMessage(
cascadeId: string,
text: string,
model?: ModelId,
plannerType?: string,
): Promise<void> {
const payload: any = {
cascadeId,
items: [{ chunk: { case: 'text', value: text } }],
cascadeConfig: {
plannerConfig: {
plannerTypeConfig: {
case: plannerType || 'conversational',
value: {},
},
requestedModel: {
choice: { case: 'model', value: model || Models.GEMINI_FLASH },
},
},
},
};
await this._rpc('SendUserCascadeMessage', payload);
}
/**
* Discover LS port and CSRF token from the Language Server process.
*
* VERIFIED 2026-03-01 from Antigravity extension.js source:
*
* 1. CSRF header is "x-codeium-csrf-token" (NOT x-csrf-token)
* 2. CSRF value is --csrf_token from CLI (NOT --extension_server_csrf_token)
* 3. ConnectRPC endpoint is on httpsPort (HTTPS) or httpPort (HTTP)
* These ports are NOT in CLI args (--random_port flag means random).
* We discover them via netstat/PID, excluding extension_server_port.
*
* Source code proof:
* n.header.set("x-codeium-csrf-token", e) // header name
* address = `127.0.0.1:${te.httpsPort}` // ConnectRPC address
* csrfToken = a = d.randomUUID() → --csrf_token // token source
* t.headers["x-codeium-csrf-token"] === this.csrfToken ? ... : 403
*
* Discovery: 2 phases
* Phase 1: Get-CimInstance/ps → PID, --csrf_token, --extension_server_port
* Phase 2: netstat → find LISTENING ports for PID, exclude ext_server_port
*/
private async _discoverFromProcess(): Promise<{ port: number; csrfToken: string; useTls: boolean } | null> {
try {
const platform = process.platform;
// Phase 1: find LS process, extract PID, csrf_token, extension_server_port
let processInfo = await this._findLSProcess(platform);
if (!processInfo) {
log.debug('No LS processes found');
return null;
}
log.debug(`LS process found: PID=${processInfo.pid}, csrf=present, ext_port=${processInfo.extPort}`);
// Phase 2: find actual ConnectRPC port via netstat
const connectPort = await this._findConnectPort(platform, processInfo.pid, processInfo.extPort);
if (!connectPort) {
log.debug('Could not find ConnectRPC port via netstat, trying extension_server_port as fallback');
// Fallback: try extension_server_port with HTTP
if (processInfo.extPort) {
return { port: processInfo.extPort, csrfToken: processInfo.csrfToken, useTls: false };
}
return null;
}
return {
port: connectPort.port,
csrfToken: processInfo.csrfToken,
useTls: connectPort.tls,
};
} catch (err) {
log.debug('Process discovery failed', err);
}
return null;
}
/**
* Phase 1: Find the LS process for this workspace.
*/
private async _findLSProcess(
platform: string,
): Promise<{ pid: number; csrfToken: string; extPort: number } | null> {
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
let output: string;
if (platform === 'win32') {
// Use -EncodedCommand to avoid all PowerShell escaping issues with $_ and quotes
const psScript = "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'language_server' -and $_.CommandLine -match 'csrf_token' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }";
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
const result = await execAsync(
`powershell.exe -NoProfile -EncodedCommand ${encoded}`,
{ encoding: 'utf8', timeout: 10000, windowsHide: true },
);
output = result.stdout;
} else {
const result = await execAsync(
'ps -eo pid,args 2>/dev/null | grep language_server | grep csrf_token | grep -v grep',
{ encoding: 'utf8', timeout: 5000 },
);
output = result.stdout;
}
const lines = output.split('\n').filter((l: string) => l.trim().length > 0);
if (lines.length === 0) return null;
const workspaceHint = this._getWorkspaceHint();
let bestLine: string | null = null;
if (workspaceHint) {
for (const line of lines) {
if (line.includes(workspaceHint)) {
bestLine = line;
break;
}
}
}
if (!bestLine) bestLine = lines[0];
// Extract PID (first field before | on Windows, first token on Unix)
let pid: number;
if (platform === 'win32') {
pid = parseInt(bestLine.split('|')[0].trim(), 10);
} else {
pid = parseInt(bestLine.trim().split(/\s+/)[0], 10);
}
const csrfToken = this._extractArg(bestLine, 'csrf_token');
const extPortStr = this._extractArg(bestLine, 'extension_server_port');
const extPort = extPortStr ? parseInt(extPortStr, 10) : 0;
if (!csrfToken || isNaN(pid)) return null;
return { pid, csrfToken, extPort };
}
/**
* Phase 2: Find ConnectRPC port via netstat.
*
* The LS process listens on multiple ports:
* - httpsPort (HTTPS, ConnectRPC) ← this is what we want
* - httpPort (HTTP, ConnectRPC) ← also works
* - lspPort (LSP JSON-RPC)
* - extension_server_port is separate (for Extension Host IPC)
*
* We find all LISTENING ports for the LS PID, exclude ext_server_port,
* then try HTTPS first (preferred), fall back to HTTP.
*/
private async _findConnectPort(
platform: string,
pid: number,
extPort: number,
): Promise<{ port: number; tls: boolean } | null> {
try {
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
let output: string;
if (platform === 'win32') {
const result = await execAsync(
`netstat -aon | findstr "LISTENING" | findstr "${pid}"`,
{ encoding: 'utf8', timeout: 5000, windowsHide: true },
);
output = result.stdout;
} else {
const result = await execAsync(
`ss -tlnp 2>/dev/null | grep "pid=${pid}" || netstat -tlnp 2>/dev/null | grep "${pid}"`,
{ encoding: 'utf8', timeout: 5000 },
);
output = result.stdout;
}
// Extract all listening ports for this PID
const portMatches = output.matchAll(/127\.0\.0\.1:(\d+)/g);
const ports: number[] = [];
for (const m of portMatches) {
const p = parseInt(m[1], 10);
// Exclude extension_server_port
if (p !== extPort && !ports.includes(p)) {
ports.push(p);
}
}
if (ports.length === 0) return null;
log.debug(`LS ports (excl ext ${extPort}): ${ports.join(', ')}`);
// Try to identify httpsPort vs httpPort by probing
// Strategy: try HTTPS first on each port (httpsPort is preferred)
for (const port of ports) {
const tls = await this._probePort(port, true);
if (tls) return { port, tls: true };
}
// Fallback: try HTTP
for (const port of ports) {
const http = await this._probePort(port, false);
if (http) return { port, tls: false };
}
} catch (err) {
log.debug('netstat port discovery failed', err);
}
return null;
}
/**
* Quick probe: check if a port accepts ConnectRPC requests.
* Returns true if the port responds (even with error) on the given protocol.
*/
private _probePort(port: number, useTls: boolean): Promise<boolean> {
const mod = useTls ? require('https') : require('http');
const proto = useTls ? 'https' : 'http';
return new Promise((resolve) => {
const req = mod.request(`${proto}://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/GetUserStatus`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': 2 },
rejectUnauthorized: false,
timeout: 2000,
}, (res: any) => {
// 401 = correct endpoint, just missing CSRF (expected)
// 200 = also correct (unlikely without CSRF but possible)
resolve(res.statusCode === 401 || res.statusCode === 200);
});
req.on('error', () => resolve(false));
req.on('timeout', () => { req.destroy(); resolve(false); });
req.write('{}');
req.end();
});
}
/**
* Get a workspace hint string used to match the correct LS process.
*
* The LS process has --workspace_id like:
* file_d_3A_programming_better_antigravity
* which is an encoded version of the workspace URI.
*/
private _getWorkspaceHint(): string {
try {
const vscode = require('vscode');
const folders = vscode.workspace?.workspaceFolders;
if (folders && folders.length > 0) {
// Convert workspace path to LS workspace_id format
// e.g., "d:\programming\better-antigravity" -> "better_antigravity"
// (LS uses underscored path segments)
const folder = folders[0].uri.fsPath;
const parts = folder.replace(/\\/g, '/').split('/');
// Use last 2-3 segments for matching
return parts.slice(-2).join('_').replace(/[-.\s]/g, '_').toLowerCase();
}
} catch {
// vscode not available (e.g., testing)
}
return '';
}
/**
* Extract a CLI argument value from a command-line string.
* Supports both --key=value and --key value formats.
*/
private _extractArg(cmdLine: string, argName: string): string | null {
// --argName=value
const eqMatch = cmdLine.match(new RegExp(`--${argName}=([^\\s"]+)`));
if (eqMatch) return eqMatch[1];
// --argName value
const spaceMatch = cmdLine.match(new RegExp(`--${argName}\\s+([^\\s"]+)`));
if (spaceMatch) return spaceMatch[1];
return null;
}
/**
* Fallback: discover port from getDiagnostics console logs.
* NOTE: This does NOT discover the CSRF token.
* In recent Antigravity versions, the port URL may no longer appear in logs.
*/
private async _discoverPortFromDiagnostics(): Promise<number | null> {
try {
const raw = await this._executeCommand<string>('antigravity.getDiagnostics');
if (!raw || typeof raw !== 'string') return null;
const diag = JSON.parse(raw);
const logs: string = diag.agentWindowConsoleLogs || '';
// Pattern: 127.0.0.1:{port}/exa.language_server_pb
const m1 = logs.match(/127\.0\.0\.1:(\d+)\/exa\.language_server_pb/);
if (m1) return parseInt(m1[1], 10);
// Fallback: any 127.0.0.1:{port} in HTTPS context
const m2 = logs.match(/https?:\/\/127\.0\.0\.1:(\d+)/);
if (m2) return parseInt(m2[1], 10);
// Check mainThreadLogs for port info
if (diag.mainThreadLogs) {
const mainLogs = typeof diag.mainThreadLogs === 'string'
? diag.mainThreadLogs
: JSON.stringify(diag.mainThreadLogs);
const m3 = mainLogs.match(/127\.0\.0\.1:(\d+)/);
if (m3) return parseInt(m3[1], 10);
}
} catch (err) {
log.error('Failed to discover LS port from diagnostics', err);
}
return null;
}
/**
* Make an authenticated RPC call to the Language Server.
* Sends x-csrf-token header when available.
*
* VERIFIED 2026-03-01:
* - extension_server_port uses plain HTTP (no TLS)
* - Main LS port (--random_port) uses HTTPS with self-signed cert
*/
private async _rpc(method: string, payload: any): Promise<any> {
const httpModule = this._useTls ? require('https') : require('http');
const protocol = this._useTls ? 'https' : 'http';
const url = `${protocol}://127.0.0.1:${this._port}/exa.language_server_pb.LanguageServerService/${method}`;
return new Promise((resolve, reject) => {
const body = JSON.stringify(payload);
const headers: Record<string, string | number> = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
};
// CSRF header: "x-codeium-csrf-token" (verified from extension.js source)
if (this._csrfToken) {
headers['x-codeium-csrf-token'] = this._csrfToken;
}
const reqOptions: any = {
method: 'POST',
headers,
};
// Self-signed TLS when using HTTPS
if (this._useTls) {
reqOptions.rejectUnauthorized = false;
}
const req = httpModule.request(url, reqOptions, (res: any) => {
let data = '';
res.on('data', (chunk: string) => { data += chunk; });
res.on('end', () => {
if (res.statusCode === 200) {
try { resolve(JSON.parse(data)); }
catch { resolve(data); }
} else {
const hint = res.statusCode === 401
? ' (CSRF token may be invalid or missing -- try setConnection() with the correct token)'
: '';
reject(new Error(`LS ${method}: ${res.statusCode} -- ${data.substring(0, 200)}${hint}`));
}
});
});
req.on('error', (err: Error) => reject(err));
req.write(body);
req.end();
});
}
}

View File

@@ -0,0 +1,518 @@
/**
* State Bridge — reads Antigravity's USS state from the SQLite database.
*
* Antigravity stores settings, conversation metadata, and agent preferences
* in `state.vscdb` (SQLite). This bridge provides read-only access to that data.
*
* VERIFIED against live state.vscdb on 2026-02-28.
*
* @module transport/state-bridge
*/
import * as path from 'path';
import * as fs from 'fs';
import { IDisposable } from '../core/disposable';
import { StateReadError } from '../core/errors';
import { Logger } from '../core/logger';
import type {
IAgentPreferences,
TerminalExecutionPolicy,
ArtifactReviewPolicy,
} from '../core/types';
const log = new Logger('StateBridge');
/**
* USS (Unified State Sync) keys in state.vscdb.
*
* VERIFIED: All keys listed below were confirmed to exist
* in a live Antigravity v1.107.0 installation on 2026-02-28.
* Values are Base64-encoded protobuf unless noted otherwise.
*/
export const USSKeys = {
/** Agent preferences — terminal policy, review policy, secure mode, etc. (1020 bytes) */
AGENT_PREFERENCES: 'antigravityUnifiedStateSync.agentPreferences',
/** Conversation/trajectory summaries — titles, timestamps, workspace URIs (74KB+) */
TRAJECTORY_SUMMARIES: 'antigravityUnifiedStateSync.trajectorySummaries',
/** Agent manager window state (192 bytes) */
AGENT_MANAGER_WINDOW: 'antigravityUnifiedStateSync.agentManagerWindow',
/** Enterprise override store (56 bytes) */
OVERRIDE_STORE: 'antigravityUnifiedStateSync.overrideStore',
/** Model preferences — selected model, sentinel key */
MODEL_PREFERENCES: 'antigravityUnifiedStateSync.modelPreferences',
/** Artifact review state (1204 bytes) */
ARTIFACT_REVIEW: 'antigravityUnifiedStateSync.artifactReview',
/** Browser preferences (380 bytes) */
BROWSER_PREFERENCES: 'antigravityUnifiedStateSync.browserPreferences',
/** Editor preferences (108 bytes) */
EDITOR_PREFERENCES: 'antigravityUnifiedStateSync.editorPreferences',
/** Tab preferences (404 bytes) */
TAB_PREFERENCES: 'antigravityUnifiedStateSync.tabPreferences',
/** Window preferences (44 bytes) */
WINDOW_PREFERENCES: 'antigravityUnifiedStateSync.windowPreferences',
/** Scratch/playground workspaces (268 bytes) */
SCRATCH_WORKSPACES: 'antigravityUnifiedStateSync.scratchWorkspaces',
/** Sidebar workspaces — recent workspace list (5604 bytes) */
SIDEBAR_WORKSPACES: 'antigravityUnifiedStateSync.sidebarWorkspaces',
/** User status info (5196 bytes) */
USER_STATUS: 'antigravityUnifiedStateSync.userStatus',
/** Model credits/usage info */
MODEL_CREDITS: 'antigravityUnifiedStateSync.modelCredits',
/** Onboarding state (140 bytes) */
ONBOARDING: 'antigravityUnifiedStateSync.onboarding',
/** Seen NUX (new user experience) IDs (76 bytes) */
SEEN_NUX_IDS: 'antigravityUnifiedStateSync.seenNuxIds',
// ⚠️ Jetski-specific state (separate sync namespace)
/** Agent manager initialization state — contains auth tokens, workspace map (5144 bytes) */
AGENT_MANAGER_INIT: 'jetskiStateSync.agentManagerInitState',
// ⚠️ Non-USS but relevant keys
/** All user settings — JSON format */
ALL_USER_SETTINGS: 'antigravityUserSettings.allUserSettings',
/** Allowed model configs for commands */
ALLOWED_COMMAND_MODEL_CONFIGS: 'antigravity_allowed_command_model_configs',
/** Chat session store index (JSON: {"version":1,"entries":{}}) */
CHAT_SESSION_INDEX: 'chat.ChatSessionStore.index',
} as const;
/**
* Keys that contain sensitive data and MUST NOT be exposed through the SDK.
*
* VERIFIED 2026-02-28:
* - oauthToken: OAuth access token (732 bytes)
* - agentManagerInitState: Contains LIVE ya29.* access token + g1//* refresh token!
* - antigravityAuthStatus: Auth status
*/
const SENSITIVE_KEYS = new Set([
'antigravityUnifiedStateSync.oauthToken',
'jetskiStateSync.agentManagerInitState',
'antigravityAuthStatus',
]);
/**
* Protobuf sentinel keys found in agentPreferences.
*
* ALL 16 sentinel keys verified from live state.vscdb on 2026-02-28.
* Each sentinel key string is followed by a small Base64 value encoding
* a protobuf varint (the actual preference value).
*/
const SENTINEL_KEYS = {
PLANNING_MODE: 'planningModeSentinelKey',
ARTIFACT_REVIEW_POLICY: 'artifactReviewPolicySentinelKey',
TERMINAL_AUTO_EXECUTION_POLICY: 'terminalAutoExecutionPolicySentinelKey',
TERMINAL_ALLOWED_COMMANDS: 'terminalAllowedCommandsSentinelKey',
TERMINAL_DENIED_COMMANDS: 'terminalDeniedCommandsSentinelKey',
ALLOW_NON_WORKSPACE_FILES: 'allowAgentAccessNonWorkspaceFilesSentinelKey',
ALLOW_GITIGNORE_ACCESS: 'allowCascadeAccessGitignoreFilesSentinelKey',
SECURE_MODE: 'secureModeSentinelKey',
EXPLAIN_FIX_IN_CONVO: 'explainAndFixInCurrentConversationSentinelKey',
AUTO_CONTINUE_ON_MAX: 'autoContinueOnMaxGeneratorInvocationsSentinelKey',
DISABLE_AUTO_OPEN_EDITED: 'disableAutoOpenEditedFilesSentinelKey',
ENABLE_SOUNDS: 'enableSoundsForSpecialEventsSentinelKey',
DISABLE_AUTO_FIX_LINTS: 'disableCascadeAutoFixLintsSentinelKey',
ENABLE_SHELL_INTEGRATION: 'enableShellIntegrationSentinelKey',
SANDBOX_ALLOW_NETWORK: 'sandboxAllowNetworkSentinelKey',
ENABLE_TERMINAL_SANDBOX: 'enableTerminalSandboxSentinelKey',
} as const;
/**
* Reads Antigravity's internal state from the SQLite database.
*
* Uses **sql.js** (pure JavaScript SQLite, compiled to WASM) which is
* verified to work in Antigravity's Extension Host (unlike better-sqlite3
* which fails due to ABI mismatch with Electron v22.21.1 / ABI v140).
*
* @example
* ```typescript
* const bridge = new StateBridge();
* await bridge.initialize();
*
* const prefs = await bridge.getAgentPreferences();
* console.log(prefs.terminalExecutionPolicy);
* ```
*/
export class StateBridge implements IDisposable {
private _dbPath: string | null = null;
private _db: any = null; // sql.js Database instance
private _disposed = false;
/**
* Initialize the state bridge by locating and opening state database.
*
* @throws {StateReadError} If the database cannot be found
*/
async initialize(): Promise<void> {
const dbPath = this._findStateDb();
if (!dbPath) {
throw new StateReadError('state.vscdb', 'Could not locate Antigravity state database');
}
this._dbPath = dbPath;
// Open with sql.js (pure JS — verified working in Extension Host)
try {
const path = require('path');
const fs = require('fs');
// Try to load sql.js from multiple locations:
// 1. Adjacent sql-wasm.js (for VSIX bundles where consumer copies it to dist/)
// 2. Standard require('sql.js') (for npm install / dev setups)
let initSqlJs: any;
const localSqlJs = path.join(__dirname, 'sql-wasm.js');
if (fs.existsSync(localSqlJs)) {
initSqlJs = require(localSqlJs);
} else {
initSqlJs = require('sql.js');
}
// Auto-locate sql-wasm.wasm — try multiple paths so devs
// don't need to manually copy anything after `npm install`
const candidates = [
// 1. Adjacent to this file (if wasm was bundled/copied to dist/)
path.join(__dirname, 'sql-wasm.wasm'),
// 2. sql.js package dist/ (standard npm install)
path.resolve(__dirname, '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
// 3. Hoisted node_modules (monorepo / npm workspaces)
path.resolve(__dirname, '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
// 4. Walk up to find it (deep hoisting)
path.resolve(__dirname, '..', '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
];
// Try require.resolve — works in all layouts
try {
const sqlJsMain = require.resolve('sql.js');
candidates.unshift(path.join(path.dirname(sqlJsMain), 'sql-wasm.wasm'));
} catch {
// sql.js might not have a resolvable main in all setups
}
let wasmPath: string | null = null;
for (const p of candidates) {
if (fs.existsSync(p)) {
wasmPath = p;
break;
}
}
if (!wasmPath) {
throw new Error('sql-wasm.wasm not found in any expected location');
}
const SQL = await initSqlJs({
locateFile: () => wasmPath!,
});
const fileBuffer = fs.readFileSync(dbPath);
this._db = new SQL.Database(fileBuffer);
log.info(`State database opened via sql.js: ${dbPath}`);
} catch (error) {
log.warn('sql.js not available, will use child_process fallback', error);
}
}
/**
* Read a raw value from the state database.
*
* @param key - The SQLite key to read
* @returns The raw string value, or null if not found
* @throws {StateReadError} If the key is sensitive or read fails
*/
async getRawValue(key: string): Promise<string | null> {
if (this._disposed) {
throw new StateReadError(key, 'StateBridge has been disposed');
}
if (!this._dbPath) {
throw new StateReadError(key, 'StateBridge not initialized');
}
// Block access to sensitive keys
if (SENSITIVE_KEYS.has(key)) {
throw new StateReadError(key, 'Access to sensitive keys is blocked by the SDK for security');
}
try {
if (this._db) {
return this._querySqlJs(key);
}
return await this._queryChildProcess(key);
} catch (error) {
if (error instanceof StateReadError) throw error;
const msg = error instanceof Error ? error.message : String(error);
throw new StateReadError(key, msg);
}
}
/**
* Get agent preferences from USS.
*
* @returns Parsed agent preferences
*/
async getAgentPreferences(): Promise<IAgentPreferences> {
const raw = await this.getRawValue(USSKeys.AGENT_PREFERENCES);
if (!raw) {
log.warn('No agent preferences found, returning defaults');
return this._defaultPreferences();
}
try {
return this._parseAgentPreferences(raw);
} catch (error) {
log.error('Failed to parse preferences, returning defaults', error);
return this._defaultPreferences();
}
}
/**
* Get all stored USS keys from the state database.
*
* @returns List of key names related to Antigravity (excludes sensitive keys)
*/
async getAntigravityKeys(): Promise<string[]> {
if (!this._dbPath) {
throw new StateReadError('*', 'StateBridge not initialized');
}
let keys: string[];
if (this._db) {
const result = this._db.exec(
"SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%' OR key LIKE 'chat.%'",
);
keys = result.length > 0 ? result[0].values.map((r: any[]) => r[0] as string) : [];
} else {
const result = await this._queryChildProcess('*');
keys = result ? result.split('\n').map((l: string) => l.trim()).filter(Boolean) : [];
}
// Filter out sensitive keys
return keys.filter((k) => !SENSITIVE_KEYS.has(k));
}
/**
* Query using sql.js (in-process, pure JS).
*/
private _querySqlJs(key: string): string | null {
const stmt = this._db.prepare('SELECT value FROM ItemTable WHERE key = $key');
stmt.bind({ $key: key });
if (stmt.step()) {
const row = stmt.getAsObject();
stmt.free();
return (row.value as string) ?? null;
}
stmt.free();
return null;
}
/**
* Query using child_process sqlite3 CLI (fallback).
*/
private async _queryChildProcess(key: string): Promise<string | null> {
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const sql =
key === '*'
? "SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%'"
: `SELECT value FROM ItemTable WHERE key = '${key.replace(/'/g, "''")}'`;
try {
const { stdout } = await execAsync(`sqlite3 "${this._dbPath}" "${sql}"`, {
encoding: 'utf8',
timeout: 5000,
});
return stdout.trim() || null;
} catch {
return null;
}
}
/**
* Locate the state.vscdb file across platforms.
*/
private _findStateDb(): string | null {
const candidates: string[] = [];
// Windows (VERIFIED: this is the correct path)
const appData = process.env.APPDATA;
if (appData) {
candidates.push(path.join(appData, 'Antigravity', 'User', 'globalStorage', 'state.vscdb'));
}
// macOS
const home = process.env.HOME;
if (home) {
candidates.push(
path.join(
home,
'Library',
'Application Support',
'Antigravity',
'User',
'globalStorage',
'state.vscdb',
),
);
}
// Linux
if (home) {
candidates.push(
path.join(home, '.config', 'Antigravity', 'User', 'globalStorage', 'state.vscdb'),
);
}
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
/**
* Parse agent preferences from Base64(Protobuf).
*
* The protobuf structure uses "sentinel keys" as string fields:
* - `planningModeSentinelKey` → nested message with Base64(varint)
* - `terminalAutoExecutionPolicySentinelKey` → nested message with Base64(varint)
* - `artifactReviewPolicySentinelKey` → nested message with Base64(varint)
*
* Each sentinel value is itself a small Base64 string (e.g., "EAM=" = varint 3 = EAGER).
*/
private _parseAgentPreferences(raw: string): IAgentPreferences {
const buffer = Buffer.from(raw, 'base64');
const text = buffer.toString('utf8');
// Extract all sentinel values
const terminalPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.TERMINAL_AUTO_EXECUTION_POLICY);
const artifactPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.ARTIFACT_REVIEW_POLICY);
const planningMode = this._extractSentinelValue(text, SENTINEL_KEYS.PLANNING_MODE);
const secureMode = this._extractSentinelValue(text, SENTINEL_KEYS.SECURE_MODE);
const terminalSandbox = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_TERMINAL_SANDBOX);
const sandboxNetwork = this._extractSentinelValue(text, SENTINEL_KEYS.SANDBOX_ALLOW_NETWORK);
const shellIntegration = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SHELL_INTEGRATION);
const nonWorkspaceFiles = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_NON_WORKSPACE_FILES);
const gitignoreAccess = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_GITIGNORE_ACCESS);
const explainFix = this._extractSentinelValue(text, SENTINEL_KEYS.EXPLAIN_FIX_IN_CONVO);
const autoContinue = this._extractSentinelValue(text, SENTINEL_KEYS.AUTO_CONTINUE_ON_MAX);
const disableAutoOpen = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_OPEN_EDITED);
const enableSounds = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SOUNDS);
const disableAutoFix = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_FIX_LINTS);
return {
terminalExecutionPolicy: (terminalPolicy ?? 1) as TerminalExecutionPolicy,
artifactReviewPolicy: (artifactPolicy ?? 1) as ArtifactReviewPolicy,
planningMode: planningMode ?? 0,
secureModeEnabled: (secureMode ?? 0) === 1,
terminalSandboxEnabled: (terminalSandbox ?? 0) === 1,
sandboxAllowNetwork: (sandboxNetwork ?? 0) === 1,
shellIntegrationEnabled: (shellIntegration ?? 1) === 1,
allowNonWorkspaceFiles: (nonWorkspaceFiles ?? 0) === 1,
allowGitignoreAccess: (gitignoreAccess ?? 0) === 1,
explainFixInCurrentConvo: (explainFix ?? 0) === 1,
autoContinueOnMax: autoContinue ?? 0,
disableAutoOpenEdited: (disableAutoOpen ?? 0) === 1,
enableSounds: (enableSounds ?? 0) === 1,
disableAutoFixLints: (disableAutoFix ?? 0) === 1,
allowedCommands: [],
deniedCommands: [],
};
}
/**
* Extract a varint value from a protobuf sentinel key.
*
* The structure is: sentinel_key_string followed by a small
* Base64 value like "EAM=" (which decodes to a protobuf varint).
*
* Known mappings:
* - "CAE=" → field 1, value 1 (OFF / ALWAYS)
* - "EAI=" → field 2, value 2 (AUTO / TURBO)
* - "EAM=" → field 2, value 3 (EAGER / AUTO)
*/
private _extractSentinelValue(text: string, sentinelKey: string): number | null {
const idx = text.indexOf(sentinelKey);
if (idx === -1) return null;
// After the sentinel key, look for a small Base64 fragment
const after = text.substring(idx + sentinelKey.length, idx + sentinelKey.length + 30);
// Match a Base64 chunk (typically 4-8 chars ending with =)
const b64Match = after.match(/([A-Za-z0-9+/]{2,8}={0,2})/);
if (!b64Match) return null;
try {
const decoded = Buffer.from(b64Match[1], 'base64');
// Protobuf varint: last byte of the value
// For simple single-byte varints, the value is in the lower 7 bits
if (decoded.length >= 2) {
// The first byte is (field_number << 3 | wire_type)
// The second byte is the actual value
return decoded[1];
} else if (decoded.length === 1) {
return decoded[0];
}
} catch {
// Not valid base64
}
return null;
}
private _defaultPreferences(): IAgentPreferences {
return {
terminalExecutionPolicy: 1 as TerminalExecutionPolicy, // OFF
artifactReviewPolicy: 1 as ArtifactReviewPolicy, // ALWAYS
planningMode: 0,
secureModeEnabled: false,
terminalSandboxEnabled: false,
sandboxAllowNetwork: false,
shellIntegrationEnabled: true,
allowNonWorkspaceFiles: false,
allowGitignoreAccess: false,
explainFixInCurrentConvo: false,
autoContinueOnMax: 0,
disableAutoOpenEdited: false,
enableSounds: false,
disableAutoFixLints: false,
allowedCommands: [],
deniedCommands: [],
};
}
dispose(): void {
this._disposed = true;
if (this._db) {
try {
this._db.close();
} catch {
// Ignore close errors
}
this._db = null;
}
this._dbPath = null;
}
}

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": [
"ES2020"
],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"docs-site"
]
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs'],
dts: true,
sourcemap: true,
clean: true,
external: ['vscode'],
splitting: false,
treeshake: true,
});

127
auth.py Normal file
View File

@@ -0,0 +1,127 @@
"""Authentication module — JWT token management for WebSocket Hub.
Two-stage auth:
1. Extension connects with a registration code (built into .vsix at build time)
2. Hub validates the code and issues a short-lived JWT session token
3. Subsequent reconnections use the JWT token directly
The master secret is stored server-side only (GRAVITY_HUB_SECRET env var).
"""
import hashlib
import hmac
import json
import logging
import os
import time
from base64 import urlsafe_b64decode, urlsafe_b64encode
logger = logging.getLogger(__name__)
# Defaults
DEFAULT_TOKEN_TTL = 86400 # 24 hours
DEFAULT_REGISTRATION_CODE = "" # Set via GRAVITY_REGISTRATION_CODE env var
class TokenManager:
"""Manages JWT-like token creation and verification.
Uses HMAC-SHA256 for signing. Tokens contain:
- project: project name scope
- pc: PC identifier (hostname or custom name)
- iat: issued at (unix timestamp)
- exp: expiration (unix timestamp)
"""
def __init__(self, secret: str = "", registration_code: str = ""):
self.secret = secret or os.getenv("GRAVITY_HUB_SECRET", "")
self.registration_code = registration_code or os.getenv(
"GRAVITY_REGISTRATION_CODE", DEFAULT_REGISTRATION_CODE
)
if not self.secret:
# Auto-generate a secret if not set (ephemeral — tokens invalid after restart)
self.secret = hashlib.sha256(os.urandom(32)).hexdigest()
logger.warning(
"[AUTH] No GRAVITY_HUB_SECRET set — generated ephemeral secret. "
"Tokens will be invalid after server restart."
)
def validate_registration_code(self, code: str) -> bool:
"""Check if the provided registration code matches."""
if not self.registration_code:
# No registration code configured → allow all connections
logger.warning("[AUTH] No registration code configured — accepting all")
return True
return hmac.compare_digest(code, self.registration_code)
def create_token(
self, project: str, pc_name: str, ttl: int = DEFAULT_TOKEN_TTL
) -> str:
"""Create a signed token for a specific project and PC.
Returns a base64-encoded string: {header}.{payload}.{signature}
"""
now = int(time.time())
payload = {
"project": project,
"pc": pc_name,
"iat": now,
"exp": now + ttl,
}
payload_b64 = _b64_encode(json.dumps(payload))
signature = self._sign(payload_b64)
return f"{payload_b64}.{signature}"
def verify_token(self, token: str) -> dict | None:
"""Verify and decode a token.
Returns the payload dict if valid, None if invalid or expired.
"""
try:
parts = token.split(".")
if len(parts) != 2:
return None
payload_b64, signature = parts
expected_sig = self._sign(payload_b64)
if not hmac.compare_digest(signature, expected_sig):
logger.warning("[AUTH] Invalid token signature")
return None
payload = json.loads(_b64_decode(payload_b64))
# Check expiration
if payload.get("exp", 0) < time.time():
logger.info(f"[AUTH] Token expired for {payload.get('pc', '?')}")
return None
return payload
except (json.JSONDecodeError, ValueError, KeyError) as e:
logger.warning(f"[AUTH] Token decode error: {e}")
return None
def _sign(self, data: str) -> str:
"""HMAC-SHA256 sign and return base64."""
sig = hmac.new(
self.secret.encode("utf-8"),
data.encode("utf-8"),
hashlib.sha256,
).digest()
return _b64_encode_bytes(sig)
def _b64_encode(data: str) -> str:
"""URL-safe base64 encode a string, no padding."""
return urlsafe_b64encode(data.encode("utf-8")).rstrip(b"=").decode("ascii")
def _b64_encode_bytes(data: bytes) -> str:
"""URL-safe base64 encode bytes, no padding."""
return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _b64_decode(data: str) -> str:
"""URL-safe base64 decode, handles missing padding."""
padded = data + "=" * (4 - len(data) % 4)
return urlsafe_b64decode(padded).decode("utf-8")

View File

@@ -0,0 +1,2 @@
custom:
- https://github.com/Kanezal/better-antigravity#support

10
better-antigravity-main/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
GEMINI.md
node_modules/
dist/
out/
.env
*.vsix
*.bak
*.log
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,12 @@
.env
publish-ovsx.mjs
.git/
.gitignore
node_modules/
out/
src/
build.mjs
tsconfig.json
*.bak
*.log
*.map

View File

@@ -0,0 +1,105 @@
# Fixes — Technical Details
Detailed root cause analysis and patch descriptions for each fix in Better Antigravity.
---
## Auto-Run Fix
**Status:** Working
**Affected versions:** 1.107.0+
**Files patched:** `workbench.desktop.main.js`, `jetskiAgent.js`
### The Problem
You set **Settings -> Agent -> Terminal Execution -> "Always Proceed"**, but Antigravity **still asks you to click "Run"** on every single terminal command. Every. Single. Time.
The setting saves correctly, Strict Mode is off -- it just doesn't work.
### Root Cause
Found in the source code: the `run_command` step renderer component has an `onChange` handler that auto-confirms commands when you switch the dropdown to "Always run" **on a specific step**. But there's **no `useEffect` hook** that checks the saved policy at mount time and auto-confirms **new steps**.
In other words: the UI reads your setting, displays the correct dropdown value, but never actually acts on it automatically.
```javascript
// What exists (only fires on dropdown CHANGE):
y = Mt(_ => {
setTerminalAutoExecutionPolicy(_),
_ === EAGER && confirm(true) // <- only when you manually switch
}, [])
// What's MISSING (should fire on component mount):
useEffect(() => {
if (policy === EAGER && !secureMode) confirm(true) // <- auto-confirm new steps
}, [])
```
### How the Patch Works
The patcher uses **structural regex matching** to find the `onChange` handler in the minified source. It matches the code by shape, not by variable names -- so it works even when Antigravity re-minifies on update.
**Step 1: Find the onChange handler**
Pattern: `<callback>=<useCallback>((<arg>)=>{<setFn>(<arg>),<arg>===<ENUM>.EAGER&&<confirm>(!0)},[...])`
This matches the handler structurally:
- An assignment to a variable
- A `useCallback` call
- Arrow function with one argument
- Two expressions: set state + check EAGER and confirm
**Step 2: Extract variable names from context**
From the surrounding 3000 characters, extract:
- `policyVar`: `<var>=<something>?.terminalAutoExecutionPolicy??<ENUM>.OFF`
- `secureVar`: `<var>=<something>?.secureModeEnabled??!1`
- `useEffectFn`: the most frequently used short-named function matching the `fn(()=>{...})` pattern (frequency analysis)
**Step 3: Generate and inject the patch**
```javascript
/*BA:autorun*/<useEffect>(()=>{<policyVar>===<ENUM>.EAGER&&!<secureVar>&&<confirm>(!0)},[])
```
The patch is injected immediately after the `onChange` handler's closing bracket.
### Example Output
```
Antigravity "Always Proceed" Auto-Run Fix
C:\Users\user\AppData\Local\Programs\Antigravity
Version: 1.107.0 (IDE 1.19.5)
[workbench] Found onChange at offset 12362782
callback=Mt, enum=Dhe, confirm=b
policyVar=u
secureVar=d
useEffect=mn (confidence: 30 hits)
[workbench] Patched (+43 bytes)
[jetskiAgent] Found onChange at offset 8388797
callback=ve, enum=rx, confirm=F
policyVar=d
secureVar=f
useEffect=At (confidence: 55 hits)
[jetskiAgent] Patched (+42 bytes)
Done! Restart Antigravity.
```
### Safety
- Original files are saved as `.ba-backup` before patching
- The patch marker `/*BA:autorun*/` prevents double-patching
- Only **adds** code, never removes existing logic
- `--revert` restores the original file from backup
- Async I/O in the extension prevents blocking the Extension Host
### Why two files?
The `run_command` step renderer exists in **two** bundles:
1. `workbench.desktop.main.js` -- the main workbench bundle (~15MB)
2. `jetskiAgent.js` -- the Cascade chat panel webview (~10MB)
Both contain the same bug with slightly different minified variable names. The structural matcher handles both transparently.

View File

@@ -0,0 +1,59 @@
# Legal Notice
## Disclaimer
This project is an unofficial, community-maintained collection of patches for
[Antigravity IDE](https://antigravity.dev). It is **not affiliated with,
endorsed by, or sponsored by Google LLC or any of its subsidiaries.**
## Nature of the Project
Better Antigravity provides **bugfix patches** that restore documented, expected
functionality in Antigravity IDE. Specifically:
- The "Always Proceed" terminal execution policy is documented to auto-execute
commands, but does not function as described. Our patch restores this behavior.
- All patches are non-destructive: they create automatic backups and can be
fully reverted at any time.
- No data is collected, transmitted, or shared with any party.
## Compliance
- This project **does not access** Google's backend servers, APIs, or
authentication systems.
- This project **does not extract** AI models, training data, or proprietary
algorithms.
- This project **does not bypass** security features, licensing, or
usage restrictions.
- All modifications are local to the user's machine and affect only the
client-side UI behavior.
## Interoperability
Where applicable, this project relies on the right to achieve interoperability
as provided by:
- **EU Software Directive** (Directive 2009/24/EC), Article 6
- **UK Copyright, Designs and Patents Act 1988**, Section 50B
- Similar provisions in other jurisdictions
## User Responsibility
Users are responsible for ensuring their use of this software complies with
applicable terms of service and local laws. By using this software, you
acknowledge that:
1. You are applying modifications to software on your own machine at your
own risk.
2. Backups of original files are created automatically and can be restored.
3. This project may stop working after Antigravity updates — in that case,
revert and wait for an updated patch.
## Takedown
If Google or the Antigravity team requests removal of this project, we will
comply promptly. Contact: [open a GitHub issue](https://github.com/Kanezal/better-antigravity/issues).
## License
This project is released under the [GNU Affero General Public License v3.0](LICENSE).

View File

@@ -0,0 +1,644 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they receive
widespread use, become available for other developers to incorporate.
Many developers of free software are heartened and encouraged by the
resulting cooperation. However, in the case of software used on network
servers, this result may fail to come about. The GNU General Public
License permits making a modified version and letting the public access
it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding Source
of the work are being offered to the general public at no charge under
subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied by
the Installation Information. But this requirement does not apply if
neither you nor any third party retains the ability to install modified
object code on the User Product (for example, the work has been
installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in source
code form), and must require no special password or key for unpacking,
reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR
DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR
A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Better Antigravity
Copyright (C) 2026 Kanezal
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,209 @@
<div align="center">
# Better Antigravity
**Community-driven fixes and improvements for [Antigravity IDE](https://antigravity.dev)**
[![Open VSX](https://img.shields.io/open-vsx/v/kanezal/better-antigravity)](https://open-vsx.org/extension/kanezal/better-antigravity)
[![npm](https://img.shields.io/npm/v/better-antigravity)](https://www.npmjs.com/package/better-antigravity)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
[![Antigravity](https://img.shields.io/badge/Antigravity-v1.107.0+-blue.svg)](https://antigravity.dev)
[![Sponsor](https://img.shields.io/badge/Sponsor-Support%20this%20project-ff69b4?logo=githubsponsors&logoColor=white)](https://github.com/Kanezal/better-antigravity#support)
*Antigravity is great. We just make it a little better.*
</div>
---
## What is this?
Better Antigravity is both a **VS Code extension** and an **npm CLI tool** that fixes known bugs and adds quality-of-life features to Antigravity IDE.
| Channel | What it does | Install |
|---------|-------------|---------|
| **Extension** | Auto-applies fixes on startup + chat rename + SDK features | [Open VSX](https://open-vsx.org/extension/kanezal/better-antigravity) |
| **CLI** | Quick one-off patching via `npx` (no extension install needed) | `npx better-antigravity auto-run` |
> [!NOTE]
> The extension includes everything the CLI does, plus extra features powered by the [Antigravity SDK](https://www.npmjs.com/package/antigravity-sdk). If you install the extension, you don't need the CLI.
---
## Install (Extension)
Search for **"Better Antigravity"** in the Extensions panel, or install from [Open VSX](https://open-vsx.org/extension/kanezal/better-antigravity).
Manual install:
```bash
antigravity --install-extension better-antigravity-0.5.0.vsix --force
```
On activation the extension will:
1. **Auto-apply the auto-run fix** (silent, no prompt)
2. **Initialize the SDK** for chat rename and future features
3. **Install the integration script** (prompts for reload on first install, auto-reloads on updates)
4. **Suppress integrity warnings** ("corrupt installation" notification silenced automatically)
---
## Install (CLI only)
If you just want the auto-run fix without installing an extension:
```bash
npx better-antigravity auto-run # apply fix
npx better-antigravity auto-run --check # check status
npx better-antigravity auto-run --revert # revert to original
```
Custom install path (if Antigravity is not in the default location):
```bash
npx better-antigravity auto-run --path "D:\Antigravity"
```
---
## Features
### Auto-Run Fix
**The problem:** You set **Settings -> Agent -> Terminal Execution -> "Always Proceed"**, but Antigravity **still asks you to click "Run"** on every terminal command.
**Root cause:** The `run_command` step renderer has an `onChange` handler that auto-confirms when you switch the dropdown, but there's **no `useEffect`** that checks the saved policy at mount time.
```javascript
// What exists (only fires on dropdown CHANGE):
onChange = useCallback(_ => {
setPolicy(_), _ === EAGER && confirm(true)
}, [])
// What's MISSING (should fire on mount):
useEffect(() => {
if (policy === EAGER && !secureMode) confirm(true)
}, [])
```
**The fix:** Our patcher adds the missing `useEffect`. It uses **structural regex matching** (not hardcoded variable names) so it works across Antigravity versions.
> For the full root cause analysis, pattern matching explanation, and example output, see **[FIXES.md](FIXES.md)**.
### Chat Rename (Extension only)
Rename conversations to custom titles via the [Antigravity SDK](https://www.npmjs.com/package/antigravity-sdk) title proxy. Custom titles override the auto-generated summaries in the sidebar.
### Integrity Check Suppression (Extension only)
When the SDK patches workbench.html, Antigravity shows a sticky "Your installation appears to be corrupt" warning with no dismiss button. As of v0.4.0, the extension automatically updates the checksum in `product.json` after patching so IntegrityService sees `isPure = true`. No warnings on next restart.
Multiple SDK-based extensions are coordinated automatically -- the original checksum is restored only when the last extension uninstalls.
### Status Command (Extension only)
`Ctrl+Shift+P` -> **"Better Antigravity: Show Status"** to see:
- SDK initialization state
- Language Server connection
- Integration script status
- Auto-run fix status per file
---
## Commands
| Command | Description |
|---------|-------------|
| `Better Antigravity: Show Status` | Show extension and fix status |
| `Better Antigravity: Revert Auto-Run Fix` | Restore original files from backup |
---
## Safety
- **Automatic backups** -- original files saved as `.ba-backup` before patching
- **One-command revert** -- CLI `--revert` or extension command
- **Non-destructive** -- patches only add code, never remove existing logic
- **Version-resilient** -- structural regex matching, not hardcoded variable names
- **Async I/O** -- file operations don't block the extension host
---
## Compatibility
| Antigravity Version | Status |
|---------------------|--------|
| 1.107.0 | Tested |
| Other versions | Should work (dynamic pattern matching) |
---
## Project Structure
```
better-antigravity/
├── src/
│ ├── extension.ts # Extension entry point (thin orchestrator)
│ ├── auto-run.ts # Auto-run fix logic (async, no vscode dependency)
│ └── commands.ts # VS Code command handlers
├── fixes/
│ └── auto-run-fix/
│ └── patch.js # Standalone CLI patcher
├── cli.js # npx entry point
├── build.mjs # esbuild config
├── publish-ovsx.mjs # Open VSX publish script
└── package.json # Dual: npm package + VS Code extension
```
---
## Development
```bash
npm install
npm run build # Compile extension
npm run watch # Watch mode
npm run package # Build VSIX -> out/
npm run publish:ovsx # Publish to Open VSX (reads .env)
```
The extension depends on [antigravity-sdk](https://www.npmjs.com/package/antigravity-sdk) from the monorepo sibling directory. The build script aliases it automatically.
---
## Contributing
Found another Antigravity bug? Have a fix? PRs are welcome.
### Adding a new fix:
1. Create a folder under `fixes/` with a descriptive name
2. Include a `patch.js` that supports `--check` and `--revert` flags
3. Use structural pattern matching, not hardcoded variable names
4. Update this README's feature table
---
## Disclaimer
> [!WARNING]
> This project is not affiliated with Google or the Antigravity team. These are community patches and improvements. If Antigravity updates and the patches break, simply revert and re-apply (or wait for an updated patch).
**Always report bugs officially** at [antigravity.google/support](https://antigravity.google/support) -- community patches are temporary solutions, not replacements for official fixes.
---
## ❤️ Support
If you find this project useful and want to support its development, you can send **USDT** to:
| Network | Address |
|---------|---------|
| **TON** | `UQCjVh3C3mZc44GjT2IDsS4pmeOoUgRNxWMcb85NS5Bz_v1d` |
| **TRON (TRC20)** | `TH3JKGjNrSDCsjkkSuneaSMZoJYF7CNTXD` |
---
## License
[AGPL-3.0-or-later](LICENSE)

View File

@@ -0,0 +1,57 @@
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as path from 'path';
const isWatch = process.argv.includes('--watch');
/** @type {esbuild.BuildOptions} */
const config = {
entryPoints: ['src/extension.ts'],
bundle: true,
outfile: 'dist/extension.js',
external: ['vscode'],
format: 'cjs',
platform: 'node',
target: 'es2020',
sourcemap: true,
minify: false,
// Resolve antigravity-sdk from monorepo sibling
alias: {
'antigravity-sdk': path.resolve('..', 'antigravity-sdk', 'dist', 'index.js'),
},
};
// Ensure dist/ exists
if (!fs.existsSync('dist')) fs.mkdirSync('dist');
// Copy sql-wasm.wasm AND sql-wasm.js to dist/ (required by antigravity-sdk's StateBridge)
const sqlFiles = ['sql-wasm.wasm', 'sql-wasm.js'];
for (const sqlFile of sqlFiles) {
const searchPaths = [
path.join('node_modules', 'sql.js', 'dist', sqlFile),
path.join('..', 'antigravity-sdk', 'node_modules', 'sql.js', 'dist', sqlFile),
];
let copied = false;
for (const src of searchPaths) {
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join('dist', sqlFile));
console.log(`Copied ${sqlFile} from ${src}`);
copied = true;
break;
}
}
if (!copied) {
console.error(`ERROR: ${sqlFile} not found. Run "npm install" first.`);
process.exit(1);
}
}
if (isWatch) {
const ctx = await esbuild.context(config);
await ctx.watch();
console.log('Watching...');
} else {
await esbuild.build(config);
console.log('Build complete');
}

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* better-antigravity CLI
* Usage:
* npx better-antigravity — list available fixes
* npx better-antigravity auto-run — apply auto-run fix
* npx better-antigravity auto-run --check — check status
* npx better-antigravity auto-run --revert — revert fix
*/
const path = require('path');
const fs = require('fs');
const fixes = {
'auto-run': {
script: path.join(__dirname, 'fixes', 'auto-run-fix', 'patch.js'),
description: '"Always Proceed" terminal policy doesn\'t auto-execute commands'
}
};
const args = process.argv.slice(2);
const fixName = args[0];
const flags = args.slice(1);
// Header
console.log('');
console.log(' better-antigravity — community fixes for Antigravity IDE');
console.log(' https://github.com/Kanezal/better-antigravity');
console.log('');
if (!fixName || fixName === '--help' || fixName === '-h') {
console.log(' Available fixes:');
console.log('');
for (const [name, fix] of Object.entries(fixes)) {
console.log(` ${name.padEnd(15)} ${fix.description}`);
}
console.log('');
console.log(' Usage:');
console.log(' npx better-antigravity <fix-name> Apply fix');
console.log(' npx better-antigravity <fix-name> --check Check status');
console.log(' npx better-antigravity <fix-name> --revert Revert fix');
console.log(' npx better-antigravity <fix-name> --path <dir> Use custom install path');
console.log('');
console.log(' The tool auto-detects Antigravity in: CWD, PATH, Registry, default locations.');
console.log(' Use --path if auto-detection fails (e.g. custom install on another drive).');
console.log('');
process.exit(0);
}
const fix = fixes[fixName];
if (!fix) {
console.log(` Unknown fix: "${fixName}"`);
console.log(` Available: ${Object.keys(fixes).join(', ')}`);
process.exit(1);
}
if (!fs.existsSync(fix.script)) {
console.log(` Fix script not found: ${fix.script}`);
process.exit(1);
}
// Forward to the fix script with flags
process.argv = [process.argv[0], fix.script, ...flags];
require(fix.script);

View File

@@ -0,0 +1,400 @@
#!/usr/bin/env node
/**
* Antigravity "Always Proceed" Auto-Run Fix
* ==========================================
*
* Fixes a bug where the "Always Proceed" terminal execution policy doesn't
* actually auto-execute commands. Uses regex patterns to find code structures
* regardless of minified variable names — works across versions.
*
* Usage:
* node patch.js - Apply patch
* node patch.js --revert - Restore original files
* node patch.js --check - Check patch status
*
* License: MIT
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
// ─── Installation Detection ─────────────────────────────────────────────────
/**
* Validates that a candidate directory is a real Antigravity installation
* by checking for the workbench main JS file.
*/
function isAntigravityDir(dir) {
if (!dir) return false;
try {
const workbench = path.join(dir, 'resources', 'app', 'out', 'vs', 'workbench', 'workbench.desktop.main.js');
return fs.existsSync(workbench);
} catch { return false; }
}
/**
* Checks if a directory looks like the Antigravity installation root
* (contains Antigravity.exe or antigravity binary).
*/
function looksLikeAntigravityRoot(dir) {
if (!dir) return false;
try {
const exe = process.platform === 'win32' ? 'Antigravity.exe' : 'antigravity';
return fs.existsSync(path.join(dir, exe));
} catch { return false; }
}
/**
* Tries to find Antigravity installation path from Windows Registry.
* InnoSetup writes uninstall info to HKCU or HKLM.
*/
function findFromRegistry() {
if (process.platform !== 'win32') return null;
try {
const { execSync } = require('child_process');
// InnoSetup typically writes to this key; try HKCU first, then HKLM
const regPaths = [
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1',
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1',
'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1',
];
for (const regPath of regPaths) {
try {
const output = execSync(
`reg query "${regPath}" /v InstallLocation`,
{ encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }
);
const match = output.match(/InstallLocation\s+REG_SZ\s+(.+)/i);
if (match) {
const dir = match[1].trim().replace(/\\$/, '');
if (isAntigravityDir(dir)) return dir;
}
} catch { /* key not found, try next */ }
}
} catch { /* child_process failed */ }
return null;
}
/**
* Tries to find Antigravity by looking at PATH entries for the executable.
*/
function findFromPath() {
try {
const pathDirs = (process.env.PATH || '').split(path.delimiter);
const exe = process.platform === 'win32' ? 'Antigravity.exe' : 'antigravity';
for (const dir of pathDirs) {
if (!dir) continue;
if (fs.existsSync(path.join(dir, exe))) {
// The exe could be in the root or in a bin/ subdirectory
if (isAntigravityDir(dir)) return dir;
const parent = path.dirname(dir);
if (isAntigravityDir(parent)) return parent;
}
}
} catch { /* PATH parsing failed */ }
return null;
}
function findAntigravityPath() {
// 1. Check CWD and its ancestors (user may run from install dir or a subdir)
let dir = process.cwd();
const root = path.parse(dir).root;
while (dir && dir !== root) {
if (looksLikeAntigravityRoot(dir) && isAntigravityDir(dir)) return dir;
dir = path.dirname(dir);
}
// 2. Check PATH
const fromPath = findFromPath();
if (fromPath) return fromPath;
// 3. Check Windows Registry (InnoSetup uninstall keys)
const fromReg = findFromRegistry();
if (fromReg) return fromReg;
// 4. Hardcoded well-known locations
const candidates = [];
if (process.platform === 'win32') {
candidates.push(
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Antigravity'),
path.join(process.env.PROGRAMFILES || '', 'Antigravity'),
);
} else if (process.platform === 'darwin') {
candidates.push(
'/Applications/Antigravity.app/Contents/Resources',
path.join(os.homedir(), 'Applications', 'Antigravity.app', 'Contents', 'Resources')
);
} else {
candidates.push('/usr/share/antigravity', '/opt/antigravity',
path.join(os.homedir(), '.local', 'share', 'antigravity'));
}
for (const c of candidates) {
if (isAntigravityDir(c)) return c;
}
return null;
}
// ─── Smart Pattern Matching ─────────────────────────────────────────────────
/**
* Finds the onChange handler for terminalAutoExecutionPolicy and extracts
* variable names from context, regardless of minification.
*
* Pattern we're looking for (structure, not exact names):
* <VAR_CONFIRM>=<useCallback>((<ARG>)=>{
* <stepHandler>?.setTerminalAutoExecutionPolicy?.(<ARG>),
* <ARG>===<ENUM>.EAGER&&<CONFIRM_FN>(!0)
* },[...])
*
* From the surrounding context we also extract:
* <POLICY_VAR> = <stepHandler>?.terminalAutoExecutionPolicy ?? <ENUM>.OFF
* <SECURE_VAR> = <stepHandler>?.secureModeEnabled ?? !1
*/
function analyzeFile(content, label) {
// 1. Find the onChange handler: contains setTerminalAutoExecutionPolicy AND .EAGER
// Pattern: VARNAME=CALLBACK(ARG=>{...setTerminalAutoExecutionPolicy...,ARG===ENUM.EAGER&&CONFIRM(!0)},[...])
const onChangeRe = /(\w+)=(\w+)\((\w+)=>\{\w+\?\.setTerminalAutoExecutionPolicy\?\.\(\3\),\3===(\w+)\.EAGER&&(\w+)\(!0\)\},\[[\w,]*\]\)/;
const onChangeMatch = content.match(onChangeRe);
if (!onChangeMatch) {
console.log(` ❌ [${label}] Could not find onChange handler pattern`);
return null;
}
const [fullMatch, assignVar, callbackAlias, argName, enumAlias, confirmFn] = onChangeMatch;
const matchIndex = content.indexOf(fullMatch);
console.log(` 📋 [${label}] Found onChange at offset ${matchIndex}`);
console.log(` callback=${callbackAlias}, enum=${enumAlias}, confirm=${confirmFn}`);
// 2. Find policy variable: VARNAME=HANDLER?.terminalAutoExecutionPolicy??ENUM.OFF
const policyRe = new RegExp(`(\\w+)=\\w+\\?\\.terminalAutoExecutionPolicy\\?\\?${enumAlias}\\.OFF`);
const policyMatch = content.substring(Math.max(0, matchIndex - 2000), matchIndex).match(policyRe);
if (!policyMatch) {
console.log(` ❌ [${label}] Could not find policy variable`);
return null;
}
const policyVar = policyMatch[1];
console.log(` policyVar=${policyVar}`);
// 3. Find secureMode variable: VARNAME=HANDLER?.secureModeEnabled??!1
const secureRe = /(\w+)=\w+\?\.secureModeEnabled\?\?!1/;
const secureMatch = content.substring(Math.max(0, matchIndex - 2000), matchIndex).match(secureRe);
if (!secureMatch) {
console.log(` ❌ [${label}] Could not find secureMode variable`);
return null;
}
const secureVar = secureMatch[1];
console.log(` secureVar=${secureVar}`);
// 4. Find useEffect alias: look for ALIAS(()=>{...},[...]) calls nearby (not useCallback/useMemo)
const nearbyCode = content.substring(Math.max(0, matchIndex - 5000), matchIndex + 5000);
const effectCandidates = {};
const effectRe = /\b(\w{2,3})\(\(\)=>\{[^}]{3,80}\},\[/g;
let m;
while ((m = effectRe.exec(nearbyCode)) !== null) {
const alias = m[1];
if (alias !== callbackAlias && alias !== 'var' && alias !== 'new') {
effectCandidates[alias] = (effectCandidates[alias] || 0) + 1;
}
}
// Also check broader file for common useEffect patterns (with cleanup return)
const cleanupRe = /\b(\w{2,3})\(\(\)=>\{[^}]*return\s*\(\)=>/g;
while ((m = cleanupRe.exec(content)) !== null) {
const alias = m[1];
if (alias !== callbackAlias) {
effectCandidates[alias] = (effectCandidates[alias] || 0) + 5; // higher weight
}
}
// Remove known non-useEffect aliases (useMemo patterns)
// useMemo: alias(()=>EXPRESSION,[deps]) — returns a value, often assigned
// useEffect: alias(()=>{STATEMENTS},[deps]) — no return value
// Pick the most common candidate
let useEffectAlias = null;
let maxCount = 0;
for (const [alias, count] of Object.entries(effectCandidates)) {
if (count > maxCount) {
maxCount = count;
useEffectAlias = alias;
}
}
if (!useEffectAlias) {
console.log(` ❌ [${label}] Could not determine useEffect alias`);
return null;
}
console.log(` useEffect=${useEffectAlias} (confidence: ${maxCount} hits)`);
// 5. Build patch
const patchCode = `_aep=${useEffectAlias}(()=>{${policyVar}===${enumAlias}.EAGER&&!${secureVar}&&${confirmFn}(!0)},[]),`;
return {
target: fullMatch,
replacement: patchCode + fullMatch,
patchMarker: `_aep=${useEffectAlias}(()=>{${policyVar}===${enumAlias}.EAGER`,
label
};
}
// ─── File Operations ────────────────────────────────────────────────────────
function patchFile(filePath, label) {
if (!fs.existsSync(filePath)) {
console.log(` ❌ [${label}] File not found: ${filePath}`);
return false;
}
const content = fs.readFileSync(filePath, 'utf8');
// Check if already patched
if (content.includes('_aep=')) {
const existingPatch = content.match(/_aep=\w+\(\(\)=>\{[^}]+EAGER[^}]+\},\[\]\)/);
if (existingPatch) {
console.log(` ⏭️ [${label}] Already patched`);
return true;
}
}
const analysis = analyzeFile(content, label);
if (!analysis) return false;
// Verify target is unique
const count = content.split(analysis.target).length - 1;
if (count !== 1) {
console.log(` ❌ [${label}] Target found ${count} times (expected 1)`);
return false;
}
// Backup
if (!fs.existsSync(filePath + '.bak')) {
fs.copyFileSync(filePath, filePath + '.bak');
console.log(` 📦 [${label}] Backup created`);
}
// Apply
const patched = content.replace(analysis.target, analysis.replacement);
fs.writeFileSync(filePath, patched, 'utf8');
const diff = fs.statSync(filePath).size - fs.statSync(filePath + '.bak').size;
console.log(` ✅ [${label}] Patched (+${diff} bytes)`);
return true;
}
function revertFile(filePath, label) {
const bak = filePath + '.bak';
if (!fs.existsSync(bak)) {
console.log(` ⏭️ [${label}] No backup, skipping`);
return;
}
fs.copyFileSync(bak, filePath);
console.log(` ✅ [${label}] Restored`);
}
function checkFile(filePath, label) {
if (!fs.existsSync(filePath)) {
console.log(` ❌ [${label}] Not found`);
return false;
}
const content = fs.readFileSync(filePath, 'utf8');
const patched = content.includes('_aep=') && /_aep=\w+\(\(\)=>\{[^}]+EAGER/.test(content);
const hasBak = fs.existsSync(filePath + '.bak');
if (patched) {
console.log(` ✅ [${label}] PATCHED` + (hasBak ? ' (backup exists)' : ''));
} else {
const analysis = analyzeFile(content, label);
if (analysis) {
console.log(` ⬜ [${label}] NOT PATCHED (patchable)`);
} else {
console.log(` ⚠️ [${label}] NOT PATCHED (may be incompatible)`);
}
}
return patched;
}
// ─── Version Info ───────────────────────────────────────────────────────────
function getVersion(basePath) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(basePath, 'resources', 'app', 'package.json'), 'utf8'));
const product = JSON.parse(fs.readFileSync(path.join(basePath, 'resources', 'app', 'product.json'), 'utf8'));
return `${pkg.version} (IDE ${product.ideVersion})`;
} catch { return 'unknown'; }
}
// ─── Main ───────────────────────────────────────────────────────────────────
function main() {
const args = process.argv.slice(2);
const action = args.includes('--revert') ? 'revert' : args.includes('--check') ? 'check' : 'apply';
// Parse --path flag
let explicitPath = null;
const pathIdx = args.indexOf('--path');
if (pathIdx !== -1 && args[pathIdx + 1]) {
explicitPath = path.resolve(args[pathIdx + 1]);
}
console.log('');
console.log('╔══════════════════════════════════════════════════╗');
console.log('║ Antigravity "Always Proceed" Auto-Run Fix ║');
console.log('╚══════════════════════════════════════════════════╝');
let basePath;
if (explicitPath) {
if (!isAntigravityDir(explicitPath)) {
console.log(`\n\u274C --path "${explicitPath}" does not look like an Antigravity installation.`);
console.log(' Expected to find: resources/app/out/vs/workbench/workbench.desktop.main.js');
process.exit(1);
}
basePath = explicitPath;
} else {
basePath = findAntigravityPath();
}
if (!basePath) {
console.log('\n\u274C Antigravity installation not found!');
console.log('');
console.log(' Try one of:');
console.log(' 1. Run from the Antigravity install directory:');
console.log(' cd "C:\\Path\\To\\Antigravity" && npx better-antigravity auto-run');
console.log(' 2. Specify the path explicitly:');
console.log(' npx better-antigravity auto-run --path "D:\\Antigravity"');
process.exit(1);
}
console.log(`\n📍 ${basePath}`);
console.log(`📦 Version: ${getVersion(basePath)}`);
console.log('');
const files = [
{ path: path.join(basePath, 'resources', 'app', 'out', 'vs', 'workbench', 'workbench.desktop.main.js'), label: 'workbench' },
{ path: path.join(basePath, 'resources', 'app', 'out', 'jetskiAgent', 'main.js'), label: 'jetskiAgent' },
];
switch (action) {
case 'check':
files.forEach(f => checkFile(f.path, f.label));
break;
case 'revert':
files.forEach(f => revertFile(f.path, f.label));
console.log('\n✨ Restored! Restart Antigravity.');
break;
case 'apply':
const ok = files.every(f => patchFile(f.path, f.label));
console.log(ok
? '\n✨ Done! Restart Antigravity.\n💡 Run with --revert to undo.\n⚠ Re-run after Antigravity updates.'
: '\n⚠ Some patches failed.');
break;
}
}
main();

503
better-antigravity-main/package-lock.json generated Normal file
View File

@@ -0,0 +1,503 @@
{
"name": "better-antigravity",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "better-antigravity",
"version": "0.2.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"antigravity-sdk": "^1.3.0",
"sql.js": "^1.14.0"
},
"bin": {
"better-antigravity": "cli.js"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.85.0",
"esbuild": "^0.20.0"
},
"engines": {
"node": ">=16.0.0",
"vscode": "^1.85.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@types/node": {
"version": "20.19.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz",
"integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.109.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz",
"integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==",
"license": "MIT"
},
"node_modules/antigravity-sdk": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/antigravity-sdk/-/antigravity-sdk-1.3.0.tgz",
"integrity": "sha512-AonqXNmtnkYYib/pSCcDlxnVxLsNIafIbBQxwTV0zHt6RZBjG8ejknkJAhd8hRyilMefMTOE28oPzTblby2K2A==",
"license": "AGPL-3.0-or-later",
"dependencies": {
"sql.js": "^1.14.0"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@types/vscode": "^1.85.0"
}
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/sql.js": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.0.tgz",
"integrity": "sha512-NXYh+kFqLiYRCNAaHD0PcbjFgXyjuolEKLMk5vRt2DgPENtF1kkNzzMlg42dUk5wIsH8MhUzsRhaUxIisoSlZQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@@ -0,0 +1,94 @@
{
"name": "better-antigravity",
"displayName": "Better Antigravity",
"description": "Community-driven fixes and improvements for Antigravity IDE — auto-run fix, chat rename, and more",
"version": "0.6.0",
"publisher": "kanezal",
"icon": "static/BA-background.png",
"galleryBanner": {
"color": "#1a1a1a",
"theme": "dark"
},
"markdown": "github",
"badges": [
{
"url": "https://img.shields.io/npm/v/better-antigravity",
"href": "https://www.npmjs.com/package/better-antigravity",
"description": "npm version"
},
{
"url": "https://img.shields.io/badge/License-AGPL--3.0-blue.svg",
"href": "https://github.com/Kanezal/better-antigravity/blob/main/LICENSE",
"description": "License: AGPL-3.0"
},
{
"url": "https://img.shields.io/badge/Antigravity-v1.107.0+-blue.svg",
"href": "https://antigravity.dev",
"description": "Antigravity compatibility"
}
],
"engines": {
"vscode": "^1.85.0",
"node": ">=16.0.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./dist/extension.js",
"bin": {
"better-antigravity": "cli.js"
},
"contributes": {
"commands": [
{
"command": "better-antigravity.status",
"title": "Better Antigravity: Show Status"
},
{
"command": "better-antigravity.revertAutoRun",
"title": "Better Antigravity: Revert Auto-Run Fix"
}
]
},
"scripts": {
"build": "node build.mjs",
"watch": "node build.mjs --watch",
"prepackage": "node -e \"require('fs').mkdirSync('out',{recursive:true})\"",
"package": "npm run prepackage && npx @vscode/vsce package --no-dependencies --out out/better-antigravity.vsix",
"publish:ovsx": "node publish-ovsx.mjs",
"fix:auto-run": "node fixes/auto-run-fix/patch.js",
"fix:auto-run:check": "node fixes/auto-run-fix/patch.js --check",
"fix:auto-run:revert": "node fixes/auto-run-fix/patch.js --revert"
},
"repository": {
"type": "git",
"url": "https://github.com/Kanezal/better-antigravity"
},
"homepage": "https://github.com/Kanezal/better-antigravity#readme",
"bugs": {
"url": "https://github.com/Kanezal/better-antigravity/issues"
},
"author": "Kanezal",
"license": "AGPL-3.0-or-later",
"keywords": [
"antigravity",
"antigravity-ide",
"google-antigravity",
"fix",
"auto-run",
"rename-chat",
"community"
],
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.85.0",
"esbuild": "^0.20.0"
},
"dependencies": {
"antigravity-sdk": "^1.5.0",
"sql.js": "^1.14.0"
}
}

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env node
/**
* Publish to Open VSX using token from .env
*
* Usage:
* node publish-ovsx.mjs — publish VSIX
* node publish-ovsx.mjs create-namespace — create publisher namespace (first time only)
*/
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');
const cmd = process.argv[2] || 'publish';
// Read token from .env
let pat;
try {
const env = readFileSync('.env', 'utf8');
const match = env.match(/OVSX_PAT=(.+)/);
if (!match) throw new Error('OVSX_PAT not found in .env');
pat = match[1].trim();
} catch (err) {
console.error('ERROR: Could not read .env file. Create .env with OVSX_PAT=<token>');
process.exit(1);
}
try {
if (cmd === 'create-namespace') {
console.log(`Creating namespace "${pkg.publisher}" on Open VSX...`);
execSync(`npx ovsx create-namespace ${pkg.publisher} --pat ${pat}`, { stdio: 'inherit' });
console.log('Namespace created!');
} else {
const vsixFile = `out/better-antigravity.vsix`;
console.log(`Publishing ${vsixFile} to Open VSX...`);
execSync(`npx ovsx publish ${vsixFile} --pat ${pat}`, { stdio: 'inherit' });
console.log('Done!');
}
} catch {
process.exit(1);
}

View File

@@ -0,0 +1,235 @@
/**
* Auto-Run Fix — Patches the "Always Proceed" terminal policy to actually auto-execute.
*
* Uses structural regex matching to find the onChange handler in minified code
* and injects a missing useEffect that auto-confirms commands when policy is EAGER.
*
* Works across AG versions because it matches code STRUCTURE, not variable NAMES.
*
* @module auto-run
*/
import * as path from 'path';
import * as fs from 'fs';
import * as fsp from 'fs/promises';
/** Marker comment to identify our patches */
const PATCH_MARKER = '/*BA:autorun*/';
/**
* Resolve the Antigravity workbench directory.
*/
export function getWorkbenchDir(): string | null {
const appData = process.env.LOCALAPPDATA || '';
const dir = path.join(
appData,
'Programs', 'Antigravity', 'resources', 'app', 'out',
'vs', 'code', 'electron-browser', 'workbench',
);
return fs.existsSync(dir) ? dir : null;
}
/**
* Target files that need the auto-run patch.
*/
export function getTargetFiles(workbenchDir: string): Array<{ path: string; label: string }> {
return [
{ path: path.join(workbenchDir, 'workbench.desktop.main.js'), label: 'workbench' },
{ path: path.join(workbenchDir, 'jetskiAgent.js'), label: 'jetskiAgent' },
].filter(f => fs.existsSync(f.path));
}
/**
* Check if a file already has the auto-run patch applied.
*/
export async function isPatched(filePath: string): Promise<boolean> {
try {
// Read only first 50 bytes of the marker area via a small buffer scan
// The marker is injected mid-file, so we must read the full file.
// Use async to avoid blocking extension host.
const content = await fsp.readFile(filePath, 'utf8');
return content.includes(PATCH_MARKER);
} catch {
return false;
}
}
/**
* Analyze a file to find the onChange handler and extract variable names.
*
* Returns null if pattern not found (file may already be fixed by AG update).
*/
function analyzeFile(content: string): AnalysisResult | null {
// Find onChange handler for terminalAutoExecutionPolicy
// Pattern: <callback>=<useCallback>((<arg>)=>{<setFn>(<arg>),<arg>===<ENUM>.EAGER&&<confirm>(true)},[...])
const onChangeRegex = /(\w+)=(\w+)\((\(\w+\))=>\{(\w+)\(\w+\),\w+===(\w+)\.EAGER&&(\w+)\(!0\)\},\[/g;
const match = onChangeRegex.exec(content);
if (!match) return null;
const [fullMatch, , , , , enumName, confirmFn] = match;
const insertPos = match.index + fullMatch.length;
// Extract context variables from surrounding code
const contextStart = Math.max(0, match.index - 3000);
const contextEnd = Math.min(content.length, match.index + 3000);
const context = content.substring(contextStart, contextEnd);
// policyVar: <var>=<something>?.terminalAutoExecutionPolicy??<ENUM>.OFF
const policyMatch = /(\w+)=\w+\?\.terminalAutoExecutionPolicy\?\?(\w+)\.OFF/.exec(context);
// secureVar: <var>=<something>?.secureModeEnabled??!1
const secureMatch = /(\w+)=\w+\?\.secureModeEnabled\?\?!1/.exec(context);
if (!policyMatch || !secureMatch) return null;
const policyVar = policyMatch[1];
const secureVar = secureMatch[1];
// Find useEffect — most frequently used short-named function in the scope
const useEffectFn = findUseEffect(context, [confirmFn]);
if (!useEffectFn) return null;
// Find insertion point: after the useCallback closing
const afterOnChange = content.indexOf('])', insertPos);
if (afterOnChange === -1) return null;
const insertAt = content.indexOf(';', afterOnChange);
if (insertAt === -1) return null;
return {
enumName,
confirmFn,
policyVar,
secureVar,
useEffectFn,
insertAt: insertAt + 1,
};
}
/**
* Find the useEffect function name by frequency analysis.
*/
function findUseEffect(context: string, exclude: string[]): string | null {
const candidates: Record<string, number> = {};
const regex = /(\w{1,3})\(\(\)=>\{/g;
let m;
while ((m = regex.exec(context)) !== null) {
const fn = m[1];
if (fn.length <= 3 && !exclude.includes(fn)) {
candidates[fn] = (candidates[fn] || 0) + 1;
}
}
let best = '';
let maxCount = 0;
for (const [fn, count] of Object.entries(candidates)) {
if (count > maxCount) {
best = fn;
maxCount = count;
}
}
return best || null;
}
interface AnalysisResult {
enumName: string;
confirmFn: string;
policyVar: string;
secureVar: string;
useEffectFn: string;
insertAt: number;
}
/**
* Apply the auto-run patch to a single file.
*
* @returns Patch status message
*/
export async function patchFile(filePath: string, label: string): Promise<PatchResult> {
try {
let content = await fsp.readFile(filePath, 'utf8');
if (content.includes(PATCH_MARKER)) {
return { success: true, label, status: 'already-patched' };
}
const analysis = analyzeFile(content);
if (!analysis) {
return { success: false, label, status: 'pattern-not-found' };
}
const { enumName, confirmFn, policyVar, secureVar, useEffectFn, insertAt } = analysis;
// Build the patch
const patch = `${PATCH_MARKER}${useEffectFn}(()=>{${policyVar}===${enumName}.EAGER&&!${secureVar}&&${confirmFn}(!0)},[])`;
// Create backup (only if one doesn't exist)
const backup = filePath + '.ba-backup';
try { await fsp.access(backup); } catch {
await fsp.copyFile(filePath, backup);
}
// Insert
content = content.substring(0, insertAt) + patch + content.substring(insertAt);
await fsp.writeFile(filePath, content, 'utf8');
return { success: true, label, status: 'patched', bytesAdded: patch.length };
} catch (err: any) {
return { success: false, label, status: 'error', error: err.message };
}
}
/**
* Revert the auto-run patch on a single file.
*/
export function revertFile(filePath: string, label: string): PatchResult {
const backup = filePath + '.ba-backup';
if (!fs.existsSync(backup)) {
return { success: false, label, status: 'no-backup' };
}
try {
fs.copyFileSync(backup, filePath);
fs.unlinkSync(backup);
return { success: true, label, status: 'reverted' };
} catch (err: any) {
return { success: false, label, status: 'error', error: err.message };
}
}
export interface PatchResult {
success: boolean;
label: string;
status: 'patched' | 'already-patched' | 'pattern-not-found' | 'reverted' | 'no-backup' | 'error';
bytesAdded?: number;
error?: string;
}
/**
* Auto-apply the fix to all target files.
*
* @returns Array of results for each file
*/
export async function autoApply(): Promise<PatchResult[]> {
const dir = getWorkbenchDir();
if (!dir) return [];
const files = getTargetFiles(dir);
return Promise.all(files.map(f => patchFile(f.path, f.label)));
}
/**
* Revert all target files from backups.
*
* @returns Number of files reverted
*/
export function revertAll(): PatchResult[] {
const dir = getWorkbenchDir();
if (!dir) return [];
const files = getTargetFiles(dir);
return files.map(f => revertFile(f.path, f.label));
}

View File

@@ -0,0 +1,81 @@
/**
* Better Antigravity — VS Code command handlers.
*
* Each exported function is a command handler registered in extension.ts.
*
* @module commands
*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as fsp from 'fs/promises';
import { AntigravitySDK } from 'antigravity-sdk';
import { getWorkbenchDir, getTargetFiles, isPatched, revertAll } from './auto-run';
/**
* Show extension status in the output channel.
*/
export async function status(sdk: AntigravitySDK | null, output: vscode.OutputChannel): Promise<void> {
const lines = [
'=== Better Antigravity ===',
'',
`SDK: ${sdk?.isInitialized ? `v${sdk.version}` : 'not initialized'}`,
`LS: ${sdk?.ls?.isReady ? `port ${sdk.ls.port}` : 'not ready'}`,
`UI: ${sdk?.integration.isInstalled() ? 'installed' : 'not installed'}`,
`Titles: ${sdk?.integration.titles.count ?? 0} custom`,
];
const dir = getWorkbenchDir();
if (dir) {
const files = getTargetFiles(dir);
for (const f of files) {
const patched = await isPatched(f.path);
lines.push(`AutoRun: ${f.label} = ${patched ? 'fixed' : 'not fixed'}`);
}
} else {
lines.push('AutoRun: workbench directory not found');
}
output.appendLine(lines.join('\n'));
output.show(true);
}
/**
* Revert the auto-run fix and prompt for reload.
*
* Also clears V8 Code Cache to prevent stale cached patched code
* from being loaded by Electron (which causes grey screen).
*/
export async function revertAutoRun(): Promise<void> {
const dir = getWorkbenchDir();
if (!dir) {
vscode.window.showErrorMessage('Workbench directory not found.');
return;
}
const results = revertAll();
const reverted = results.filter(r => r.status === 'reverted').length;
if (reverted > 0) {
// Clear V8 Code Cache — stale cache after revert causes grey screen
const appData = process.env.APPDATA || '';
const cacheDirs = [
path.join(appData, 'Antigravity', 'CachedData'),
path.join(appData, 'Antigravity', 'GPUCache'),
path.join(appData, 'Antigravity', 'Code Cache'),
];
for (const d of cacheDirs) {
try { await fsp.rm(d, { recursive: true, force: true }); } catch { /* may not exist */ }
}
const action = await vscode.window.showInformationMessage(
`Auto-run fix reverted (${reverted} file(s)). Caches cleared. Reload to apply.`,
'Reload Now',
);
if (action === 'Reload Now') {
vscode.commands.executeCommand('workbench.action.reloadWindow');
}
} else {
vscode.window.showInformationMessage('No backups found. Nothing to revert.');
}
}

View File

@@ -0,0 +1,72 @@
/**
* Better Antigravity — Extension entry point.
*
* Thin orchestrator: wires up modules, no business logic here.
*
* @module extension
*/
import * as vscode from 'vscode';
import { AntigravitySDK } from 'antigravity-sdk';
import { autoApply } from './auto-run';
import { status, revertAutoRun } from './commands';
let sdk: AntigravitySDK | null = null;
let output: vscode.OutputChannel;
function log(msg: string): void {
const ts = new Date().toISOString().substring(11, 19);
output?.appendLine(`[${ts}] ${msg}`);
}
export async function activate(context: vscode.ExtensionContext) {
output = vscode.window.createOutputChannel('Better Antigravity');
context.subscriptions.push(output);
log('Activating...');
// ── Commands ──────────────────────────────────────────────────────
context.subscriptions.push(
vscode.commands.registerCommand('better-antigravity.status', () => status(sdk, output)),
vscode.commands.registerCommand('better-antigravity.revertAutoRun', revertAutoRun),
);
// ── Auto-Run Fix (async, non-blocking, no prompt) ─────────────────
autoApply().then(fixResults => {
for (const r of fixResults) {
log(`[auto-run] ${r.label}: ${r.status}${r.bytesAdded ? ` (+${r.bytesAdded}b)` : ''}${r.error ? ` -- ${r.error}` : ''}`);
}
});
// ── SDK Init ─────────────────────────────────────────────────────
try {
sdk = new AntigravitySDK(context);
await sdk.initialize();
log(`SDK v${sdk.version} initialized`);
// Title proxy for chat rename
sdk.integration.enableTitleProxy();
// Seamless install (handles first-time prompt + auto-reload on update)
await sdk.integration.installSeamless(
(cmd) => vscode.commands.executeCommand(cmd),
(msg, ...items) => vscode.window.showInformationMessage(msg, ...items),
);
// Heartbeat (keeps renderer script alive)
const hbTimer = setInterval(() => sdk?.integration.signalActive(), 30_000);
context.subscriptions.push({ dispose: () => clearInterval(hbTimer) });
// Auto-repair (re-patch after AG updates)
sdk.integration.enableAutoRepair();
log('Active');
} catch (err: any) {
log(`SDK init failed: ${err.message}`);
log('Running in degraded mode (auto-run fix only)');
}
}
export function deactivate() {
sdk?.dispose();
sdk = null;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": [
"ES2020"
],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"rootDir": "src",
"outDir": "dist",
"declaration": false,
"sourceMap": true,
"resolveJsonModule": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"fixes"
]
}

788
bot.py
View File

@@ -5,12 +5,19 @@ Multi-project channel architecture:
- Each conversation maps to a project via conv_to_project dict - Each conversation maps to a project via conv_to_project dict
- Extension registers projects via bridge/pending/ files - Extension registers projects via bridge/pending/ files
- Commands include project_name for routing to correct IDE window - Commands include project_name for routing to correct IDE window
Multi-PC UX:
- When multiple AG instances are active, messages get instance numbers (PC #1, #2)
- Users can target specific instances with !N <message> (e.g. !2 hello)
- When only one instance is active, natural conversation without numbers
""" """
import asyncio import asyncio
import re
import json import json
import logging import logging
import time import time
from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@@ -23,8 +30,7 @@ from parser import (
md_to_discord_text, md_to_discord_text,
format_task_embed_text, format_task_embed_text,
) )
from watcher import BrainEvent, EventType from models import BrainEvent, EventType, ApprovalRequest, UserResponse
from bridge import BridgeProtocol, ApprovalRequest, UserResponse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -32,23 +38,98 @@ logger = logging.getLogger(__name__)
# ─── Discord UI Components ────────────────────────────────────────── # ─── Discord UI Components ──────────────────────────────────────────
class ApprovalView(discord.ui.View): class ApprovalView(discord.ui.View):
"""Discord buttons for approving/rejecting Antigravity actions.""" """Discord buttons for approving/rejecting Antigravity actions.
def __init__(self, bridge: BridgeProtocol, request: ApprovalRequest): Supports two modes:
super().__init__(timeout=300) 1. Legacy: ✅ 승인 / ❌ 거부 (when no buttons array)
self.bridge = bridge 2. Multi-choice: dynamic buttons from pending's buttons array
(e.g., ✅ Allow Once / ✅ Allow This Conversation / ❌ Deny)
"""
def __init__(self, request: ApprovalRequest,
buttons: list[dict] | None = None, hub=None):
super().__init__(timeout=1800) # 30 minutes
self.hub = hub # WSHub instance for WS response routing
self.request = request self.request = request
self.responded = False self.responded = False
self.buttons_data = buttons
if buttons and len(buttons) > 1:
# Multi-choice mode: remove the default decorated buttons first
# (they are added by @discord.ui.button at class definition time)
self.clear_items()
# Add a Discord button for each option
for btn_info in buttons:
btn_text = btn_info.get("text", "?")
btn_index = btn_info.get("index", 0)
is_reject = btn_text.lower() in ("deny", "reject", "cancel",
"reject all", "decline",
"dismiss", "stop")
style = discord.ButtonStyle.red if is_reject else discord.ButtonStyle.green
emoji = "" if is_reject else ""
button = discord.ui.Button(
label=f"{emoji} {btn_text}",
style=style,
custom_id=f"choice_{request.request_id}_{btn_index}",
)
# Bind the callback with closure over btn_index and btn_text
button.callback = self._make_choice_callback(btn_index, btn_text,
is_reject)
self.add_item(button)
# else: use the default @discord.ui.button decorated methods below
def _make_choice_callback(self, btn_index: int, btn_text: str,
is_reject: bool):
async def callback(interaction: discord.Interaction):
if self.responded:
await interaction.response.send_message("이미 응답됨",
ephemeral=True)
return
self.responded = True
response_data = {
"request_id": self.request.request_id,
"approved": not is_reject,
"button_index": btn_index,
"step_type": getattr(self.request, 'step_type', ''),
"project_name": getattr(self.request, 'project_name', ''),
}
# Hub WS route (primary — reaches remote Extensions)
delivered = False
if self.hub:
await self.hub.send_response_to_pending_owner(self.request.request_id, {
"type": "response", "data": response_data,
})
embed = interaction.message.embeds[0] if interaction.message.embeds else None
if embed:
color = discord.Color.red() if is_reject else discord.Color.green()
embed.color = color
emoji = "" if is_reject else ""
embed.set_footer(
text=f"{emoji} {btn_text} by {interaction.user.display_name}"
)
await interaction.response.edit_message(embed=embed, view=None)
return callback
@discord.ui.button(label="✅ 승인", style=discord.ButtonStyle.green) @discord.ui.button(label="✅ 승인", style=discord.ButtonStyle.green)
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button): async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
# Only active in legacy mode (no buttons array)
if self.buttons_data and len(self.buttons_data) > 1:
return # multi-choice mode handles via dynamic buttons
if self.responded: if self.responded:
await interaction.response.send_message("이미 응답됨", ephemeral=True) await interaction.response.send_message("이미 응답됨", ephemeral=True)
return return
self.responded = True self.responded = True
self.bridge.write_response(UserResponse( response_data = {
request_id=self.request.request_id, approved=True, "request_id": self.request.request_id, "approved": True,
)) "step_type": getattr(self.request, 'step_type', ''),
"project_name": getattr(self.request, 'project_name', ''),
}
if self.hub:
await self.hub.send_response_to_pending_owner(self.request.request_id, {
"type": "response", "data": response_data,
})
embed = interaction.message.embeds[0] if interaction.message.embeds else None embed = interaction.message.embeds[0] if interaction.message.embeds else None
if embed: if embed:
embed.color = discord.Color.green() embed.color = discord.Color.green()
@@ -57,13 +138,22 @@ class ApprovalView(discord.ui.View):
@discord.ui.button(label="❌ 거부", style=discord.ButtonStyle.red) @discord.ui.button(label="❌ 거부", style=discord.ButtonStyle.red)
async def reject(self, interaction: discord.Interaction, button: discord.ui.Button): async def reject(self, interaction: discord.Interaction, button: discord.ui.Button):
# Only active in legacy mode (no buttons array)
if self.buttons_data and len(self.buttons_data) > 1:
return # multi-choice mode handles via dynamic buttons
if self.responded: if self.responded:
await interaction.response.send_message("이미 응답됨", ephemeral=True) await interaction.response.send_message("이미 응답됨", ephemeral=True)
return return
self.responded = True self.responded = True
self.bridge.write_response(UserResponse( response_data = {
request_id=self.request.request_id, approved=False, "request_id": self.request.request_id, "approved": False,
)) "step_type": getattr(self.request, 'step_type', ''),
"project_name": getattr(self.request, 'project_name', ''),
}
if self.hub:
await self.hub.send_response_to_pending_owner(self.request.request_id, {
"type": "response", "data": response_data,
})
embed = interaction.message.embeds[0] if interaction.message.embeds else None embed = interaction.message.embeds[0] if interaction.message.embeds else None
if embed: if embed:
embed.color = discord.Color.red() embed.color = discord.Color.red()
@@ -71,10 +161,14 @@ class ApprovalView(discord.ui.View):
await interaction.response.edit_message(embed=embed, view=None) await interaction.response.edit_message(embed=embed, view=None)
async def on_timeout(self): async def on_timeout(self):
if not self.responded: if not self.responded and self.hub:
self.bridge.write_response(UserResponse( await self.hub.send_response_to_pending_owner(self.request.request_id, {
request_id=self.request.request_id, approved=False, "type": "response", "data": {
)) "request_id": self.request.request_id, "approved": False,
"step_type": getattr(self.request, 'step_type', ''),
"project_name": getattr(self.request, 'project_name', ''),
}
})
# ─── Bot ───────────────────────────────────────────────────────────── # ─── Bot ─────────────────────────────────────────────────────────────
@@ -99,12 +193,56 @@ class GravityBot(commands.Bot):
self.conv_to_project: dict[str, str] = {} # conv_id → project self.conv_to_project: dict[str, str] = {} # conv_id → project
self.channel_to_project: dict[int, str] = {} # channel.id → project self.channel_to_project: dict[int, str] = {} # channel.id → project
self.session_status_messages: dict[str, int] = {} # conv_id → msg_id self.session_status_messages: dict[str, int] = {} # conv_id → msg_id
self._sent_approval_ids: set[str] = set() self._sent_approval_ids: dict[str, bool] = {} # request_id → bool
self._deferred_ids: dict[str, int] = {} # request_id → defer count
self._sent_commands: dict[str, str] = {} # request_id → command text (for MERGE edit detection)
self._ready_event = asyncio.Event() self._ready_event = asyncio.Event()
self._channel_lock = asyncio.Lock() self._channel_lock = asyncio.Lock()
self.bridge = BridgeProtocol()
self.session_category: discord.CategoryChannel | None = None self.session_category: discord.CategoryChannel | None = None
self.guild: discord.Guild | None = None self.guild: discord.Guild | None = None
self.auto_approve_projects: set[str] = set() # projects with auto-approve enabled
self._processed_message_ids: deque[int] = deque(maxlen=200) # dedup for Gateway event replay
self._approval_messages: dict[str, int] = {} # FIX #4: request_id → discord message_id (for auto_resolved lookup)
self._last_auto_toggle: dict[str, float] = {} # project → timestamp (dedup for !auto embed)
self.gateway = None # Set by main.py in gateway mode
self.hub = None # Set by main.py in gateway mode (WSHub instance)
def _write_command(self, project: str, text: str, *,
target_instance: int | None = None, **kwargs):
"""Write command to Extension via Hub WS (primary) or file bridge (fallback).
When Hub is connected, ONLY use WS to prevent duplicate delivery.
File bridge + Gateway are legacy fallbacks for when Hub is unavailable.
Args:
target_instance: If set, send only to this instance number (via Hub).
If None, broadcast to all instances.
"""
cmd_data = {
"text": text,
"project_name": kwargs.get('project_name', project),
}
# Hub route (primary)
if self.hub:
import time as _time
cmd_data["id"] = str(int(_time.time() * 1000))
msg = {"type": "command", "data": cmd_data}
if target_instance is not None:
asyncio.create_task(
self.hub.send_to_instance(project, target_instance, msg)
)
else:
asyncio.create_task(
self.hub.broadcast_to_project(project, msg)
)
def _cap_dict(self, d: dict, max_size: int = 5000):
"""Prevent memory leaks by capping dictionary sizes using insertion order (oldest first)."""
if len(d) >= max_size:
to_remove = len(d) - max_size + max_size // 10 # remove 10%
for k in list(d.keys())[:to_remove]:
d.pop(k, None)
@staticmethod @staticmethod
def _make_channel_name(project_name: str) -> str: def _make_channel_name(project_name: str) -> str:
@@ -113,9 +251,9 @@ class GravityBot(commands.Bot):
async def setup_hook(self): async def setup_hook(self):
self.loop.create_task(self._process_events()) self.loop.create_task(self._process_events())
self.pending_approval_scanner.start()
self.chat_snapshot_scanner.start()
self._register_slash_commands() self._register_slash_commands()
# Register Hub handlers (if Hub is available, set after setup_hook by main.py)
asyncio.get_event_loop().call_soon(self._register_hub_handlers)
logger.info("Bot setup complete") logger.info("Bot setup complete")
def _register_slash_commands(self): def _register_slash_commands(self):
@@ -127,7 +265,7 @@ class GravityBot(commands.Bot):
if not project: if not project:
await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True) await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True)
return return
self.bridge.write_command(project, "!stop", project_name=project) self._write_command(project, "!stop", project_name=project)
await interaction.response.send_message( await interaction.response.send_message(
embed=discord.Embed( embed=discord.Embed(
title="⏹️ AI 작업 중지", title="⏹️ AI 작업 중지",
@@ -137,13 +275,19 @@ class GravityBot(commands.Bot):
) )
@self.tree.command(name="auto", description="자동 승인 토글") @self.tree.command(name="auto", description="자동 승인 토글")
async def slash_auto(interaction: discord.Interaction, mode: str): async def slash_auto(interaction: discord.Interaction):
project = self.channel_to_project.get(interaction.channel_id) project = self.channel_to_project.get(interaction.channel_id)
if not project: if not project:
await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True) await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True)
return return
enabled = mode.lower() in ("on", "true", "1") # Toggle
self.bridge.write_command(project, f"!auto {'on' if enabled else 'off'}", project_name=project) if project in self.auto_approve_projects:
self.auto_approve_projects.discard(project)
enabled = False
else:
self.auto_approve_projects.add(project)
enabled = True
self._write_command(project, f"!auto {'on' if enabled else 'off'}", project_name=project)
emoji = "🟢" if enabled else "🔴" emoji = "🟢" if enabled else "🔴"
await interaction.response.send_message( await interaction.response.send_message(
embed=discord.Embed( embed=discord.Embed(
@@ -159,7 +303,7 @@ class GravityBot(commands.Bot):
if not project: if not project:
await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True) await interaction.response.send_message("⚠️ 프로젝트 채널이 아닙니다.", ephemeral=True)
return return
self.bridge.write_command(project, message, project_name=project) self._write_command(project, message, project_name=project)
await interaction.response.send_message( await interaction.response.send_message(
embed=discord.Embed( embed=discord.Embed(
description=f"📨 → **{project}** IDE에 전달됨\n`{message[:100]}`", description=f"📨 → **{project}** IDE에 전달됨\n`{message[:100]}`",
@@ -189,57 +333,12 @@ class GravityBot(commands.Bot):
logger.error("No permission to create category!") logger.error("No permission to create category!")
return return
# Discover existing project channels # Start WS Hub processors by ensuring ready gate is open
await self._discover_channels()
# Load conversation → project registrations from Extension
self._load_registrations()
# Sync slash commands to guild
try:
self.tree.copy_global_to(guild=self.guild)
synced = await self.tree.sync(guild=self.guild)
logger.info(f"Synced {len(synced)} slash commands to guild")
except Exception as e:
logger.warning(f"Slash command sync failed: {e}")
# Open the gate
self._ready_event.set() self._ready_event.set()
logger.info("Ready gate opened — event processing enabled") logger.info("Ready gate opened — event processing enabled")
# Start scanner loops
if not self.pending_approval_scanner.is_running():
self.pending_approval_scanner.start()
if not self.chat_snapshot_scanner.is_running():
self.chat_snapshot_scanner.start()
logger.info("Scanner loops started")
# ─── Channel Management ────────────────────────────────────────── # ─── Channel Management ──────────────────────────────────────────
def _load_registrations(self):
"""Read bridge/register/ to learn conversation → project mappings."""
register_dir = self.bridge.bridge_dir / "register"
if not register_dir.exists():
return
count = 0
for f in register_dir.glob("*.json"):
try:
data = json.loads(f.read_text(encoding="utf-8-sig"))
conv_id = data.get("conversation_id", "")
project = data.get("project_name", "")
if conv_id and project:
self.conv_to_project[conv_id] = project
count += 1
except (json.JSONDecodeError, OSError):
pass
# Only log when count changes
prev = getattr(self, '_last_reg_count', -1)
if count != prev:
self._last_reg_count = count
if count:
logger.info(f"Loaded {count} conversation→project registrations")
# ─── Channel Management ────────────────────────────────────────── # ─── Channel Management ──────────────────────────────────────────
@@ -260,32 +359,38 @@ class GravityBot(commands.Bot):
logger.info(f"Discovered {len(self.project_channels)} project channels") logger.info(f"Discovered {len(self.project_channels)} project channels")
async def _get_channel(self, project_name: str) -> discord.TextChannel: async def _get_channel(self, project_name: str) -> discord.TextChannel:
"""Get or create a channel for a project. Lock-protected.""" """Get or create a channel for a project.
Uses guild.channels cache first (NO API call), only locks + creates
if channel truly doesn't exist. This prevents O(N) fetch_channels()
API calls when multiple projects arrive simultaneously.
"""
if project_name in self.project_channels: if project_name in self.project_channels:
return self.project_channels[project_name] return self.project_channels[project_name]
async with self._channel_lock: if not self.session_category:
# Double-check after lock logger.error(f"[CHANNEL] session_category is None — cannot get channel for project={project_name}")
if project_name in self.project_channels: return None
return self.project_channels[project_name]
channel_name = self._make_channel_name(project_name) channel_name = self._make_channel_name(project_name)
# Search existing channels FIRST (prevents duplicates) # 1. Check guild channel cache (NO API call — instant)
try: existing = discord.utils.get(
all_channels = await self.guild.fetch_channels() self.guild.channels, name=channel_name,
for ch in all_channels: category_id=self.session_category.id,
if (isinstance(ch, discord.TextChannel) )
and ch.name == channel_name if existing and isinstance(existing, discord.TextChannel):
and ch.category_id == self.session_category.id): self.project_channels[project_name] = existing
self.project_channels[project_name] = ch self.channel_to_project[existing.id] = project_name
self.channel_to_project[ch.id] = project_name logger.info(f"Found channel (cache): #{channel_name}")
logger.info(f"Found existing channel: #{channel_name}") return existing
return ch
except Exception as e: # 2. Only lock + API call if truly creating new channel
logger.warning(f"fetch_channels failed: {e}") async with self._channel_lock:
# Double-check after lock (another coroutine may have created it)
if project_name in self.project_channels:
return self.project_channels[project_name]
# No existing channel — create new
try: try:
ch = await self.guild.create_text_channel( ch = await self.guild.create_text_channel(
name=channel_name, name=channel_name,
@@ -307,6 +412,9 @@ class GravityBot(commands.Bot):
except discord.errors.Forbidden: except discord.errors.Forbidden:
logger.error(f"No permission to create channel: {channel_name}") logger.error(f"No permission to create channel: {channel_name}")
return None return None
except Exception as e:
logger.error(f"[CHANNEL] Failed to create channel #{channel_name}: {e}")
return None
def _resolve_project(self, conversation_id: str) -> str: def _resolve_project(self, conversation_id: str) -> str:
"""Get project name for a conversation. Falls back to default.""" """Get project name for a conversation. Falls back to default."""
@@ -398,110 +506,93 @@ class GravityBot(commands.Bot):
event_label = "생성" if event.event_type == EventType.FILE_CREATED else "업데이트" event_label = "생성" if event.event_type == EventType.FILE_CREATED else "업데이트"
full_content = event.content.strip() full_content = event.content.strip()
CHUNK_SIZE = 4000 # Discord embed desc limit is 4096 if not full_content:
full_content = "(빈 파일)"
# Split into chunks for long content FILE_ATTACH_THRESHOLD = 4000 # Above this, send as file attachment
chunks = []
while full_content:
chunks.append(full_content[:CHUNK_SIZE])
full_content = full_content[CHUNK_SIZE:]
if not chunks: if len(full_content) > FILE_ATTACH_THRESHOLD:
chunks = ["(빈 파일)"] # Long content → summary embed + file attachment
# Extract first meaningful paragraph for summary
summary_lines = []
for line in full_content.split('\n'):
if line.strip():
summary_lines.append(line.strip())
if len('\n'.join(summary_lines)) > 300:
break
summary = '\n'.join(summary_lines[:5])
if len(summary) > 500:
summary = summary[:500] + '...'
# First chunk with title
embed = discord.Embed( embed = discord.Embed(
title=f"{label} ({event_label}됨)", title=f"{label} ({event_label}됨)",
description=chunks[0], description=f"{summary}\n\n📎 *전체 내용은 첨부 파일을 확인하세요* ({len(full_content):,}자)",
color=discord.Color.blue(),
timestamp=datetime.now(timezone.utc),
)
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
# Create in-memory file attachment
import io
file_bytes = full_content.encode('utf-8')
discord_file = discord.File(
io.BytesIO(file_bytes),
filename=event.file_name,
)
await channel.send(embed=embed, file=discord_file)
else:
# Short content → inline embed (original behavior)
embed = discord.Embed(
title=f"{label} ({event_label}됨)",
description=full_content,
color=discord.Color.blue(), color=discord.Color.blue(),
timestamp=datetime.now(timezone.utc), timestamp=datetime.now(timezone.utc),
) )
embed.set_footer(text=f"Session: {event.conversation_id[:8]}") embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
await channel.send(embed=embed) await channel.send(embed=embed)
# Additional chunks if content is long
for i, chunk in enumerate(chunks[1:], 2):
embed = discord.Embed(
title=f"{label} (계속 {i}/{len(chunks)})",
description=chunk,
color=discord.Color.blue(),
)
await channel.send(embed=embed)
# ─── Approval Scanner ──────────────────────────────────────────── # ─── Approval Scanner ────────────────────────────────────────────
@tasks.loop(seconds=3)
async def pending_approval_scanner(self):
"""Scan bridge/pending/ for new approval requests + reload registrations."""
try:
# Reload conv→project registrations each cycle
self._load_registrations()
# Ensure channels exist for all registered projects
for project in set(self.conv_to_project.values()):
if project not in self.project_channels:
await self._get_channel(project)
logger.info(f"Auto-created channel for registered project: {project}")
requests = self.bridge.get_pending_requests()
for req in requests:
if req.request_id in self._sent_approval_ids:
continue
if req.discord_message_id != 0:
continue
# Learn project mapping from pending approval # ─── Discord → IDE Text Relay + Multi-PC UX ───────────────────────────
project = req.project_name or Config.PROJECT_NAME
if req.conversation_id and req.conversation_id != '__global__':
self.conv_to_project[req.conversation_id] = project
channel = await self._get_channel(project) def _get_instance_header(self, project: str, instance_number: int) -> str:
if channel: """Format instance header based on active count.
self._sent_approval_ids.add(req.request_id)
await self._send_approval_request(channel, req)
except Exception as e:
logger.error(f"Error scanning approvals: {e}")
@pending_approval_scanner.before_loop Single instance: empty string (natural conversation)
async def before_scanner(self): Multiple instances: **[PC #N]** prefix
await self.wait_until_ready() """
if not self.hub:
return ""
active = self.hub.get_active_count(project)
if active <= 1:
return ""
return f"**[PC #{instance_number}]** "
async def _send_approval_request( def _parse_instance_target(self, text: str) -> tuple[int | None, str]:
self, channel: discord.TextChannel, request: ApprovalRequest """Parse !N prefix from message text.
):
embed = discord.Embed(
title="⚠️ 승인 요청",
description=(
f"**명령어:**\n```\n{request.command[:1000]}\n```\n"
f"{request.description[:500]}"
),
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc),
)
embed.set_footer(text=f"ID: {request.request_id}")
view = ApprovalView(self.bridge, request) Returns (target_instance, remaining_text).
msg = await channel.send(embed=embed, view=view) '!2 hello' -> (2, 'hello')
'hello' -> (None, 'hello')
pending_file = self.bridge.pending_dir / f"{request.request_id}.json" '!stop' -> (None, '!stop') # special commands not treated as targeting
if pending_file.exists(): """
try: match = re.match(r'^!(\d+)\s+(.+)', text, re.DOTALL)
data = json.loads(pending_file.read_text(encoding="utf-8-sig")) if match:
data["discord_message_id"] = msg.id return int(match.group(1)), match.group(2).strip()
pending_file.write_text( return None, text
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
)
except (json.JSONDecodeError, OSError):
pass
logger.info(f"Sent approval request: {request.request_id[:12]}")
# ─── Discord → IDE Text Relay ─────────────────────────────────────
async def on_message(self, message: discord.Message): async def on_message(self, message: discord.Message):
if message.author == self.user: if message.author == self.user:
return return
# Dedup: Discord Gateway can deliver MESSAGE_CREATE twice on reconnection
if message.id in self._processed_message_ids:
return
self._processed_message_ids.append(message.id)
# Determine project from channel # Determine project from channel
project = self.channel_to_project.get(message.channel.id) project = self.channel_to_project.get(message.channel.id)
if not project: if not project:
@@ -510,83 +601,362 @@ class GravityBot(commands.Bot):
text = message.content.strip() text = message.content.strip()
# Parse !N instance targeting (before special commands)
target_instance, actual_text = self._parse_instance_target(text)
# Special command: !stop — cancel AI work # Special command: !stop — cancel AI work
if text == "!stop": if actual_text == "!stop":
self.bridge.write_command(project, "!stop", project_name=project) self._write_command(project, "!stop", target_instance=target_instance,
project_name=project)
target_label = f" (PC #{target_instance})" if target_instance else ""
embed = discord.Embed( embed = discord.Embed(
title="⏹️ AI 작업 중지", title="⏹️ AI 작업 중지",
description=f"프로젝트: **{project}**\n중지 요청을 Extension에 전달했습니다.", description=f"프로젝트: **{project}**{target_label}\n중지 요청을 Extension에 전달했습니다.",
color=discord.Color.orange(), color=discord.Color.orange(),
) )
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
return return
# Special command: !auto on/off # Special command: !auto — toggle auto-approve
if text in ("!auto on", "!auto off"): if actual_text == "!auto":
self.bridge.write_command(project, text, project_name=project) # Dedup: skip if toggled within 5s for same project (Gateway event replay)
enabled = text == "!auto on" now = time.time()
last = self._last_auto_toggle.get(project, 0)
if now - last < 5.0:
logger.info(f"[AUTO] Dedup: skipping duplicate !auto for {project} ({now-last:.1f}s ago)")
return
self._last_auto_toggle[project] = now
# Toggle per-project auto-approve
if project in self.auto_approve_projects:
self.auto_approve_projects.discard(project)
enabled = False
else:
self.auto_approve_projects.add(project)
enabled = True
self._write_command(project, f"!auto {'on' if enabled else 'off'}",
target_instance=target_instance, project_name=project)
emoji = "🟢" if enabled else "🔴" emoji = "🟢" if enabled else "🔴"
mode = "자동 승인" if enabled else "수동 승인" mode = "자동 승인" if enabled else "수동 승인"
embed = discord.Embed( embed = discord.Embed(
title=f"{emoji} {mode} 모드", title=f"{emoji} {mode} 모드",
description=f"프로젝트: **{project}**\n" description=f"프로젝트: **{project}**\n"
f"`chat.tools.autoApprove = {enabled}`\n" f"모든 승인 요청이 {'자동으로 승인됩니다' if enabled else '수동 확인이 필요합니다'}",
f"`chat.agent.autoApprove = {enabled}`",
color=discord.Color.green() if enabled else discord.Color.red(), color=discord.Color.green() if enabled else discord.Color.red(),
) )
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
return return
# General text relay — routed by project # General text relay — routed by project (+ optional instance targeting)
if text: if actual_text:
self.bridge.write_command(project, text, project_name=project) self._write_command(project, actual_text, target_instance=target_instance,
project_name=project)
await message.add_reaction("📨") await message.add_reaction("📨")
target_label = f" PC #{target_instance}" if target_instance else ""
embed = discord.Embed( embed = discord.Embed(
description=f"📨 → **{project}** IDE에 전달됨\n`{text[:100]}`", description=f"📨 → **{project}**{target_label} IDE에 전달됨\n`{actual_text[:100]}`",
color=discord.Color.blurple(), color=discord.Color.blurple(),
) )
await message.channel.send(embed=embed, delete_after=10) await message.channel.send(embed=embed, delete_after=10)
await self.process_commands(message) await self.process_commands(message)
# ─── Chat Snapshot Scanner ───────────────────────────────────────── # ─── Hub Event Handlers ──────────────────────────────────────────
@tasks.loop(seconds=5) def _register_hub_handlers(self):
async def chat_snapshot_scanner(self): """Register callbacks on the Hub for Extension->Bot messages."""
"""Scan bridge/chat_snapshots/ for AI response dumps.""" if not self.hub:
return
self.hub.set_bot_handlers(
on_pending=self._hub_on_pending,
on_chat=self._hub_on_chat,
on_register=self._hub_on_register,
on_auto_resolve=self._hub_on_auto_resolve,
on_brain_event=self._hub_on_brain_event,
)
logger.info("[BOT] Hub handlers registered")
async def _hub_on_pending(self, project: str, data: dict):
"""Handle pending approval from Hub (Extension->Hub->Bot)."""
try: try:
snapshot_dir = self.bridge.bridge_dir / "chat_snapshots" request_id = data.get("request_id", "")
if not snapshot_dir.exists(): if not request_id:
return return
for f in snapshot_dir.glob("*.json"): # Skip if already sent
try: if request_id in self._sent_approval_ids:
data = json.loads(f.read_text(encoding="utf-8-sig")) return
project = data.get("project_name", Config.PROJECT_NAME)
content = data.get("content", "")
if content: # Check auto_resolved / auto_approved status
status = data.get("status", "pending")
if status in ("auto_resolved", "expired"):
await self._handle_auto_resolved(request_id, status)
return
if status == "auto_approved":
# Bridge-level auto-approve (e.g. "Always run") — show notification only
channel = await self._get_channel(project) channel = await self._get_channel(project)
if channel: if channel:
# Split long content cmd_text = data.get("command", "")[:200]
CHUNK = 4000 desc_text = data.get("description", "")[:300]
chunks = [content[i:i+CHUNK] for i in range(0, len(content), CHUNK)]
for i, chunk in enumerate(chunks):
title = "💬 AI 대화 내용" if i == 0 else f"💬 (계속 {i+1}/{len(chunks)})"
embed = discord.Embed( embed = discord.Embed(
title=title, title="🤖 자동 승인됨 (Always run)",
description=chunk, description=f"✅ **{cmd_text}**" + (f"\n```\n{desc_text}\n```" if desc_text and len(desc_text) > 3 else ""),
color=discord.Color.green(),
)
embed.set_footer(text=f"auto-approve | {request_id[:12]}")
await channel.send(embed=embed)
self._cap_dict(self._sent_approval_ids)
self._sent_approval_ids[request_id] = True
logger.info(f"[HUB-PENDING] Auto-approved (Always run): {request_id[:12]} project={project}")
return
instance_number = data.get("_instance_number", 0)
pc_name = data.get("_pc_name", "")
header = self._get_instance_header(project, instance_number)
# Build approval request
request = ApprovalRequest(
request_id=request_id,
conversation_id=data.get("conversation_id", ""),
command=data.get("command", ""),
description=data.get("description", ""),
timestamp=data.get("timestamp", time.time()),
project_name=project,
step_type=data.get("step_type", ""),
status=status,
)
# Auto-approve check
if project in self.auto_approve_projects:
await self._auto_approve_via_hub(request)
return
# Send to Discord
channel = await self._get_channel(project)
if not channel:
logger.warning(f"[HUB-PENDING] No channel for project={project}")
return
buttons = data.get("buttons", [])
desc_parts = []
if header:
desc_parts.append(header)
# Clean command text (remove "Running2" artifacts → "Running 2")
cmd_text = request.command[:200]
import re
cmd_text = re.sub(r'Running(\d)', r'Running \1', cmd_text)
desc_parts.append(f"**명령:** `{cmd_text}`")
if buttons:
btn_names = [b.get("text", "?") for b in buttons]
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
# Clean description: strip noise headers and garbage
desc_raw = request.description or ""
# Remove old-style headers
desc_raw = re.sub(r'\[AI 본문 요약\]\s*', '', desc_raw)
desc_raw = re.sub(r'\[결행 명령\]\s*', '', desc_raw)
# Remove lines that are clearly noise
desc_lines = desc_raw.split('\n')
clean_desc_lines = []
for dline in desc_lines:
dline_stripped = dline.strip()
if not dline_stripped:
continue
# Skip UI artifacts
if dline_stripped in ('chevron_right', 'chevron_left', 'close', 'check',
'content_copy', 'expand_more', 'expand_less',
'Show more', 'Show less', 'Copy', 'Edit', 'Copied!'):
continue
# Skip "Thought for Xs"
if re.match(r'^Thought for \d+', dline_stripped):
continue
# Skip TypeScript declarations and file paths
if re.match(r'^(declare|import|export)\s+(class|function|interface|type|enum|const)', dline_stripped):
continue
if re.search(r'\.ts:\d+:', dline_stripped):
continue
if re.search(r'extension.*src.*sdk', dline_stripped, re.IGNORECASE):
continue
clean_desc_lines.append(dline_stripped)
clean_desc = '\n'.join(clean_desc_lines).strip()
if clean_desc and len(clean_desc) > 3:
# Truncate and wrap in code block for readability
if len(clean_desc) > 300:
clean_desc = clean_desc[:300] + ''
desc_parts.append(f"```\n{clean_desc}\n```")
embed = discord.Embed(
title=f"⚠️ 승인 요청 — {request.step_type or 'action'}",
description="\n".join(desc_parts),
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc),
)
embed.set_footer(text=f"ID: {request_id}")
view = ApprovalView(request, buttons=buttons, hub=self.hub)
msg = await channel.send(
content=f"🔔 **새로운 승인 요청이 도착했습니다** (ID: {request_id[:8]})",
embed=embed,
view=view
)
self._cap_dict(self._sent_approval_ids)
self._sent_approval_ids[request_id] = True
self._cap_dict(self._approval_messages)
self._approval_messages[request_id] = msg.id
logger.info(f"[HUB-PENDING] Sent approval: {request_id[:12]} project={project} | URL: {msg.jump_url}")
except Exception as e:
logger.error(f"[HUB-PENDING] Error: {e}")
async def _auto_approve_via_hub(self, request: ApprovalRequest):
"""Auto-approve a pending request via Hub."""
self._cap_dict(self._sent_approval_ids)
self._sent_approval_ids[request.request_id] = True
if self.hub:
await self.hub.send_response_to_pending_owner(request.request_id, {
"type": "response",
"data": {
"request_id": request.request_id,
"approved": True,
"button_index": 0,
"step_type": request.step_type,
"project_name": request.project_name,
},
})
# Send compact auto-approved embed to Discord (was missing — caused silent approvals)
channel = await self._get_channel(request.project_name)
if channel:
try:
embed = discord.Embed(
title="🤖 자동 승인됨",
description=f"✅ **{request.command}**\n\n```\n{request.description[:2000]}\n```" if getattr(request, "description", "") else f"✅ **{request.command}**",
color=discord.Color.green(),
)
embed.set_footer(text=f"auto-approve | {request.request_id[:12]}")
await channel.send(embed=embed)
except Exception as e:
logger.error(f"[HUB-AUTO] Discord send failed: {e}")
logger.info(f"[HUB-AUTO] Auto-approved: {request.request_id[:12]} project={request.project_name}")
async def _hub_on_chat(self, project: str, data: dict):
"""Handle chat snapshot from Hub (Extension->Hub->Bot->Discord)."""
try:
content = data.get("content", "")
attached_files = data.get("attached_files", [])
if not content and not attached_files:
return
instance_number = data.get("_instance_number", 0)
header = self._get_instance_header(project, instance_number)
channel = await self._get_channel(project)
if not channel:
return
import io as _io
discord_files = []
for af in attached_files:
af_name = af.get("name", "document.md")
af_content = af.get("content", "")
if af_content:
discord_files.append(discord.File(
_io.BytesIO(af_content.encode("utf-8")),
filename=af_name,
))
display_content = f"{header}{content}" if header else content
FILE_ATTACH_THRESHOLD = 4000
if len(display_content) > FILE_ATTACH_THRESHOLD:
summary = display_content[:500].rsplit('\n', 1)[0]
embed = discord.Embed(
title="💬 AI 대화 내용",
description=f"{summary}\n\n📎 *전체 내용은 첨부 파일 참조* ({len(content):,}자)",
color=discord.Color.purple(), color=discord.Color.purple(),
timestamp=datetime.now(timezone.utc), timestamp=datetime.now(timezone.utc),
) )
await channel.send(embed=embed) discord_files.append(discord.File(
_io.BytesIO(content.encode("utf-8")),
filename="chat_message.md",
))
await channel.send(embed=embed, files=discord_files)
else:
embed = discord.Embed(
title="💬 AI 대화 내용",
description=display_content,
color=discord.Color.purple(),
timestamp=datetime.now(timezone.utc),
)
await channel.send(
embed=embed,
files=discord_files if discord_files else discord.utils.MISSING,
)
f.unlink() # Cleanup logger.info(f"[HUB-CHAT] Sent to #{channel.name} ({len(content)} chars)")
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"Bad chat snapshot {f.name}: {e}")
except Exception as e: except Exception as e:
logger.error(f"Error scanning chat snapshots: {e}") logger.error(f"[HUB-CHAT] Error: {e}")
async def _hub_on_register(self, data: dict):
"""Handle session registration from Hub."""
conv_id = data.get("conversation_id", "")
project = data.get("project_name", "")
if conv_id and project:
self.conv_to_project[conv_id] = project
logger.info(f"[HUB-REG] {conv_id[:8]}{project}")
async def _hub_on_auto_resolve(self, project: str, data: dict):
"""Handle auto_resolve notification from Hub."""
request_id = data.get("request_id", "")
if request_id:
await self._handle_auto_resolved(request_id, "auto_resolved")
async def _hub_on_brain_event(self, project: str, data: dict):
"""Handle brain event from Hub (Extension->Hub->Bot->Discord)."""
try:
from models import BrainEvent, EventType
event = BrainEvent(
event_type=EventType(data.get("event_type", "file_changed")),
conversation_id=data.get("conversation_id", ""),
file_name=data.get("file_name", ""),
file_path=None,
content=data.get("content", ""),
timestamp=data.get("timestamp", time.time()),
)
await self.event_queue.put(event)
except Exception as e:
logger.error(f"[HUB-EVENT] Error: {e}")
async def _handle_auto_resolved(self, request_id: str, status: str):
"""Edit Discord message to show auto-resolved/expired status."""
msg_id = self._approval_messages.get(request_id)
if not msg_id:
return
# Find the channel containing this message
for channel in self.project_channels.values():
try:
msg = await channel.fetch_message(msg_id)
embed = msg.embeds[0] if msg.embeds else None
if embed:
if status == "auto_resolved":
embed.color = discord.Color.green()
embed.set_footer(text="✅ 자동 해결됨")
else:
embed.color = discord.Color.greyple()
embed.set_footer(text="⏰ 만료됨")
await msg.edit(embed=embed, view=None)
self._approval_messages.pop(request_id, None)
break
except (discord.NotFound, discord.Forbidden):
continue
except Exception:
break
# ─── Chat Snapshot Scanner ─────────────────────────────────────────
@chat_snapshot_scanner.before_loop
async def before_chat_scanner(self):
await self.wait_until_ready()

132
bridge.py
View File

@@ -1,132 +0,0 @@
"""Bridge protocol — file-based communication between Discord bot and Antigravity.
Bridge directory: ~/.gemini/antigravity/bridge/
Structure:
bridge/
pending/ ← Bot writes approval requests for Discord
response/ ← Bot writes user responses from Discord
commands/ ← Bot writes user text input from Discord
Protocol:
1. VS Code Extension detects pending approval → writes JSON to pending/
2. Bot reads pending/ → sends Discord message with ✅/❌ buttons
3. User clicks button → Bot writes JSON to response/
4. VS Code Extension reads response/ → executes action
"""
import json
import time
import logging
from pathlib import Path
from dataclasses import dataclass, asdict
from enum import Enum
from config import Config
logger = logging.getLogger(__name__)
class ApprovalStatus(Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
TIMEOUT = "timeout"
@dataclass
class ApprovalRequest:
"""An approval request from Antigravity."""
request_id: str
conversation_id: str
command: str # The command/action needing approval
description: str # Human-readable description
timestamp: float
status: str = "pending"
discord_message_id: int = 0
project_name: str = "" # Project routing key
@dataclass
class UserResponse:
"""A user response from Discord."""
request_id: str
approved: bool
user_input: str = ""
timestamp: float = 0
class BridgeProtocol:
"""Manages the file-based bridge protocol."""
def __init__(self):
self.bridge_dir = Config.BRAIN_PATH.parent / "bridge"
self.pending_dir = self.bridge_dir / "pending"
self.response_dir = self.bridge_dir / "response"
self.commands_dir = self.bridge_dir / "commands"
# Create directories
for d in [self.pending_dir, self.response_dir, self.commands_dir]:
d.mkdir(parents=True, exist_ok=True)
logger.info(f"Bridge protocol initialized: {self.bridge_dir}")
def get_pending_requests(self) -> list[ApprovalRequest]:
"""Read all pending approval requests."""
requests = []
fields = {f.name for f in ApprovalRequest.__dataclass_fields__.values()}
for f in self.pending_dir.glob("*.json"):
try:
data = json.loads(f.read_text(encoding="utf-8-sig"))
if data.get("status") == "pending":
# Filter to known fields only
filtered = {k: v for k, v in data.items() if k in fields}
requests.append(ApprovalRequest(**filtered))
except (json.JSONDecodeError, TypeError, OSError) as e:
logger.warning(f"Bad pending request {f.name}: {e}")
return requests
def write_response(self, response: UserResponse):
"""Write a user response to the response directory."""
response.timestamp = time.time()
filename = f"{response.request_id}.json"
filepath = self.response_dir / filename
filepath.write_text(
json.dumps(asdict(response), ensure_ascii=False, indent=2),
encoding="utf-8"
)
logger.info(f"Response written: {filename} (approved={response.approved})")
# Mark pending request as processed
pending_file = self.pending_dir / filename
if pending_file.exists():
try:
data = json.loads(pending_file.read_text(encoding="utf-8"))
data["status"] = "approved" if response.approved else "rejected"
pending_file.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8"
)
except (json.JSONDecodeError, OSError):
pass
def write_command(self, conversation_id: str, text: str, *, project_name: str = ""):
"""Write a user text command for Antigravity to consume."""
cmd_id = f"{int(time.time() * 1000)}"
filepath = self.commands_dir / f"{cmd_id}.json"
data = {
"id": cmd_id,
"conversation_id": conversation_id,
"project_name": project_name,
"text": text,
"timestamp": time.time(),
"consumed": False,
}
filepath.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8"
)
logger.info(f"Command written: {cmd_id} → project={project_name}")
return cmd_id

View File

@@ -16,10 +16,11 @@ class Config:
DISCORD_GUILD_ID: int = int(os.getenv("DISCORD_GUILD_ID") or "0") DISCORD_GUILD_ID: int = int(os.getenv("DISCORD_GUILD_ID") or "0")
# Antigravity Brain path # Antigravity Brain path
BRAIN_PATH: Path = Path(os.getenv( # NOTE: os.getenv returns "" (not None) when .env has BRAIN_PATH= (empty value).
"BRAIN_PATH", # Path("") resolves to "." (CWD), which is WRONG. Use `or` to handle both None and "".
os.path.expanduser("~/.gemini/antigravity/brain") BRAIN_PATH: Path = Path(
)) os.getenv("BRAIN_PATH") or os.path.expanduser("~/.gemini/antigravity/brain")
)
# Watcher settings # Watcher settings
DEBOUNCE_SECONDS: float = float(os.getenv("DEBOUNCE_SECONDS", "5")) DEBOUNCE_SECONDS: float = float(os.getenv("DEBOUNCE_SECONDS", "5"))
@@ -31,6 +32,9 @@ class Config:
"walkthrough.md", "walkthrough.md",
} }
# Extension-based monitoring: any file with these extensions in brain/{conv}/ is watched
WATCHED_EXTENSIONS: set = {".md"}
# Discord message limits # Discord message limits
DISCORD_MSG_LIMIT: int = 2000 DISCORD_MSG_LIMIT: int = 2000
DISCORD_EMBED_DESC_LIMIT: int = 4096 DISCORD_EMBED_DESC_LIMIT: int = 4096
@@ -39,6 +43,14 @@ class Config:
CHANNEL_PREFIX: str = "AG" CHANNEL_PREFIX: str = "AG"
PROJECT_NAME: str = os.getenv("PROJECT_NAME", "gravity_control") PROJECT_NAME: str = os.getenv("PROJECT_NAME", "gravity_control")
# Bot mode: 'local' (file-based bridge) or 'gateway' (WS Hub + HTTP API)
BOT_MODE: str = os.getenv("BOT_MODE", "local")
GATEWAY_API_KEY: str = os.getenv("GATEWAY_API_KEY", "")
# WebSocket Hub
GRAVITY_HUB_SECRET: str = os.getenv("GRAVITY_HUB_SECRET", "") # JWT signing secret
GRAVITY_REGISTRATION_CODE: str = os.getenv("GRAVITY_REGISTRATION_CODE", "") # Extension auth
@classmethod @classmethod
def validate(cls) -> list[str]: def validate(cls) -> list[str]:
"""Return list of configuration errors.""" """Return list of configuration errors."""
@@ -47,6 +59,7 @@ class Config:
errors.append("DISCORD_TOKEN is not set") errors.append("DISCORD_TOKEN is not set")
if not cls.DISCORD_GUILD_ID: if not cls.DISCORD_GUILD_ID:
errors.append("DISCORD_GUILD_ID is not set") errors.append("DISCORD_GUILD_ID is not set")
if not cls.BRAIN_PATH.exists(): # Gateway mode doesn't need local BRAIN_PATH
if cls.BOT_MODE != 'gateway' and not cls.BRAIN_PATH.exists():
errors.append(f"BRAIN_PATH does not exist: {cls.BRAIN_PATH}") errors.append(f"BRAIN_PATH does not exist: {cls.BRAIN_PATH}")
return errors return errors

0
diag_output.txt Normal file
View File

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
services:
gateway:
build: .
container_name: gravity-gateway
restart: unless-stopped
ports:
- "127.0.0.1:8585:8585"
env_file:
- .env
environment:
- BOT_MODE=gateway
- GATEWAY_PORT=8585
- BRAIN_PATH=/app/data/brain
volumes:
- gateway-data:/app/data
networks:
- default
- proxy-net
logging:
driver: json-file
options:
max-size: 10m
max-file: 3
volumes:
gateway-data:
networks:
proxy-net:
external: true

30
docker-compose_server.yml Normal file
View File

@@ -0,0 +1,30 @@
services:
gateway:
build: .
container_name: gravity-gateway
restart: unless-stopped
ports:
- "127.0.0.1:8585:8585"
env_file:
- .env
environment:
- BOT_MODE=gateway
- GATEWAY_PORT=8585
- BRAIN_PATH=/app/data/brain
volumes:
- gateway-data:/app/data
networks:
- default
- proxy-net
logging:
driver: json-file
options:
max-size: 10m
max-file: 3
volumes:
gateway-data:
networks:
proxy-net:
external: true

View File

@@ -232,3 +232,37 @@
| `git.antigravityCloneNonInteractive` | Clone non-interactive | | `git.antigravityCloneNonInteractive` | Clone non-interactive |
| `git.antigravityGetRemoteUrl` | Get remote URL | | `git.antigravityGetRemoteUrl` | Get remote URL |
| `git.antigravityReportCloneProgress` | Report clone progress | | `git.antigravityReportCloneProgress` | Report clone progress |
---
## 🔌 Language Server ConnectRPC (Direct RPC)
> LS Go 바이너리에서 추출한 RPC 메서드. ConnectRPC(HTTPS) 프로토콜 사용.
> 서비스: `exa.language_server_pb.LanguageServerService`
> 상세: [ls-rpc-reference.md](ls-rpc-reference.md)
### 대화/Trajectory
| 메서드 | 용도 |
|--------|------|
| `GetUserTrajectoryDescriptions` | 모든 대화 목록 (googleAgentId, summary, lastStepIndex) |
| `GetCascadeTrajectory` | 대화 전체 내용 |
| `GetCascadeTrajectorySteps` | 대화 단계별 데이터 (startStepIndex 지원) |
| `StartCascade` | 새 대화 생성 (headless) |
| `SendUserCascadeMessage` | 기존 대화에 메시지 전송 |
### 실시간 스트림
| 메서드 | 용도 |
|--------|------|
| `StreamCascadeReactiveUpdates` | Cascade 실시간 업데이트 |
| `StreamAgentStateUpdates` | 에이전트 상태 스트림 |
### 에이전트 제어
| 메서드 | 용도 |
|--------|------|
| `HandleCascadeUserInteraction` | 사용자 상호작용 (승인/거절) |
| `AcknowledgeCascadeCodeEdit` | 코드 편집 승인 |
| `ResolveOutstandingSteps` | 미해결 단계 처리 |

322
docs/approach-history.md Normal file
View File

@@ -0,0 +1,322 @@
# Antigravity Chat Relay — 접근 히스토리
> 최종 업데이트: 2026-03-07 20:38 KST
> 목표: Discord ↔ Antigravity IDE 양방향 대화 릴레이
---
## 📊 현재 상태 요약
| 방향 | 상태 | 방법 |
|------|------|------|
| Discord → Antigravity | ✅ **동작** | `sendPromptToAgentPanel` 명령어 |
| Antigravity → Discord | ⚠️ **부분** | `getDiagnostics.summary` (영어 제목만) |
| **실제 AI 텍스트 릴레이** | ❌ **미해결** | 모든 RPC 차단됨 |
---
## Phase 1: Discord → Antigravity (채팅 전송) — ✅ 해결
### 시도 1: 채팅 입력 시뮬레이션 ❌
- `vscode.commands.executeCommand('workbench.action.chat.open')` + 클립보드 붙여넣기
- **결과**: 채팅 패널은 열리지만 텍스트 입력 안 됨
- **커밋**: `b42475c`, `35f39ab`, `5780896`, `7f81528`
### 시도 2: `sendTextToChat` API ❌
- `antigravity.sendTextToChat` 명령어 사용
- **결과**: 명령어 존재하지만 동작 안 함 (파라미터 불일치)
- **커밋**: `e4eb756`, `c688812`, `180dba1`, `ae0fd78`
### 시도 3: 전체 명령어 열거 → `sendPromptToAgentPanel` 발견 ✅
- `vscode.commands.getCommands(true)` → 171개 명령어 덤프
- `antigravity.sendPromptToAgentPanel` 발견 → 직접 AI 에이전트에 텍스트 전송
- **결과**: ✅ 성공! Discord 메시지 → AI 에이전트 직접 전달
- **커밋**: `8d5e59c`
**핵심 교훈**: 문서화되지 않은 내부 API는 전체 열거 후 시도해야 함
---
## Phase 2: Antigravity → Discord (AI 응답 릴레이) — ❌ 미해결
### ❌ 시도 1: 파일 변경 감시 (task.md 등)
- Brain 디렉토리의 `task.md`, `walkthrough.md` 변경 감시
- **결과**: 이 파일들은 AI 대화 텍스트가 아님! artifact(계획서, 보고서)만 저장됨
- **사용자 피드백**: "md파일은 참고일뿐. 실제 AI가 대답한 것이 전달되어야"
- **커밋**: `befa5d7`
### ❌ 시도 2: `getManagerTrace` / `getWorkbenchTrace` 명령어
- `antigravity.getManagerTrace`, `antigravity.getWorkbenchTrace` 실행
- **결과**: 양쪽 모두 `undefined` 반환
- **커밋**: `150967d`
### ❌ 시도 3: `getDiagnostics` → `recentTrajectories` 발견 → LS ConnectRPC 시도
- `getDiagnostics` → 대형 JSON (LS PID, CSRF 토큰, `recentTrajectories` 포함)
- **발견**: LS 프로세스(language_server_windows_x64.exe) PID=7856, CSRF 토큰, 포트 3274
- **커밋**: `952883d`
### ❌ 시도 4: ConnectRPC `GetTrajectoryDescriptions` (잘못된 메서드명)
- `exa.language_server_pb.LanguageServerService/GetTrajectoryDescriptions`
- **결과**: HTTP 404 — 메서드명이 잘못됨
- **커밋**: `91b3a7e`
### ❌ 시도 5: HTTPS 프로토콜 문제 → HTTP임을 발견
- LS가 HTTPS를 요구한다고 오인 → "Client sent HTTP to HTTPS server" 에러
- 실제로는 **HTTP** 포트 3274
- **결과**: 프로토콜 수정 후 연결 성공 (Heartbeat OK)
- **커밋**: `f2ed431`
### ✅ 시도 6: Go 바이너리 분석 → 정확한 RPC 메서드명 추출
- Python으로 `language_server_windows_x64.exe`에서 100+ RPC 메서드 추출
- **발견**:
- `Heartbeat` ← 연결 확인 ✅ (200 OK)
- `GetUserTrajectoryDescriptions` ← trajectory 목록 ✅ (데이터 반환)
- `GetCascadeTrajectorySteps` ← 대화 단계 ❌ (아래 참조)
- **커밋**: `be6fae7`, `e4b98af`
### ❌ 시도 7: `GetCascadeTrajectorySteps` + `trajectoryId` (from GetUserTrajectoryDescriptions)
- `GetUserTrajectoryDescriptions``{trajectoryId: "9807a8ec...", current: true}` 반환
- `GetCascadeTrajectorySteps({trajectoryId: "9807a8ec..."})` 호출
- **결과**: `"trajectory not found"` — workspace-level ID이지 conversation ID가 아님!
- **커밋**: `b6adeff`
### ❌ 시도 8: 5가지 RPC 메서드+필드 조합 시도
1. `GetCascadeTrajectorySteps({trajectoryId})` → ❌ trajectory not found
2. `GetCascadeTrajectory({trajectoryId})` → ❌ trajectory not found
3. `GetCascadeTrajectorySteps({cascadeId})` → ❌ trajectory not found
4. `GetCascadeTrajectory({cascadeId})` → ❌ trajectory not found
5. `GetUserTrajectory({trajectoryId})` → ❌ not_found
- **결론**: `GetUserTrajectoryDescriptions``trajectoryId`로는 단계 데이터를 가져올 수 없음
- **커밋**: `dfc76a9`
### ⚠️ 시도 9: `getDiagnostics.recentTrajectories` → `googleAgentId` + `lastStepIndex`
- `getDiagnostics``recentTrajectories`에 cascade-level `googleAgentId`, `lastStepIndex`, `summary` 존재
- **결과**:
-`lastStepIndex` 변화 감지 가능 (step 42 → 48 등)
-`summary` 추출 가능 (하지만 영어 대화 제목뿐 "Greeting User")
- ❌ 실제 AI 응답 텍스트는 없음
- **커밋**: `0c96645`
### ❌ 시도 10: latest trajectory만 추적 → 새 대화 놓침
- `trajectories[length-1]` 하나만 추적
- **결과**: 새 대화의 step 변화를 놓침
- **수정**: 모든 trajectory 추적으로 변경
- **커밋**: `41f90b3`
### ⚠️ 시도 11: 새 대화 감지 → summary 릴레이 성공
- 새 trajectory 출현 (step > 0) 감지 → summary를 Discord에 전달
- **결과**: ✅ Discord에서 메시지 수신! 하지만 "Greeting User" (영어 제목)
- **사용자 피드백**: "대화창은 한글인데 돌아온건 영어야"
- **커밋**: `7415ab7`
### ❌ 시도 12: `LoadTrajectory` + 기존 RPC (googleAgentId 키로 재시도)
1. `LoadTrajectory({trajectoryId})` → ❌ internal: failed to load trajectory
2. `LoadTrajectory({googleAgentId})` → ❌ internal: failed to load trajectory
3. `GetCascadeTrajectory({googleAgentId})` → ❌ trajectory not found
4. `GetCascadeTrajectorySteps({googleAgentId})` → ❌ trajectory not found
- **결론**: 모든 trajectory/cascade RPC가 외부 호출자에게 차단됨
- **커밋**: `b0c2f86`
### ❌ 시도 13: Brain 디렉토리에서 대화 텍스트 검색
- 짧은 대화 (`e8238b5e`)의 brain 폴더 → **빈 폴더** (파일 없음)
- `.system_generated/logs/` → 존재하지 않음 (모든 대화)
- `.system_generated/click_feedback/` → 브라우저 스크린샷만
- **결론**: Brain 디렉토리에는 AI 대화 텍스트가 저장되지 않음
### ❌ 시도 14: `getDiagnostics.extensionLogs`에서 AI 텍스트 추출
- extensionLogs에서 notify_user 패턴, content 블록, 한국어 텍스트 검색
- **결과**: ❌ 실패 — summary fallback 트리거됨, extensionLogs에 AI 응답 텍스트 없음
- **커밋**: `0d90b25`
---
## 🔑 확인된 사실
### 동작하는 것
| 항목 | 상태 | 비고 |
|------|------|------|
| `sendPromptToAgentPanel` | ✅ | Discord → AI 전달 완벽 동작 |
| LS Heartbeat (port 3274) | ✅ | ConnectRPC 연결 확인 |
| `GetUserTrajectoryDescriptions` | ✅ | workspace-level trajectory 목록 반환 |
| `getDiagnostics.recentTrajectories` | ✅ | cascade-level 메타데이터 (googleAgentId, summary, lastStepIndex) |
| Step count 변화 감지 | ✅ | lastStepIndex diff로 새 응답 감지 |
| Summary를 Discord에 전달 | ✅ | 영어 대화 제목만 (실제 AI 텍스트 아님) |
### 동작하지 않는 것 (차단됨)
| 항목 | 에러 | 시도 횟수 |
|------|------|-----------|
| `GetCascadeTrajectorySteps` | trajectory not found | 6+ |
| `GetCascadeTrajectory` | trajectory not found | 4+ |
| `LoadTrajectory` | internal: failed to load | 2 |
| `GetUserTrajectory` | not_found | 1 |
| Brain 디렉토리 대화 텍스트 | 파일 없음 | 1 |
| `getManagerTrace` | undefined | 1 |
| `getWorkbenchTrace` | undefined | 1 |
---
## 🔬 SDK / Extension.js 심층 분석 결과
> extension.js (2.9MB minified) 정적 분석 (2026-03-07 20:50 KST)
### 핵심 아키텍처 발견
| 항목 | 발견 |
|------|------|
| **LS 통신** | `LanguageServerClient.getInstance().client.XXX()` — raw HTTP가 아닌 내부 client 객체 |
| **채팅 패널** | `ChatPanelProvider` 클래스 → WebView 관리 |
| **WebView 통신** | `sendActionToChatPanel()` → LS → WebView |
| **WebView → Host** | `onDidReceiveMessage({type:'update', content:...})` |
| **내보내기 API** | **없음** — exports는 npm 라이브러리뿐 |
| **명령어** | 21개 등록, 대화 접근 명령어 없음 |
### ChatActionType 열거형 (확인된 값)
| 액션 | 용도 |
|------|------|
| `toggleFocus` | 채팅 포커스 토글 |
| `openChatPanel` | 채팅 패널 열기 |
| `CodeBlockMention` | 코드 블록 멘션 |
| `FileMention` | 파일 멘션 |
| `setCascadeId` | cascade ID 설정 |
| `setApiKey` | API 키 설정 |
| `pollMcpServerStates` | MCP 서버 폴링 |
| `updateUserStatus` | 사용자 상태 업데이트 |
| `updateStateForCascadeFilesWithInIdeDiffs` | IDE diff 상태 업데이트 |
### 핵심 통신 경로 (확인됨)
```
Extension Host → LanguageServerClient.client → LS Process (ConnectRPC)
ChatPanelProvider → WebView (chat.js)
onDidReceiveMessage / postMessage
```
> ⚠️ **우리의 raw HTTP 접근이 실패한 이유**: LS Client와 raw HTTP는 **다른 인증/세션 컨텍스트**를 사용.
> LS Client는 Extension Host 프로세스 내에서 초기화되며, 자체 gRPC 채널을 가짐.
> Raw HTTP는 CSRF 토큰으로 인증하지만, trajectory 데이터 접근에는 **추가 세션 컨텍스트** 필요.
---
## 🚧 아직 시도하지 않은 접근법
### 1. StreamCascadeReactiveUpdates (서버 스트리밍 RPC)
- ConnectRPC 서버-사이드 스트리밍 — 실시간 업데이트 구독
- 단순 요청/응답이 아닌 SSE/chunked 방식 필요
- **난이도**: 높음 (스트리밍 프로토콜 구현 필요)
### 2. Antigravity WebView 메시지 인터셉트
- 채팅 패널 WebView ↔ Extension Host 사이의 postMessage 후킹
- VS Code API의 `window.registerWebviewViewProvider` 또는 WebView panel 접근
- **난이도**: 높음 (내부 WebView 참조 필요)
### 3. Extension Host 콘솔 출력 캡처
- `console.log` 출력에서 AI 응답 텍스트 패턴 검색
- OutputChannel이나 DevTools 콘솔에서 직접 캡처
- **난이도**: 중간
### 4. LS 네트워크 트래픽 스니핑
- LS ↔ Extension Host 사이의 네트워크 트래픽 캡처
- Wireshark/mitmproxy 등으로 실제 데이터 흐름 확인
- **난이도**: 중간 (디버깅 단계, 프로덕션 불가)
### 5. Antigravity 확장의 내부 API 직접 접근
- `vscode.extensions.getExtension('google.antigravity')` → exports 탐색
- 내부 `lspClient` 또는 대화 저장소에 직접 접근
- **난이도**: 중간 (exports가 노출되어 있다면)
### 6. IndexedDB / LevelDB 대화 저장소
- VS Code는 IndexedDB(웹) 또는 LevelDB(네이티브)에 데이터 저장
- `%APPDATA%/Antigravity/` 하위의 DB 파일 직접 읽기
- **난이도**: 높음 (DB 스키마 역설계 필요)
### 7. `workbench.action.chat.openEditSession` 등 채팅 내보내기 명령
- VS Code 내장 채팅 관련 명령어로 대화 내용 추출
- **난이도**: 낮음 (시도 안 해봄)
---
## 📝 핵심 교훈
1. **`trajectoryId` ≠ 대화 ID**: `GetUserTrajectoryDescriptions``trajectoryId`는 workspace-level, `getDiagnostics``googleAgentId`는 cascade-level — 둘 다 RPC 단계 데이터 접근에 실패
2. **LS는 외부 HTTP 호출 차단**: Heartbeat, GetUserTrajectoryDescriptions는 동작하지만, 실제 대화 데이터를 반환하는 모든 RPC는 차단됨
3. **Brain 디렉토리 = artifacts만**: 실제 AI 대화 텍스트는 brain에 저장되지 않음
4. **summary = 영어 대화 제목**: `getDiagnostics.recentTrajectories.summary`는 AI가 자동 생성한 제목이지 실제 응답이 아님
5. **반복 실수**: 같은 RPC를 다른 필드명으로 반복 시도 (trajectoryId, cascadeId, googleAgentId, conversationId → 모두 실패)
---
## 🎯 체계적 시도 계획 (우선순위순)
> 아래 각 접근법을 **순서대로** 시도. 각 시도는 **별도 프로브 코드**로, 결과 로그 확인 후 다음 단계 진행.
### Trial A: Extension API exports 탐색 (난이도: 낮음)
**방법**: `vscode.extensions.getExtension('google.antigravity')``.exports` 탐색
**SDK 분석 결과**: exports는 npm 라이브러리만 (63개). 유용한 API 없을 것으로 예상
**그래도 시도하는 이유**: 런타임에서만 노출되는 API가 있을 수 있음 (minified 코드에서 못 찾은 것)
**상태**: 🔄 프로브 코드 빌드 완료, 설치 대기 중
---
### Trial B: `sendChatActionMessage` 명령어 활용 (난이도: 중, **새로 발견**)
**방법**: `antigravity.sendChatActionMessage` 명령어로 `SendActionToChatPanelRequest` JSON 전달
**SDK 분석 근거**: extension.js에서 `registerCommand(i.SEND_CHAT_ACTION_MESSAGE, async e => { ... sendActionToChatPanel(e) })` 확인
**프로브 코드**:
```typescript
// SendActionToChatPanelRequest JSON 구조로 호출
await vscode.commands.executeCommand('antigravity.sendChatActionMessage',
JSON.stringify({ actionType: 'toggleFocus', payload: [] })
);
```
**성공 기준**: 채팅 패널 상태 변경이나 데이터 반환
**기대**: 직접적인 대화 텍스트 접근보다는 채팅 패널 제어에 가까움
---
### Trial C: VS Code Chat History API (난이도: 낮음)
**방법**: `chat` 관련 내보내기/히스토리 명령어 탐색
**프로브 코드**: chat export/history/conversation 명령어 필터링 (Trial A 프로브에 포함)
**상태**: 🔄 Trial A와 함께 프로브 중
---
### Trial D: StreamCascadeReactiveUpdates (난이도: 높음, 기대확률: 높)
**방법**: ConnectRPC 서버-사이드 스트리밍으로 실시간 업데이트 구독
**프로브 코드**:
```typescript
// ConnectRPC streaming: POST + Transfer-Encoding: chunked
// 또는 SSE endpoint
const streamRes = await fetch(`http://127.0.0.1:3274/exa.language_server_pb.LanguageServerService/StreamCascadeReactiveUpdates`, {
method: 'POST',
headers: { 'Content-Type': 'application/connect+proto', 'x-codeium-csrf-token': csrfToken },
body: Buffer.from([0, 0, 0, 0, 0]) // empty protobuf message
});
// Read streaming response chunks
```
**성공 기준**: 스트림에서 AI 응답 텍스트가 실시간으로 수신됨
**실패 시**: Trial E 진행
---
### Trial E: Electron IPC / DevTools Protocol 인터셉트 (난이도: 높음, 기대확률: 높)
**방법**: Electron의 IPC 또는 Chrome DevTools Protocol로 WebView 메시지 캡처
**프로브 코드**:
```typescript
// DevTools Protocol로 WebView의 console.log/network 캡처
// 또는 Electron의 webContents.getAllWebContents()로 채팅 패널 접근
```
**성공 기준**: WebView ↔ Extension Host 사이 메시지에서 AI 텍스트 추출
**실패 시**: 완전히 다른 아키텍처 접근 필요 (예: 화면 OCR, Accessibility API 등)

185
docs/approval-flow.md Normal file
View File

@@ -0,0 +1,185 @@
# Gravity Bridge — 승인 시스템 완전 Flow 가이드
> **Last Updated**: 2026-03-16 (v0.3.12)
> **SSOT**: 이 문서는 승인 시스템의 전체 데이터 플로우와 상태 관리를 설명합니다.
> **수정 시**: known-issues.md와 동기화 필수
---
## 1. 시스템 아키텍처 개요
```
AG IDE (Antigravity)
├── Extension (extension.ts) ← Bridge 핵심
│ ├── setupMonitor() ← 5초 폴링 (GetAllCascadeTrajectories)
│ ├── step_probe ← WAITING step 감지 (GetCascadeTrajectorySteps)
│ ├── DOM Observer ← 렌더러 스크립트 (버튼 감지)
│ ├── processResponseFile() ← Discord 응답 처리
│ ├── writePendingApproval() ← pending 파일 생성 (dedup 포함)
│ └── tryApprovalStrategies() ← RPC 실행
├── bridge/ (파일 시스템)
│ ├── pending/*.json ← 승인 대기 목록
│ ├── response/*.json ← Discord 응답
│ ├── snapshot/*.json ← 채팅 릴레이
│ └── register/*.json ← 세션-프로젝트 매핑
└── Bot (bot.py) ← Discord 통신
├── pending_approval_scanner ← 3초 폴링
├── auto_approve_scanner ← !auto 토글
└── snapshot_scanner ← 채팅 릴레이
```
---
## 2. 데이터 플로우: 승인 요청 → 응답
### 2.1 Pending 생성 경로 (2개)
#### 경로 A: Step Probe → `writePendingApproval()`
```
1. setupMonitor() 5초 폴링 → GetAllCascadeTrajectories
2. RUNNING + delta=0 + modTime 미변경 → stall 감지
3. consecutiveIdleCount >= 1 && !stallProbed
4. GetCascadeTrajectorySteps → WAITING step 발견
5. si !== lastPendingStepIndex 확인 (dedup)
6. writePendingApproval() 호출
├── recentPendingSteps 메모리 dedup 체크 (60초 TTL)
├── 기존 pending 파일 dedup 체크 (15초 윈도우)
└── pending 파일 생성 + recentPendingSteps에 기록
7. stallProbed = true, lastPendingStepIndex = si
```
#### 경로 B: DOM Observer → HTTP POST `/pending`
```
1. 렌더러 MutationObserver → 버튼 감지 (Run, Accept, Allow 등)
2. FALSE_POSITIVE_RE 필터 (Proceed, Continue, Deny 등 차단)
3. "Run"은 sessionStalled=true && lastPendingStepIndex < 0 일 때만 통과
4. HTTP POST /pending → Extension HTTP 핸들러 (L738-812)
5. 파일 직접 생성 (writePendingApproval() 우회!)
⚠️ recentPendingSteps 메모리 dedup 미적용
```
> **주의**: 경로 B는 `writePendingApproval()`의 메모리 dedup을 우회합니다. 하지만 `lastPendingStepIndex >= 0`일 때 "Run" 필터(L757)와 15초 파일 기반 dedup이 방어합니다.
### 2.2 Response 처리 경로
```
1. Bot pending_approval_scanner → pending 파일 발견
2. auto-approve OR Discord 버튼 → write_response() 호출
├── response/*.json 생성
└── pending/*.json 삭제 (!)
3. Extension response watcher (fs.watch + 3초 폴링 fallback)
→ processResponseFile() (300ms 딜레이)
4. processResponseFile():
├── 파일 존재 확인 (HTTP handler가 먼저 삭제했을 수 있음)
├── stale timeout 필터 (2분)
├── auto_resolved/expired 상태 skip
├── project_name 필터
└── tryApprovalStrategies() → RPC 실행
5. sawRunningAfterPending = true (v0.3.12 핵심 수정)
6. response 파일 삭제 (DOM observer 경로는 HTTP handler에 위임)
```
---
## 3. 상태 변수 완전 참조
### 3.1 모듈 레벨 변수 (extension.ts)
| 변수 | 위치 | 역할 | 설정 | 리셋 |
|------|------|------|------|------|
| `lastPendingStepIndex` | L707 | 마지막으로 pending을 생성한 step index | step_probe(L2047,2108), error_probe(L2178) | delta>0(L1972), session change(L1841) |
| `stallProbed` | L708 | 현재 stall에서 probe 완료 여부 | step_probe(L2046,2107,2177) | delta>0(L1980), modTime changed(L1986), session change(L1842) |
| `sawRunningAfterPending` | L709 | pending 후 delta>0 발생 여부 (auto_resolve gate) | delta>0(L1979), **processResponseFile(L2622)** | step_probe(L2049,2110) |
| `sessionStalled` | L706 | AG가 stall 상태인지 | idle count≥1(L1993) | delta>0(L1937), not WAITING(L2135) |
| `recentPendingSteps` | L54 | 메모리 기반 pending dedup Map | writePendingApproval(L2787,2837) | delta>0(L1974), TTL 60초 |
### 3.2 setupMonitor() 로컬 변수
| 변수 | 역할 |
|------|------|
| `consecutiveIdleCount` | 연속 idle poll 수 (stall 감지 debounce) |
| `lastPendingTime` | 마지막 pending 생성 시간 |
| `lastModTime` | 마지막 modifiedTime (thinking vs approval 구분) |
| `wasRunning` | RUNNING→IDLE 전이 추적 |
| `lastResponseCaptureStep` | 응답 캡처 dedup |
---
## 4. 핵심 상태 전이 다이어그램
```
[IDLE] ──step진행(delta>0)──→ [RUNNING]
delta=0 + modTime 변동 → [THINKING] (stall 카운터 리셋)
delta=0 + modTime 고정 → [STALLED]
!stallProbed → step_probe 실행
WAITING 발견 → [PENDING_CREATED]
(stallProbed=true, lastPendingStepIndex=si)
┌──────────────────────────────────┤
▼ ▼
[DISCORD_APPROVED] [AG_LOCAL_APPROVED]
processResponseFile() delta > 0 + !sawRunningAfterPending
sawRunningAfterPending=true → auto_resolve → Discord 알림
│ │
└──────────────────────────────────┘
[STEP_PROGRESSED]
delta > 0 → 전체 리셋
lastPendingStepIndex = -1
stallProbed = false
sawRunningAfterPending = true
recentPendingSteps 클리어
```
---
## 5. v0.3.12 수정 — 왜 `sawRunningAfterPending = true`인가
### 5.1 이전 문제: 무한 루프 (v0.3.11 이전)
processResponseFile이 `lastPendingStepIndex = -1`로 리셋 → step_probe가 같은 WAITING step 재감지 → 새 pending → auto-approve → response → 다시 리셋 → **무한 루프**
### 5.2 v0.3.11 시도: 모든 리셋 제거
`lastPendingStepIndex``stallProbed` 리셋을 완전 제거 → **무한 루프 해소**, 하지만:
- known-issues L479 회귀: Discord 승인 후 AG 진행 시 `sawRunningAfterPending=false` + `lastPendingStepIndex>=0` → auto_resolve 중복 알림
- `stallProbed` 영구 잠금 우려 (실제로는 delta>0에서 자연 리셋)
### 5.3 v0.3.12 해결: `sawRunningAfterPending = true`
Discord 승인 response 처리 후 `sawRunningAfterPending = true`만 설정:
1. ✅ 무한 루프 방지: `lastPendingStepIndex` 유지 → dedup 작동
2. ✅ auto_resolve 중복 방지: `sawRunningAfterPending = true` → L1939 조건 FALSE
3. ✅ stallProbed 자연 리셋: delta>0에서 L1980
4. ✅ 신호 수집 무영향: step_probe, GetAllCascadeTrajectories 코드 미변경
---
## 6. 위험 지점 목록 (수정 시 반드시 확인)
| 코드 위치 | 위험 | 확인 사항 |
|-----------|------|----------|
| processResponseFile 리셋 (L2607+) | 무한 루프 or auto_resolve 중복 | `sawRunningAfterPending = true`만 설정. `lastPendingStepIndex``stallProbed`는 건드리지 말 것 |
| HTTP POST /pending (L738-812) | DOM observer 경로가 writePendingApproval() 우회 | "Run" 필터(L757)와 파일 기반 dedup이 방어 |
| bridge.py write_response (L460-461) | pending 파일 삭제 | 메모리 dedup(recentPendingSteps)이 재생성 방지 |
| auto_resolve (L1939-1977) | 중복 알림 | `sawRunningAfterPending` gate 확인 |
| step_probe offset (L2025-2070) | 775-step 리밋 | stepOffset으로 최신 step 조회 |
| session change (L1832-1854) | 모든 상태 초기화 | lastPendingStepIndex, stallProbed, sawRunningAfterPending 모두 리셋 |
---
## 7. 과거 이슈 교차 참조
| 이슈 (known-issues.md) | 방어 코드 | 상태 |
|------------------------|----------|------|
| L252: 중복 승인 요청 | writePendingApproval dedup (15초 윈도우 + 메모리 dedup) | ✅ 해결 |
| L264: pending 무한 누적 | write_response()에서 삭제 + 5분 age filter | ✅ 해결 |
| L288: DOM observer ENOENT | isDomObserver 분기 삭제 (L2619) | ✅ 해결 |
| L384: 크로스 프로젝트 MERGE | project_name 가드 (L2774) | ✅ 해결 |
| L444: DEDUP 크로스 세션 | conversation_id 가드 (L2794) | ✅ 해결 |
| L474-479: auto_resolve 중복 | `sawRunningAfterPending = true` (v0.3.12) | ✅ 해결 |
| L493: Double-Fire auto-approve | Extension auto-approve 경로 제거, Bot 단일 경로 | ✅ 해결 |
| L499: Deny false positive | FALSE_POSITIVE_RE + Bot reject guard | ✅ 해결 |

View File

@@ -14,3 +14,6 @@
| 10 | 15:00 | @bridge@gravity 이름 변경 + 슬래시 명령어 /stop /auto /send | `02e9e4d`~`0bd525a` | ✅ | | 10 | 15:00 | @bridge@gravity 이름 변경 + 슬래시 명령어 /stop /auto /send | `02e9e4d`~`0bd525a` | ✅ |
| 11 | 16:00 | sendTextToChat 탐색 → sendPromptToAgentPanel 발견 | `e4eb756`~`8d5e59c` | ✅ | | 11 | 16:00 | sendTextToChat 탐색 → sendPromptToAgentPanel 발견 | `e4eb756`~`8d5e59c` | ✅ |
| 12 | 17:15 | 양방향 통신 완성 + 171개 명령어 문서화 + scanner 시작 수정 | `befa5d7` | ✅ | | 12 | 17:15 | 양방향 통신 완성 + 171개 명령어 문서화 + scanner 시작 수정 | `befa5d7` | ✅ |
| 13 | 17:50 | getDiagnostics 구조 프로브 → 대화 텍스트 미포함 확인 | `952883d`~`150967d` | 🔧 |
| 14 | 18:15 | LS ConnectRPC 브릿지 구현 + HTTPS 프로토콜 감지 수정 | `91b3a7e`~`f2ed431` | 🔧 |
| 15 | 18:45 | Go 바이너리에서 100+ RPC 메서드 추출 → 정확한 메서드명 적용 | `be6fae7` | 🔧 |

23
docs/devlog/2026-03-08.md Normal file
View File

@@ -0,0 +1,23 @@
# 2026-03-08 Devlog — Bridge 프로토콜 수정 + 딥 디버깅
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 1 | 01:00 | Extension↔Bot 프로토콜 불일치 3건 수정 + sql-wasm 번들링 | `e4dc1b1` | 🔧 |
| 2 | 01:45~02:25 | Discord Bridge 디버깅: step 구조 파악, 승인 버튼, AI 텍스트 릴레이 | `0c3d6cd` | ✅ |
| 3 | 05:30 | 모든 WAITING step relay 구현 + Step type 전체 매핑 (775 steps, 17 types) | - | ✅ |
| 4 | 06:10 | 채널 등록 자동화 (writeRegistration) + Bot 파이프라인 검증 | - | ✅ |
| 5 | 06:30 | **근본 원인 발견**: getDiagnostics.lastStepIndex stale 문제 | - | ✅ |
| 6 | 06:45 | SDK 소스 전체 분석 (antigravity-sdk v1.6.0 — EventMonitor, CascadeManager) | - | ✅ |
| 7 | 06:55 | **PRIMARY RELAY 재작성** — rawRPC 직접 5초 폴링으로 전환 | - | 🔧 |
| 8 | 07:30 | **GetAllCascadeTrajectories** 기반 릴레이 — NOTIFY/TASK 정상 동작 확인 | `854f33b` | ✅ |
| 9 | 07:50 | SDK EventMonitor 제거 — ERR_CONNECTION_REFUSED 원인 차단 (-404 lines) | `f6ae9c8` | ✅ |
| 10 | 08:00 | GetCascadeTrajectorySteps 완전 제거 + stall-based WAITING 감지 | `9b9c9c7` | ✅ |
| 11 | 08:10 | Stall 감지 calibration + VS Code 명령어 기반 승인 핸들러 | `f1f9a0b` | 🔧 |
| 12 | 11:30~14:35 | 승인 로직 정밀 디버깅: IDLE→stall 전환, lastModifiedTime 구분, RPC/Commands 전수 테스트, ResolveOutstandingSteps cancel 발견 | - | 🔧 |
| 13 | 15:00~16:52 | Multi-window 격리 (v0.3.1→0.3.4): 세션 필터, per-project 포트, 등록 경쟁 조건 수정, DOM Observer 렌더러 디버깅 | - | 🔧 |
| 14 | 17:01~17:38 | **근본 원인 발견**: product.json 체크섬 불일치 → vscode-file:// 원본 캐시 서빙. 체크섬 수동 업데이트로 수정 | - | 🔧 |
| 15 | 17:50~18:30 | **v0.3.5**: 포트 디스커버리 수정 (결정론적 포트 + 하드코딩), 인라인 스크립트 전환 (`<script src>``<script>inline</script>`), product.json 자동 체크섬 업데이트 | - | 🔧 |
| 16 | 19:00~19:48 | 렌더러 스크립트 로딩 디버깅: sync XHR→async fetch 변환, 설치경로 불일치 발견, vscode-file:// 커스텀 파일 서빙 불가 확인, Electron 풀 재시작 필요 발견 | - | 🔧 |
| 17 | 19:53~20:00 | **AG 재시작 성공**: GB Observer Bridge connected (port 34332), Allow Once/Allow This Conversation 감지 정상 동작 확인 | - | ✅ |
| 18 | 20:00~20:15 | **승인 감지 최적화**: latestToolCallStep 즉시 감지 (30초→5초), DOM scan 범위 확장 (Accept all/Reject all), stall→100초 fallback | - | 🔧 |
| 19 | 21:30~22:55 | **E2E 디버깅**: response 파일 race condition 수정, Run 버튼 regex 패턴 수정(`^Run$``^Run`), renderer 스크립트 소스 혼동 발견(3곳), Run 버튼은 webview iframe 내부로 DOM observer 접근 불가 확인 | - | 🔧 |

16
docs/devlog/2026-03-09.md Normal file
View File

@@ -0,0 +1,16 @@
# 2026-03-09 Devlog
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|---|------|----------|------|------|
| 001 | 08:00~09:17 | 승인 실행 메커니즘 연구 + step-type별 VS Code 명령 분기 구현 | included in 002 | ✅ |
| 002 | 09:21~15:07 | SDK 승인 명령 미등록 확정 + Renderer DOM Click 구현 | `4497e96` | ✅ |
| 003 | 15:32~17:59 | Renderer v3 deep DOM traversal (iframe/webview/shadow 관통) | `32bf5ae` | ✅ |
| 004 | 18:08~18:23 | Deep inspect HTTP endpoint (/deep-inspect) + 렌더러 재귀 인스펙터 | `a07d9d3` | ✅ |
| 005 | 18:30~19:28 | workbench.html inline v3 패치 누락 수정 + pre-patch 검증 | `b61cff1` | ✅ |
| 006 | 19:38~19:56 | V8 CachedData 진단 + 캐시 삭제 (renderer 미실행 근본 원인) | docs only | ✅ |
| 007 | 20:04~20:28 | CSP script-src `'unsafe-inline'` 패치 (renderer 미실행 진짜 근본 원인) | `08077e8` | ✅ |
| 008 | 21:00~21:30 | **E2E 승인 플로우 성공 검증** — AG 재시작 후 renderer v3 실행 확인 + Discord 승인→명령 실행 | `520d36e` | ✅ |
| 009 | 21:33~22:28 | 승인 플로우 튜닝 — dedup + 텍스트 정제 + stall fallback 제거 + reject 안전화 | `18b3734` | ✅ |
| 010 | 22:38~23:10 | E2E 검증 + Retry/Dismiss/Reject all 버튼 패턴 추가 + V8 캐시 삭제 | `4ba65f9` | ✅ |
| 011 | 23:11~23:20 | agent_guide 템플릿 통합 — 워크플로우 교체 + 플레이스홀더 적용 + 중복 helper 정리 | `4ba65f9` | ✅ |
| 012 | 23:30~00:31 | 승인 플로우 안정화 — pending 누적/false positive/MERGE dedup/auto_resolve/timeout | `` | 🔧 |

19
docs/devlog/2026-03-10.md Normal file
View File

@@ -0,0 +1,19 @@
# 2026-03-10 Devlog
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|---|------|----------|------|------|
| 001 | 06:12~06:30 | Discord 승인 ENOENT race condition 수정 + 버튼 그룹화 (multi-choice) | `aab1cfb` | 🔧 |
| 002 | 21:00~02:30 | 승인 메시지 전문 표시 + 연속 승인 감지 + file_permission scope 라우팅 | `75a3482`~`c9b4fd4` | ✅ |
| 003 | 12:30~13:30 | 3버튼 UI + Run 중복 필터 + dedup + 인자 값 표시 | `14d2acf`~`47dbd38` | ✅ |
| 004 | 13:30~13:55 | auto_resolved 동기화 + expired 카드 업데이트 + DOM step_index | `048ffd9` | ✅ |
| 005 | 13:55~14:10 | #253 전체 대화 릴레이 — 사용자 메시지 + 에러 알림 | `17dd665`~`b500120` | ✅ |
| 006 | 14:00~15:00 | Discord 에코필터 + 리로드 재전송 방지 + diff review 알림 | `82b727a`~`8fbf6bf` | ✅ |
| 007 | 15:00~15:55 | step_type 패스스루 체인 수정 + file_permission 자동감지 | `7982263`~`d1586c5` | ✅ |
| 008 | 16:45~17:20 | Single active project lock + stale REJECT 필터 + Vikunja 태스크 정리 | `186875a`~`95d4f85` | ✅ |
| 009 | 17:20~17:47 | v0.3.6 릴리스 — VSIX 빌드 + start_bot.bat 런처 | `bd46bea` | ✅ |
| 010 | 18:00~18:30 | v0.3.7 — file_permission 3-button 주입 + active_project.lock 제거 (멀티프로젝트) | `27deb2a` | ✅ |
| 011 | 18:50~19:29 | v0.3.8 — workspace URI 기반 세션 필터링 (멀티프로젝트 격리 완성) | `ae91134` | ✅ |
| 012 | 19:30~20:35 | 크로스 프로젝트 response watcher 우회 수정 + file_permission write 도구 3-button 매핑 | `3b834e0` | ✅ |
| 013 | 21:04~22:19 | Deriva 신호 진단 + RUNNING 세션 우선 선택 + IDLE 채널 자동 생성 제거 | `6179c4d` | ✅ |
| 014 | 22:23~22:47 | SDK LS 프로세스 대소문자 매칭 버그 수정 — variet-agent 신호 미도달 해결 | `21fd309` | ✅ |
| 015 | 23:46~23:57 | v0.3.9 — SDK JS 파일 VSIX 미포함 수정 + start_bot.bat Python 경로 우선순위 | `71aa80d` | ✅ |

14
docs/devlog/2026-03-11.md Normal file
View File

@@ -0,0 +1,14 @@
# 2026-03-11 Devlog
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|---|------|----------|------|------|
| 001 | 00:00~00:20 | Discord 릴레이 미작동 진단 — config.py BRAIN_PATH 빈문자열 버그 수정 | `pending` | ✅ |
| 002 | 00:20~01:05 | 크로스 프로젝트 pending DEDUP MERGE 버그 진단 및 수정 (project_name 가드 3곳) | `pending` | ✅ |
| 003 | 09:25~09:33 | Auto-approve 기능 감사 (미구현 확인) + Vikunja 태스크 #304, #305 등록 | `pending` | ✅ |
| 004 | 10:00~10:35 | P1: `!auto` 토글 자동 승인 구현 (bot.py + extension.ts) | `pending` | ✅ |
| 005 | 10:35~10:45 | P2: BridgeTransport 추상화 (bridge.py 리팩토링 + config/main 모드 설정) | `pending` | ✅ |
| 006 | 10:43~10:55 | 사용 가이드 작성 (docs/usage-guide.md) + tech-stack.md Python 경로 기록 | `c130399` | ✅ |
| 007 | 19:28~19:35 | Gateway HTTP API + Docker (Dockerfile, docker-compose, Caddyfile) | `6dbbb57` | ✅ |
| 008 | 19:35~19:50 | Gateway 보안: API Key 인증 미들웨어 + Caddy HTTPS + .env.example | `95da3e9` | ✅ |
| 009 | 19:50~20:10 | RemoteTransport + CollectorBridge 구현 — Collector↔Gateway HTTP 통신 | `95c2905` | ✅ |
| 010 | 21:30~23:48 | 아키텍처 감사: aiohttp 전환 + 보안 + 기능 누락 수정 + 나노 검증 | `d7ed454` | ✅ |

View File

@@ -0,0 +1,9 @@
# 2026-03-12 Devlog
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|---|------|----------|------|------|
| 001 | 00:34~00:47 | 429 Rate Limit 무한 루프 디버깅 — 지수 백오프 + rate limit 완화 + Collector 폴링 보호 | `d9b36cf` | ✅ |
| 002 | 16:45~17:04 | workbench.html 0-byte 파괴 복구 — 멀티 인스턴스 race condition 방지 안전 가드 추가 | `a9feee6` | ✅ |
| 003 | 17:10~17:55 | workbench.html 크로스 복원 CSS 깨짐 수정 — pre-patch backup + requiredMarker 구조 검증 + .orig 자동 복원 | `6d8c6f1` | ✅ |
| 004 | 19:46~21:13 | Collector 멀티 프로젝트 command 폴링 버그 수정 + rate limit burst throttle | `ae51d28` `bcc29f9` | ✅ |
| 005 | 22:12~22:58 | Rate limit 구조적 수정 — 점진적 백오프 + adaptive 폴링 + burst-friendly 윈도우 + stale pending 정리 | `56de714` | 🔧 |

View File

@@ -0,0 +1,6 @@
# Devlog — 2026-03-13
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 08:56 | Discord 아티팩트 알림 개선 — truncation 확대, 파일 첨부 전송, 동적 .md 감시 | `e5a05e3` | ✅ |
| 002 | 19:53 | Collector 성능 최적화 — mtime 프리체크, 프로젝트 캐시, re-forward 수정, 폴링 간격 조정 | `d4a2016` | ✅ |

13
docs/devlog/2026-03-15.md Normal file
View File

@@ -0,0 +1,13 @@
# 2026-03-15 Devlog
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 07:00~08:16 | 승인 신호 누락 진단 & 5건 버그 수정 (DEDUP collision, fs.watch fail, default 보호, auto 확인, msg dedup) | `40e3cd5` | ✅ |
| 002 | 08:25~08:31 | Extension v0.3.10 버전 범프 & VSIX 빌드 | `10caae1` | ✅ |
| 003 | 10:00~10:41 | 승인 라이프사이클 race condition 4건 수정 (HTML lock, pending status skip, auto_resolve Discord 알림, Bot approval_messages) | `f962036` | ✅ |
| 004 | 10:41~10:53 | 성능 최적화 3건 (pollResponseGroup 1500ms, renderer adaptive idle, Bot single-pass scanner) + VSIX 빌드 | `ae0509f` | ✅ |
| 005 | 15:17~17:09 | 크로스 프로젝트 신호 오염 진단 & 승인 플로우 아키텍처 수정 — DEDUP project_name 가드, double-fire auto-approve 제거, 실패 RPC 전략 30+개 삭제 (v0.3.11) | `6739f8f` | ✅ |
| 006 | 18:32~18:51 | Auto-approve 크래시 수정 — DOM Observer Deny false positive 필터 + Bot reject-word 가드 + AGENT.md 규칙 #10 추가 | `5e5f515` | ✅ |
| 007 | 22:00~22:52 | 시스템 전체 감사 + 5개 파일 버그 수정 (PATS Deny 트리거 제거, auto_resolved 채팅 병합, UUID 파일명 충돌방지, IP rate limit 누수, bot.py deque) + VSIX 빌드/배포 | `c9f44af` | ✅ |
| 008 | 23:18~23:27 | AGENT.md 로컬적 사고 방지 규칙 추가 — NEVER #10 강화(반증 의무), NEVER #11(기계적 적용 금지), ALWAYS #9(프로젝트 이력 교차 참조), Bug Report Protocol 분리 | `9b93ee9` | ✅ |

12
docs/devlog/2026-03-16.md Normal file
View File

@@ -0,0 +1,12 @@
# 2026-03-16 Devlog
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 07:30~11:10 | 승인 상태 관리 근본 원인 분석 + v0.3.12 수정 (sawRunningAfterPending gate) + approval-flow.md 시스템 Flow 문서 + known-issues 2건 추가 | `2d9fe96` | ✅ |
| 002 | 13:25~14:20 | diff_review 핸들러 2-strategy 리팩토링 + 배포 불일치 발견/수정 + pending 순서 8초 지연 + 1차 테스트 (버튼 OK, RPC 미배포→재배포) + known-issues 2건 | `f302984` | ✅ |
| 003 | 15:18~16:55 | diff_review steps=[] 근본 원인 분석 + 인메모리 캐시 (v0.3.13) + 3차 E2E (RPC SUCCESS but no-op) + 4가지 파라미터 실험 배포 | `00b9491` | ✅ |
| 004 | 17:05~18:00 | AG 소스 역분석 — `AcknowledgeCascadeCodeEdit``acknowledgeCodeActionStep` 메서드명 오류 발견 + v0.3.14 3단계 전략 배포 + known-issues 2건 업데이트 | `5a1d4f0` | ✅ |
| 005 | 18:13~18:43 | v0.3.14 E2E 테스트 → RPC 3개 전략 모두 실패 확인 + v0.3.15 agentAcceptAllInFile 전환 배포 + known-issues 업데이트 | `0fdf668` | ✅ |
| 006 | 18:47~19:09 | v0.3.15 diff_review E2E 2회 성공 + 이중 승인 수정 + IDLE 종료 알림 + !auto 이중 메시지 수정 (v0.3.16) + known-issues 2건 | `3cd7122` | ✅ |
| 007 | 19:17~20:38 | Discord 알림 누락 디버깅 — Bot snapshot 로깅 추가 + 병렬 WAITING step break 제거 + 서버 Docker 재배포 3회 + known-issues 2건 | `7f079a5` | ✅ |
| 008 | 20:50~23:06 | 크로스 프로젝트 알림 폭주 + pending 139개 누적 + diff_review brain/ 거짓양성 — 근본 원인 6건 분석 + Watcher 프로젝트 필터 + Collector stale 정리 + Extension brain/ 제외 + known-issues 3건 | `e3f8fb9` | ✅ |

Some files were not shown because too many files have changed in this diff Show More