Compare commits

...

163 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
134 changed files with 13954 additions and 8593 deletions

View File

@@ -1,35 +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 모드**로 무인 작업 지원
## 디렉토리
### 시스템
```
project-root/
├── src/ # 소스 코드
├── tests/ # 테스트
├── docs/ # 문서
├── .agents/ # AI 에이전트 설정
└── ...
┌────────────────┐ 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 # 윈도우용 봇 시작 스크립트
```
## 데이터 흐름
---
<!-- 주요 데이터 흐름을 Mermaid 다이어그램이나 텍스트로 설명 -->
## 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

@@ -4,20 +4,37 @@
## 네이밍
### Python (서버)
| 대상 | 규칙 | 예시 |
|------|------|------|
| 변수/함수 | camelCase | `getUserData()` |
| 클래스 | PascalCase | `UserService` |
| 상수 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
| 파일명 | kebab-case | `user-service.js` |
| CSS 클래스 | kebab-case | `.nav-header` |
| 변수/함수 | 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()` |
## 코드 스타일
- 들여쓰기: (2 spaces / 4 spaces / tab)
- 세미콜론: (사용 / 미사용)
- 따옴표: (single / double)
- 줄바꿈: LF (Unix style)
| 항목 | Python | TypeScript |
|------|--------|-----------|
| 들여쓰기 | 4 spaces | 4 spaces |
| 따옴표 | 쌍따옴표 `"` (f-string 포함) | 작은따옴표 `'` |
| 세미콜론 | N/A | 사용 |
| 줄바꿈 | LF (Unix) | CRLF (Windows, git 자동 변환) |
| 최대 줄 길이 | 120자 권장 | 120자 권장 |
| 타입 힌트 | 적극 사용 (`-> list[str]`) | strict (`BridgeContext` 인터페이스) |
## 커밋 메시지
@@ -25,21 +42,84 @@
<type>(<scope>): <description>
type: feat|fix|refactor|test|docs|chore|ci|infra
scope: (선택)
scope: server|extension|hub|bot|gateway|bridge (선택)
```
**예시:**
- `feat(server): add WebSocket reconnection logic`
- `fix(frontend): resolve button overlap on mobile`
- `docs: update API documentation`
- `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) |
## 테스트
- 테스트 파일 위치: (예: `__tests__/` 또는 `*.test.js`)
- 테스트 네이밍: `should [expected behavior] when [condition]`
| 항목 | 위치 | 도구 |
|------|------|------|
| 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` 확인

File diff suppressed because it is too large Load Diff

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

@@ -5,42 +5,95 @@
## 언어 & 런타임
| 항목 | 버전 | 경로/비고 |
|------|------|-----------|
| Python | 3.x (miniforge3) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` |
|------|------|-----------|
| Python | 3.12 (miniforge3) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` |
| Node.js | 시스템 설치 | `node`, `npm` (PowerShell에서 `cmd /c npm` 권장) |
| TypeScript | (Extension) | `extension/src/extension.ts``tsc` 빌드 |
| TypeScript | 5.3+ | `extension/src/*.ts``tsc` `extension/out/*.js` |
> [!IMPORTANT]
> Python은 **반드시** 위 miniforge3 경로를 사용. WindowsApps의 python stub은 동작하지 않음.
## 프레임워크
## 프레임워크 & 라이브러리
| 항목 | 버전 | 용도 |
|------|------|------|
| discord.py | 2.x | Discord 봇 |
| watchdog | - | 파일시스템 감시 |
| antigravity-sdk | 로컬 | VS Code Extension SDK 연동 |
### 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`)
| 측 | 도구 | 파일 |
|----|------|------|
| Python | pip | `requirements.txt` |
| Extension | npm | `extension/package.json` |
## 개발 도구
## 개발 도구 & 명령어
| 도구 | 명령어 |
| 작업 | 명령어 |
|------|--------|
| **봇 실행** | `start_bot.bat` 또는 `C:\ProgramData\miniforge3\envs\gravity_control\python.exe main.py` |
| **Extension 빌드** | `cd extension && cmd /c npm run compile` |
| **Extension VSIX** | `cd extension && cmd /c npx vsce package` |
| **봇 구문 검사** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe -c "import bot, bridge, config, main"` |
| **봇 실행** | `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/remote) | `local` |
| REMOTE_BRIDGE_URL | 원격 브릿지 URL | (remote 모드 전용) |
| 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,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

@@ -17,12 +17,15 @@ ACTIVE_TIMEOUT_SECONDS=300
# Watcher Settings
DEBOUNCE_SECONDS=2
# Bot mode: 'local' (default, file-based) or 'gateway' (서버 Docker)
# Bot mode: 'local' (default, file-based) or 'gateway' (서버 Docker + WS Hub)
BOT_MODE=local
# Remote bridge URL (only used when BOT_MODE=remote)
REMOTE_BRIDGE_URL=
# 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=

BIN
.gitlog.txt Normal file

Binary file not shown.

View File

@@ -1,9 +0,0 @@
# Gravity Gateway — Caddy Reverse Proxy
# Automatic HTTPS via Let's Encrypt
#
# 도메인을 실제 도메인으로 변경하세요 (예: gateway.variet.net)
# Caddy가 자동으로 Let's Encrypt 인증서를 발급합니다.
gateway.variet.net {
reverse_proxy gateway:8585
}

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")

890
bot.py

File diff suppressed because it is too large Load Diff

479
bridge.py
View File

@@ -1,479 +0,0 @@
"""Bridge protocol — 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
Transport layer:
LocalTransport — file-based (default, single-PC)
RemoteTransport — HTTP-based (future: multi-PC collector mode)
"""
import json
import time
import logging
import uuid
from abc import ABC, abstractmethod
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
step_type: str = "" # e.g. 'diff_review', passed through to response
@dataclass
class UserResponse:
"""A user response from Discord."""
request_id: str
approved: bool
user_input: str = ""
timestamp: float = 0
button_index: int = -1 # -1 = legacy (approve/reject), 0+ = specific button index
step_type: str = "" # pass through from pending for extension routing
project_name: str = "" # for multi-project: extension uses this when pending file is missing
# ─── Transport Abstraction ───
class BridgeTransport(ABC):
"""Abstract transport for bridge I/O.
Implementations handle reading/writing JSON files for the bridge protocol,
regardless of whether the storage is local filesystem or remote HTTP.
"""
@abstractmethod
def list_json_files(self, subdir: str) -> list[str]:
"""List JSON filenames in a subdirectory (e.g. 'pending', 'response')."""
...
@abstractmethod
def read_json(self, subdir: str, filename: str) -> dict | None:
"""Read and parse a JSON file. Returns None if not found or corrupt."""
...
@abstractmethod
def write_json(self, subdir: str, filename: str, data: dict) -> None:
"""Write data as JSON to a file in the given subdirectory."""
...
@abstractmethod
def delete_file(self, subdir: str, filename: str) -> bool:
"""Delete a file. Returns True if deleted, False if not found."""
...
@abstractmethod
def ensure_dirs(self) -> None:
"""Ensure all required subdirectories exist."""
...
class LocalTransport(BridgeTransport):
"""File-system based transport (default, single-PC mode).
Reads/writes directly to the bridge directory on local disk.
This is the existing behavior, extracted into a transport class.
"""
def __init__(self, bridge_dir: Path):
self.bridge_dir = bridge_dir
def list_json_files(self, subdir: str) -> list[str]:
d = self.bridge_dir / subdir
if not d.exists():
return []
return [f.name for f in d.glob("*.json")]
def read_json(self, subdir: str, filename: str) -> dict | None:
fp = self.bridge_dir / subdir / filename
if not fp.exists():
return None
try:
return json.loads(fp.read_text(encoding="utf-8-sig"))
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"LocalTransport: bad file {subdir}/{filename}: {e}")
return None
def write_json(self, subdir: str, filename: str, data: dict) -> None:
d = self.bridge_dir / subdir
d.mkdir(parents=True, exist_ok=True)
fp = d / filename
fp.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def delete_file(self, subdir: str, filename: str) -> bool:
fp = self.bridge_dir / subdir / filename
if fp.exists():
try:
fp.unlink()
return True
except OSError:
return False
return False
def ensure_dirs(self) -> None:
for sub in ("pending", "response", "commands"):
(self.bridge_dir / sub).mkdir(parents=True, exist_ok=True)
class RemoteTransport(BridgeTransport):
"""HTTP-based transport for Collector → Gateway communication.
Maps BridgeTransport methods to Gateway API endpoints:
list_json_files("pending") → GET /api/pending (returns list)
write_json("pending", ...) → POST /api/pending
read_json("response", ...) → GET /api/response/{rid}
write_json("commands", ...) → (not used by Collector, Gateway pushes commands)
etc.
"""
def __init__(self, base_url: str, api_key: str = ""):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self._headers = {"Content-Type": "application/json"}
if api_key:
self._headers["Authorization"] = f"Bearer {api_key}"
self._session = None # aiohttp.ClientSession — lazy created
# Connection health
self.connected = False
self._consecutive_failures = 0
self._max_failures_before_warning = 3
# Rate limit backoff
self._rate_limited_until = 0.0 # timestamp until which we should not send requests
self._backoff_seconds = 0.0 # current backoff duration (exponential)
self._BACKOFF_BASE = 2.0
self._BACKOFF_MAX = 60.0
self._success_streak = 0 # consecutive successes for gradual backoff reduction
# Retry queue: list of (method, path, data) tuples
self._retry_queue: list[tuple[str, str, dict | None]] = []
self._retry_queue_max = 100
logger.info(f"RemoteTransport: {self.base_url} (auth={'yes' if api_key else 'no'})")
async def _get_session(self):
"""Lazy-create aiohttp session."""
if self._session is None or self._session.closed:
import aiohttp
timeout = aiohttp.ClientTimeout(total=10)
self._session = aiohttp.ClientSession(
headers=self._headers, timeout=timeout
)
return self._session
async def close(self):
"""Close the HTTP session."""
if self._session and not self._session.closed:
await self._session.close()
@property
def is_rate_limited(self) -> bool:
"""Check if we are currently in a rate-limit backoff period."""
return time.time() < self._rate_limited_until
def _apply_backoff(self, retry_after: float = 0):
"""Apply exponential backoff for rate limiting."""
self._success_streak = 0 # Reset success streak on any failure
if retry_after > 0:
self._backoff_seconds = min(retry_after, self._BACKOFF_MAX)
else:
if self._backoff_seconds == 0:
self._backoff_seconds = self._BACKOFF_BASE
else:
self._backoff_seconds = min(self._backoff_seconds * 2, self._BACKOFF_MAX)
self._rate_limited_until = time.time() + self._backoff_seconds
logger.warning(f"RemoteTransport: backing off {self._backoff_seconds:.0f}s (until +{self._backoff_seconds:.0f}s)")
def _on_request_success(self):
"""Gradually reduce backoff after consecutive successes.
Instead of instantly resetting to 0 (which causes the 1s oscillation loop
when 7 loops share one transport), require sustained success before reducing.
"""
if self._backoff_seconds <= 0:
return # Already at zero, nothing to do
self._success_streak += 1
if self._success_streak >= 5:
# Halve the backoff (gradual cooldown)
self._backoff_seconds = self._backoff_seconds / 2
if self._backoff_seconds < 0.5:
self._backoff_seconds = 0
self._rate_limited_until = 0
self._success_streak = 0
async def _arequest(self, method: str, path: str, data: dict | None = None) -> dict | None:
"""Async non-blocking HTTP request to Gateway API."""
# Skip if in backoff period (except health checks)
if self.is_rate_limited and path != "/health":
return None
session = await self._get_session()
url = f"{self.base_url}{path}"
try:
kwargs = {}
if data is not None:
kwargs["json"] = data
async with session.request(method, url, **kwargs) as resp:
if resp.status >= 400:
if resp.status == 401:
logger.error("RemoteTransport: 401 Unauthorized — check GATEWAY_API_KEY")
elif resp.status == 429:
retry_after = float(resp.headers.get("Retry-After", 0))
self._apply_backoff(retry_after)
else:
logger.warning(f"RemoteTransport: {method} {path}{resp.status}")
return None
result = await resp.json()
if not self.connected:
logger.info("RemoteTransport: ✅ Gateway connected")
self.connected = True
self._consecutive_failures = 0
self._on_request_success()
return result
except Exception as e:
self._consecutive_failures += 1
if self._consecutive_failures == self._max_failures_before_warning:
logger.error(f"RemoteTransport: ❌ Gateway unreachable ({self._consecutive_failures} failures): {e}")
elif self._consecutive_failures < self._max_failures_before_warning:
logger.warning(f"RemoteTransport: {method} {path}{e}")
self.connected = False
# Apply backoff on connection failures too
if self._consecutive_failures >= self._max_failures_before_warning:
self._apply_backoff()
return None
async def _arequest_retry(self, method: str, path: str, data: dict | None = None) -> dict | None:
"""Request with retry queue — failed POSTs are queued for later."""
result = await self._arequest(method, path, data)
if result is None and method == "POST" and data is not None:
if len(self._retry_queue) < self._retry_queue_max:
self._retry_queue.append((method, path, data))
return result
async def flush_retry_queue(self):
"""Retry queued failed requests."""
if not self._retry_queue or not self.connected:
return
queue = self._retry_queue[:]
self._retry_queue.clear()
succeeded = 0
for method, path, data in queue:
result = await self._arequest(method, path, data)
if result is None:
if len(self._retry_queue) < self._retry_queue_max:
self._retry_queue.append((method, path, data))
break
succeeded += 1
if succeeded:
logger.info(f"[RETRY] flushed {succeeded}/{len(queue)} queued requests")
async def health_check(self) -> bool:
"""Check if Gateway is reachable."""
result = await self._arequest("GET", "/health")
return result is not None and result.get("status") == "ok"
# ─── Async methods (used by Collector) ───
async def awrite_json(self, subdir: str, filename: str, data: dict) -> None:
if subdir == "pending":
await self._arequest_retry("POST", "/api/pending", data)
elif subdir == "response":
rid = data.get("request_id", filename.replace(".json", ""))
await self._arequest_retry("POST", f"/api/response/{rid}", data)
async def aread_json(self, subdir: str, filename: str) -> dict | None:
rid = filename.replace(".json", "")
if subdir == "response":
return await self._arequest("GET", f"/api/response/{rid}")
return None
async def apoll_commands(self, project: str) -> list[dict]:
result = await self._arequest("GET", f"/api/commands/{project}")
if result and isinstance(result, dict):
return result.get("commands", [])
return []
async def aregister_session(self, conv_id: str, project: str) -> None:
await self._arequest_retry("POST", "/api/register", {
"conversation_id": conv_id, "project_name": project,
})
async def asend_chat(self, project: str, content: str, *, attached_files: list[dict] | None = None) -> None:
payload: dict = {"project_name": project, "content": content}
if attached_files:
payload["attached_files"] = attached_files
await self._arequest_retry("POST", "/api/chat", payload)
async def asend_event(self, event_data: dict) -> None:
await self._arequest_retry("POST", "/api/event", event_data)
# ─── Sync stubs (ABC compliance, not used in Collector) ───
def list_json_files(self, subdir: str) -> list[str]:
return []
def read_json(self, subdir: str, filename: str) -> dict | None:
return None
def write_json(self, subdir: str, filename: str, data: dict) -> None:
pass
def delete_file(self, subdir: str, filename: str) -> bool:
return True
def ensure_dirs(self) -> None:
pass
# ─── Bridge Protocol (uses Transport) ───
class BridgeProtocol:
"""Manages the bridge protocol via a pluggable transport."""
def __init__(self, transport: BridgeTransport | None = None):
if transport is None:
bridge_dir = Config.BRAIN_PATH.parent / "bridge"
transport = LocalTransport(bridge_dir)
self.transport = transport
# Legacy attributes for backward compatibility
# (bot.py uses self.bridge.pending_dir etc. in some places)
if isinstance(transport, LocalTransport):
self.bridge_dir = transport.bridge_dir
self.pending_dir = transport.bridge_dir / "pending"
self.response_dir = transport.bridge_dir / "response"
self.commands_dir = transport.bridge_dir / "commands"
# Ensure directories exist
self.transport.ensure_dirs()
# Startup cleanup: purge stale pending files (> 5 min old)
self._cleanup_stale_pending()
logger.info(f"Bridge protocol initialized: transport={type(transport).__name__}")
def _cleanup_stale_pending(self, max_age_seconds: int = 300):
"""Remove pending files older than max_age_seconds on startup."""
now = time.time()
cleaned = 0
for fname in self.transport.list_json_files("pending"):
data = self.transport.read_json("pending", fname)
if data is None:
self.transport.delete_file("pending", fname)
cleaned += 1
continue
ts = data.get("timestamp", 0)
if now - ts > max_age_seconds:
self.transport.delete_file("pending", fname)
cleaned += 1
if cleaned:
logger.info(f"Startup cleanup: removed {cleaned} stale pending files")
def get_pending_requests(self) -> list[ApprovalRequest]:
"""Read all pending approval requests. Skips files older than 30 minutes."""
requests = []
fields = {f.name for f in ApprovalRequest.__dataclass_fields__.values()}
now = time.time()
MAX_AGE = 1800 # 30 minutes (matches Discord button timeout)
CLEANUP_AGE = 86400 # 1 day
for fname in self.transport.list_json_files("pending"):
data = self.transport.read_json("pending", fname)
if data is None:
continue
ts = data.get("timestamp", 0)
if now - ts > CLEANUP_AGE:
# Too old even to keep as expired — delete to prevent accumulation
self.transport.delete_file("pending", fname)
continue
if now - ts > MAX_AGE:
# Too old — mark expired and skip
if data.get("status") != "expired":
data["status"] = "expired"
self.transport.write_json("pending", fname, data)
continue
if data.get("status") == "pending":
# Filter to known fields only
filtered = {k: v for k, v in data.items() if k in fields}
try:
requests.append(ApprovalRequest(**filtered))
except TypeError as e:
logger.warning(f"Bad pending request {fname}: {e}")
return requests
def read_pending_request(self, request_id: str) -> ApprovalRequest | None:
"""Re-read a specific pending request (to get merged data)."""
fname = f"{request_id}.json"
data = self.transport.read_json("pending", fname)
if data is None:
return None
fields = {fn.name for fn in ApprovalRequest.__dataclass_fields__.values()}
filtered = {k: v for k, v in data.items() if k in fields}
try:
return ApprovalRequest(**filtered)
except TypeError:
return None
def write_response(self, response: UserResponse):
"""Write a user response to the response directory."""
response.timestamp = time.time()
fname = f"{response.request_id}.json"
self.transport.write_json("response", fname, asdict(response))
logger.info(f"Response written: {fname} (approved={response.approved})")
# Delete pending file after processing (prevents re-processing and accumulation)
self.transport.delete_file("pending", fname)
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)}_{uuid.uuid4().hex[:8]}"
fname = f"{cmd_id}.json"
data = {
"id": cmd_id,
"conversation_id": conversation_id,
"project_name": project_name,
"text": text,
"timestamp": time.time(),
"consumed": False,
}
self.transport.write_json("commands", fname, data)
logger.info(f"Command written: {cmd_id} → project={project_name}")
return cmd_id

View File

@@ -1,461 +0,0 @@
"""Collector — local relay between Extension (file-based) and Gateway (HTTP).
The Collector runs on the local PC alongside the AG IDE.
It bridges the gap between the Extension (which writes to local bridge/ files)
and the remote Gateway (which manages Discord).
Flow:
Extension → bridge/pending/ → Collector → POST Gateway /api/pending
Gateway /api/response/{rid} → Collector → bridge/response/ → Extension
Gateway /api/commands/{project} → Collector → bridge/commands/ → Extension
"""
import asyncio
import hashlib
import json
import os
import time
import logging
import uuid
from pathlib import Path
from bridge import LocalTransport, RemoteTransport
from config import Config
from watcher import BrainEvent, EventType
logger = logging.getLogger(__name__)
class CollectorBridge:
"""Bridges local file-based bridge with remote Gateway API.
Periodically:
1. Scans local pending/ → forwards new ones to Gateway
2. Polls Gateway for responses → writes to local response/
3. Polls Gateway for commands → writes to local commands/
"""
def __init__(self, local: LocalTransport, remote: RemoteTransport,
project_name: str, event_queue: asyncio.Queue | None = None):
self.local = local
self.remote = remote
self.project_name = project_name
self.event_queue = event_queue
self._poll_interval = 5 # seconds (was 3 — reduced I/O frequency)
self._running = False
# Pre-populate with existing pending files → skip on startup (prevents 만료됨 spam)
self._startup_pending: set[str] = set()
self._forwarded_pending: set[str] = set()
self._forwarded_timestamps: dict[str, float] = {} # rid → when forwarded
self._pending_hashes: dict[str, str] = {} # rid → content hash (for MERGE/status detection)
self._pending_mtimes: dict[str, float] = {} # rid → last known file mtime
self._RESPONSE_POLL_TTL = 300 # 5 min — stop polling responses for old pending
# Project discovery cache (avoid re-reading register/ every cycle)
self._cached_projects: set[str] | None = None
self._projects_cache_ts: float = 0
self._PROJECTS_CACHE_TTL = 60.0 # seconds
for fname in self.local.list_json_files("pending"):
rid = fname.replace(".json", "")
self._startup_pending.add(rid)
self._forwarded_pending.add(rid)
# Pre-hash existing files
data = self.local.read_json("pending", fname)
if data:
self._pending_hashes[rid] = hashlib.md5(
json.dumps(data, sort_keys=True).encode()
).hexdigest()
# Pre-cache mtime
try:
fpath = self.local.bridge_dir / "pending" / fname
self._pending_mtimes[rid] = fpath.stat().st_mtime
except OSError:
pass
if self._startup_pending:
logger.info(f"[COLLECTOR] skipping {len(self._startup_pending)} existing pending files")
# Startup cleanup: remove stale response files (> 5 min)
self._cleanup_stale_responses()
def _cleanup_stale_responses(self, max_age: int = 300):
"""Remove stale response files (> max_age seconds) on startup."""
now = time.time()
cleaned = 0
for fname in self.local.list_json_files("response"):
try:
fpath = self.local.bridge_dir / "response" / fname
if now - fpath.stat().st_mtime > max_age:
self.local.delete_file("response", fname)
cleaned += 1
except OSError:
pass
if cleaned:
logger.info(f"[COLLECTOR] startup cleanup: removed {cleaned} stale response files")
async def start(self):
"""Start the Collector polling loops with staggered offsets.
Each loop starts with a different delay to prevent all loops from waking
up at the same time and causing burst requests to Gateway.
"""
self._running = True
logger.info(f"[COLLECTOR] started for project={self.project_name}")
async def _staggered(coro, offset: float):
await asyncio.sleep(offset)
await coro()
tasks = [
_staggered(self._forward_pending_loop, 0.0),
_staggered(self._poll_responses_loop, 0.5),
_staggered(self._poll_commands_loop, 1.0),
_staggered(self._forward_chat_snapshots_loop, 1.5),
_staggered(self._forward_registrations_loop, 2.0),
_staggered(self._health_check_loop, 2.5),
_staggered(self._retry_flush_loop, 3.0),
]
if self.event_queue:
tasks.append(_staggered(self._forward_events_loop, 3.5))
await asyncio.gather(*tasks)
async def stop(self):
"""Stop the Collector and close HTTP session."""
self._running = False
await self.remote.close()
logger.info("[COLLECTOR] stopped")
# ─── Forward local pending → Gateway ───
async def _forward_pending_loop(self):
"""Scan local pending/ and forward new + updated requests to Gateway.
Tracks content hashes to detect:
- New pending files → forward immediately
- MERGE updates (step_probe updates command text) → re-forward
- Status changes (auto_resolved, expired) → re-forward
"""
while self._running:
try:
# Skip cycle if rate-limited
if self.remote.is_rate_limited:
await asyncio.sleep(self._poll_interval)
continue
current_files = set()
for fname in self.local.list_json_files("pending"):
rid = fname.replace(".json", "")
current_files.add(rid)
# mtime pre-check: skip read+hash if file hasn't been modified
try:
fpath = self.local.bridge_dir / "pending" / fname
current_mtime = fpath.stat().st_mtime
except OSError:
continue
prev_mtime = self._pending_mtimes.get(rid)
if prev_mtime is not None and current_mtime == prev_mtime:
continue # File untouched since last check — skip read+hash
self._pending_mtimes[rid] = current_mtime
data = self.local.read_json("pending", fname)
if data is None:
continue
# Compute content hash to detect changes
content_hash = hashlib.md5(
json.dumps(data, sort_keys=True).encode()
).hexdigest()
prev_hash = self._pending_hashes.get(rid)
if prev_hash == content_hash:
continue # No change
is_new = rid not in self._forwarded_pending
if rid in self._startup_pending:
# Startup files: only forward status CHANGES (not re-forward as new pending)
status = data.get("status", "pending")
if status == "pending":
continue # Still pending from before startup — skip
# Status changed (auto_resolved/expired) — forward the update
# Forward to Gateway (new or updated)
await self.remote.awrite_json("pending", fname, data)
self._forwarded_pending.add(rid)
self._forwarded_timestamps[rid] = time.time()
self._pending_hashes[rid] = content_hash
if is_new:
logger.info(f"[COLLECTOR] → Gateway: pending {rid[:12]}")
else:
status = data.get("status", "?")
logger.info(f"[COLLECTOR] → Gateway: pending UPDATE {rid[:12]} status={status}")
# Clean up tracking for deleted files
for rid in list(self._forwarded_pending):
if rid not in current_files and rid not in self._startup_pending:
self._forwarded_pending.discard(rid)
self._pending_hashes.pop(rid, None)
self._pending_mtimes.pop(rid, None)
# Also clean up orphaned hashes/mtimes for files no longer on disk
for rid in list(self._pending_hashes):
if rid not in current_files:
self._pending_hashes.pop(rid, None)
self._pending_mtimes.pop(rid, None)
except Exception as e:
logger.error(f"[COLLECTOR] forward_pending error: {e}")
await asyncio.sleep(self._poll_interval)
# ─── Poll Gateway responses → local ───
async def _poll_responses_loop(self):
"""Poll Gateway for responses and write them locally for Extension.
Only polls responses for recently-forwarded pending (within _RESPONSE_POLL_TTL).
Expired entries are removed from tracking to prevent request accumulation.
"""
while self._running:
try:
# Skip cycle if rate-limited
if self.remote.is_rate_limited:
await asyncio.sleep(self._poll_interval)
continue
now = time.time()
# Clean up expired forwarded pending (stop polling responses for old ones)
expired = [
rid for rid, ts in self._forwarded_timestamps.items()
if now - ts > self._RESPONSE_POLL_TTL
]
for rid in expired:
self._forwarded_pending.discard(rid)
self._forwarded_timestamps.pop(rid, None)
# NOTE: intentionally keep _pending_hashes[rid] to prevent
# re-forward cycle (expired pending would be re-detected as
# "new" if hash is cleared). Hash is cleaned up when file
# is actually deleted from disk (see _forward_pending_loop).
if expired:
logger.info(f"[COLLECTOR] expired {len(expired)} stale forwarded pending (>{self._RESPONSE_POLL_TTL}s)")
# Check each active forwarded pending for a response
active_rids = [
rid for rid in self._forwarded_pending
if rid not in self._startup_pending
]
for rid in active_rids:
# Rate-limit guard: stop polling if we got rate-limited mid-cycle
if self.remote.is_rate_limited:
break
data = await self.remote.aread_json("response", f"{rid}.json")
if data is None or data.get("waiting"):
await asyncio.sleep(0.3) # Throttle between individual response polls
continue
# Write response locally for Extension to pick up
self.local.write_json("response", f"{rid}.json", data)
# Also delete local pending file (Extension expects this)
self.local.delete_file("pending", f"{rid}.json")
self._forwarded_pending.discard(rid)
self._forwarded_timestamps.pop(rid, None)
approved = data.get("approved", "?")
logger.info(f"[COLLECTOR] ← Gateway: response {rid[:12]} approved={approved}")
except Exception as e:
logger.error(f"[COLLECTOR] poll_responses error: {e}")
await asyncio.sleep(self._poll_interval)
# ─── Poll Gateway commands → local ───
def _discover_local_projects(self) -> set[str]:
"""Discover all project names registered by local Extension instances.
Reads bridge/register/*.json files, which are written by each AG window's
Extension with {conversation_id, project_name}. Returns unique project names
found, always including self.project_name as a fallback.
Results are cached for _PROJECTS_CACHE_TTL seconds to avoid re-reading
22+ register files every polling cycle.
"""
now = time.time()
if self._cached_projects is not None and now - self._projects_cache_ts < self._PROJECTS_CACHE_TTL:
return self._cached_projects
projects = {self.project_name}
register_dir = self.local.bridge_dir / "register"
if not register_dir.exists():
self._cached_projects = projects
self._projects_cache_ts = now
return projects
for f in register_dir.glob("*.json"):
try:
data = json.loads(f.read_text(encoding="utf-8-sig"))
p = data.get("project_name", "")
if p:
projects.add(p)
except (json.JSONDecodeError, OSError):
pass
self._cached_projects = projects
self._projects_cache_ts = now
return projects
async def _poll_commands_loop(self):
"""Poll Gateway for commands with adaptive per-project intervals.
When a project returns empty commands repeatedly, its poll interval
increases (3s → 10s → 30s → 60s). On receiving a command, interval
resets to base. This prevents idle projects from wasting requests.
"""
# Per-project adaptive state
project_intervals: dict[str, float] = {} # project → current interval
project_last_poll: dict[str, float] = {} # project → last poll timestamp
_BASE_INTERVAL = 3.0
_IDLE_STEPS = [10.0, 30.0, 60.0] # progressive idle intervals
project_empty_streak: dict[str, int] = {} # project → consecutive empty polls
while self._running:
try:
# Skip cycle if rate-limited
if not self.remote.is_rate_limited:
projects = self._discover_local_projects()
now = time.time()
for project in projects:
if self.remote.is_rate_limited:
break
# Check if this project's interval has elapsed
interval = project_intervals.get(project, _BASE_INTERVAL)
last = project_last_poll.get(project, 0)
if now - last < interval:
continue # Not time yet for this project
project_last_poll[project] = now
commands = await self.remote.apoll_commands(project)
if commands:
# Got commands → reset to base interval
project_intervals[project] = _BASE_INTERVAL
project_empty_streak[project] = 0
for cmd in commands:
cmd_id = cmd.get("id", f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}")
fname = f"{cmd_id}.json"
self.local.write_json("commands", fname, cmd)
logger.info(f"[COLLECTOR] ← Gateway: command [{project}] {cmd.get('text', '?')[:30]}")
else:
# Empty → increase interval progressively
streak = project_empty_streak.get(project, 0) + 1
project_empty_streak[project] = streak
if streak <= len(_IDLE_STEPS):
project_intervals[project] = _IDLE_STEPS[streak - 1]
# else stays at max (60s)
await asyncio.sleep(0.3) # Throttle between projects
except Exception as e:
logger.error(f"[COLLECTOR] poll_commands error: {e}")
await asyncio.sleep(self._poll_interval)
# ─── Forward chat snapshots → Gateway ───
async def _forward_chat_snapshots_loop(self):
"""Forward chat_snapshots/ from Extension to Gateway."""
while self._running:
try:
snap_dir = self.local.bridge_dir / "chat_snapshots"
if snap_dir.exists():
for f in snap_dir.glob("*.json"):
try:
data = json.loads(f.read_text(encoding="utf-8-sig"))
project = data.get("project_name", self.project_name)
content = data.get("content", "")
attached_files = data.get("attached_files", [])
if content or attached_files:
await self.remote.asend_chat(project, content, attached_files=attached_files)
af_info = f" +{len(attached_files)} files" if attached_files else ""
logger.info(f"[COLLECTOR] → Gateway: chat snapshot len={len(content)}{af_info}")
f.unlink() # Cleanup after forwarding
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"[COLLECTOR] bad chat snapshot {f.name}: {e}")
except Exception as e:
logger.error(f"[COLLECTOR] forward_chat_snapshots error: {e}")
await asyncio.sleep(10) # Chat snapshots: less urgent, 10s interval
# ─── Forward session registrations → Gateway ───
async def _forward_registrations_loop(self):
"""Forward register/ files from Extension to Gateway."""
forwarded_regs: set[str] = set()
while self._running:
try:
register_dir = self.local.bridge_dir / "register"
if register_dir.exists():
for f in register_dir.glob("*.json"):
if f.name in forwarded_regs:
continue
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:
await self.remote.aregister_session(conv_id, project)
forwarded_regs.add(f.name)
logger.info(f"[COLLECTOR] → Gateway: register {conv_id[:8]}{project}")
await asyncio.sleep(0.3) # Spread startup burst
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"[COLLECTOR] bad register {f.name}: {e}")
except Exception as e:
logger.error(f"[COLLECTOR] forward_registrations error: {e}")
await asyncio.sleep(30) # Registration changes rarely — 30s interval
# ─── Forward brain events → Gateway ───
async def _forward_events_loop(self):
"""Read BrainEvents from Watcher queue and POST to Gateway."""
while self._running:
try:
event: BrainEvent = await asyncio.wait_for(
self.event_queue.get(), timeout=5.0
)
# Serialize event to JSON
event_data = {
"event_type": event.event_type.value,
"conversation_id": event.conversation_id,
"file_name": event.file_name,
"file_path": str(event.file_path) if event.file_path else "",
"content": event.content,
"timestamp": event.timestamp,
}
await self.remote.asend_event(event_data)
logger.info(f"[COLLECTOR] → Gateway: event {event.event_type.value} {event.file_name}")
except asyncio.TimeoutError:
continue
except Exception as e:
logger.error(f"[COLLECTOR] forward_event error: {e}")
# ─── Health check ───
async def _health_check_loop(self):
"""Periodically check Gateway connectivity."""
while self._running:
try:
ok = await self.remote.health_check()
if not ok and self.remote.connected:
logger.warning("[COLLECTOR] ❌ Gateway health check failed")
except Exception:
pass
await asyncio.sleep(30)
# ─── Retry flush ───
async def _retry_flush_loop(self):
"""Periodically flush failed request retry queue."""
while self._running:
try:
await self.remote.flush_retry_queue()
except Exception as e:
logger.error(f"[COLLECTOR] retry flush error: {e}")
await asyncio.sleep(30) # Retry flush: 30s interval (was 10s)

View File

@@ -43,11 +43,14 @@ class Config:
CHANNEL_PREFIX: str = "AG"
PROJECT_NAME: str = os.getenv("PROJECT_NAME", "gravity_control")
# Bot mode: 'local' (file-based bridge) or 'remote' (HTTP polling — future)
# Bot mode: 'local' (file-based bridge) or 'gateway' (WS Hub + HTTP API)
BOT_MODE: str = os.getenv("BOT_MODE", "local")
REMOTE_BRIDGE_URL: str = os.getenv("REMOTE_BRIDGE_URL", "")
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
def validate(cls) -> list[str]:
"""Return list of configuration errors."""

0
diag_output.txt Normal file
View File

View File

@@ -3,39 +3,28 @@ services:
build: .
container_name: gravity-gateway
restart: unless-stopped
# Port NOT exposed directly — Caddy handles external access
expose:
- "8585"
ports:
- "127.0.0.1:8585:8585"
env_file:
- .env
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN}
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
- BOT_MODE=gateway
- GATEWAY_PORT=8585
- GATEWAY_API_KEY=${GATEWAY_API_KEY}
- 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"
caddy:
image: caddy:2-alpine
container_name: gravity-caddy
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- gateway
max-size: 10m
max-file: 3
volumes:
gateway-data:
caddy-data:
caddy-config:
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

@@ -4,5 +4,9 @@
|---|------|------|------|------|
| 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건 업데이트 | `08c2c86` | 🔧 |
| 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` | ✅ |

28
docs/devlog/2026-03-17.md Normal file
View File

@@ -0,0 +1,28 @@
# Devlog — 2026-03-17
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 009 | 00:00~06:38 | Extension 모듈 분리 + Hub 통합 테스트 + VSIX v0.4.0 빌드 | `5f795b9` | ✅ |
| 010 | 06:50~07:39 | 문서 전면 재작성 + 서버 배포 + WS 호환 수정 | `6ea3211` | ✅ |
| 011 | 07:44~08:18 | VSIX v0.4.0 E2E 사전 검증 + WS 프록시 수정 | — | 🔧 |
| 012 | 09:00~17:44 | VSIX E2E: workspaceUri, 이중발송, ApprovalRequest, ApprovalView WS, 응답 라우팅 | `2eea5fa` | ✅ |
| 013 | 18:05~18:45 | Extension 모듈 분리 #398: http-bridge, html-patcher, command-handler 추출 (1296→650줄) | `6640d42` | ✅ |
| 014 | 18:45~20:35 | WS+File dual-delivery 수정 + 에코 릴레이 수정 + VSIX v0.4.4 빌드 | `0da6291` | ✅ |
| 015 | 20:45~21:00 | Accept All WS regression 수정 + auto_approve 이중쓰기 수정 + VSIX v0.4.5 | `47cc838` | ✅ |
| 016 | 21:00~21:27 | 통신 아키텍처 나노단위 감사: writeRegistration 이중쓰기 + ApprovalView fallback + scanner 최적화 | — | ✅ |
| 017 | 21:35~21:53 | Hub pending_owners 생명주기 수정: WS 재연결 시 승인 응답 소실 방지 (reconnect reassign + fallback routing) | `9ccfa83` | ✅ |
### #010 상세
- **문서**: architecture.md(250줄), tech-stack.md(100줄), conventions.md(100줄) 전면 재작성 + Wiki 동기화
- **태스크 정리**: #296 폐기, #396~#400 신규 5건 등록
- **서버 배포**: docker-compose.yml 서버 실제 구성 반영, Caddyfile 제거, ag.variet.net 도메인 확인
- **WS 호환**: ws-client.ts 브라우저 WebSocket API 호환 (.onopen/.onmessage) 수정
- **Known issue**: VS Code 캐시로 Extension 코드 반영 지연 — 완전 재시작 필요
### #011 상세
- **WS 프록시 수정**: NPM(openresty)에서 WebSocket Support 활성화 → 101 Switching Protocols 확인
- **WS 인증 검증**: `wss://ag.variet.net/ws` → auth_ok, conn_id 발급, instance=#1 확인
- **VSIX 설치**: v0.4.0 설치 확인, v0.3.16 제거, ws 모듈 수동 복사
- **AG 설정**: `settings.json`에 hubUrl + registrationCode 설정
- **ws 번들**: `.vscodeignore``!node_modules/ws/**` 추가, `package.json`에 ws dependency
- **미완료**: AG 재시작 후 Extension→Hub→Bot→Discord 실제 E2E 검증 필요

View File

@@ -0,0 +1,8 @@
# 2026-03-18 Devlog
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 1 | 11:00 | v0.5.0 Collector 제거 + dead code 정리 + HttpBridgeContext 버그 수정 | `e763117` | ✅ |
| 2 | 14:00 | bot.py unit tests 27건 — _write_command, _hub_on_pending, ApprovalView | `a41062b` | ✅ |
| 3 | 14:30 | step-probe.ts 모듈 분리 → approval-handler.ts (1597→1017+411줄) + dead code 제거 | `17978a7` | ✅ |
| 4 | 15:30 | 코드베이스 건강도 분석 + 통신 레이어 전수 감사 (8파일/7메시지타입) → 수정 필요 0건 | — | ✅ |

View File

@@ -0,0 +1,6 @@
# 2026-03-19 Devlog
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 1 | 07:30 | v0.5.1 browser_subagent Allow RPC 매핑 수정 + .env 정리 | `549af6d` | ✅ |
| 2 | 10:35 | v0.5.2 Idle→Resume 신호 소실 3중 버그 수정: auth_fail 재연결, pending_owners 보존, step-probe 리셋 | `5aad82c` | ✅ |

View File

@@ -0,0 +1,6 @@
# 2026-03-21 Devlog
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 1 | 17:48 | v0.5.3~v0.5.4 신호 감지 3중 버그 수정: 세션 전환 즉시 probe (20-25s→5s), reviewAbsoluteUris 필드 수정, stepIndex=-1 uint32 에러 수정 + permission 매핑 | `0fb33a9` | ✅ |
| 2 | 21:14 | v0.5.5 wrong-LS 자동 복구: Deriva RPC "input not registered" 근본 원인 분석 → fixLSConnection export + single-LS 조기종료 제거 + approval-handler 자동 LS 재연결 + 1회 retry | `6234301` | ✅ |

View File

@@ -0,0 +1,5 @@
# 2026-03-22 Devlog
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 1 | 01:22 | VSIX v0.5.5 빌드 — package.json 버전 범프 + vsce package | `b81135d` | ✅ |

View File

@@ -0,0 +1,6 @@
# 2026-03-23 Devlog
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|-----|-------|----------|-----------|-----------|
| 001 | 21:09 | WebSocket 좀비 커넥션 해결 및 통신망 메모리 누수 패치 | `ecebec3` | ✅ |
| 002 | 22:45 | Cross-Project DOM Observer Leakage 패치 및 포트 동적 디스커버리 적용 | `TBD` | ✅ |

View File

@@ -0,0 +1,7 @@
# 2026-03-24 Devlog
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|-----|-------|----------|-----------|-----------|
| 001 | 07:05 | v0.5.6 좀비 커넥션 패치 회귀 오류 해결 (False Positive 끊김 방지를 위한 타임스탬프 검증 도입 v0.5.8) | `f13bcc8` | ✅ |
| 002 | 13:00 | DOM Observer VS Code 네이티브 알림 UI 캡처 블라인드 스팟 해결 (v0.5.9) | `7b6cd59` | ✅ |
| 003 | 18:14 | DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징 해결 (v0.5.10) | `101ec20` | ✅ |

View File

@@ -0,0 +1,5 @@
# 2026-03-25 Devlog
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|-----|-------|----------|-----------|-----------|
| 001 | 07:15 | ws-client reconnect pacing 및 http-bridge 정규식 필터 완화로 Signal Drop 해결 (v0.5.10) | `pending` | ✅ |

View File

@@ -0,0 +1,5 @@
# Devlog — 2026-03-28
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 09:12 | guitar_score step-probe UTF-8 무한루프 수정 + approval stepIndex 보정 (v0.5.11) | `7bbd874` | ✅ #539 |

View File

@@ -0,0 +1,5 @@
# 2026-04-01 Devlog
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|-------|-------|-----------|-------------|--------------|
| 001 | 18:22 | `step-probe` 10-Item Truncation/DoS 우회 (vsix v0.5.14) | `TBD` | ✅ |

View File

@@ -0,0 +1,8 @@
# 2026-04-08
| NNN | HH:MM | 작업 설명 | `커밋해시` | 상태 |
|---|---|---|---|---|
| 004 | 14:00 | SafeToAutoRun 알림 누락 복구 (v0.5.18) | `8f2a1b3` | ✅ |
| 005 | 16:30 | SafeToAutoRun pending skip으로 인한 데드락 원인 파악 및 롤백 | `13f13ee` | ✅ |
| 006 | 07:30 | SafeToAutoRun 데드락 완전 해결을 위한 Agnostic Bridge 도입 및 프리징 방어 (v0.5.20) | `임시해시` | ✅ |
| 007 | 17:57 | Gravity Bridge 안정화: 중복 알림(SafeToAutoRun) 제거 설계 확정 및 Discord 봇 캐시/로컬 LS 크로스매칭 증상 디버깅 완료 | \-\ | ✅ |

View File

@@ -0,0 +1,9 @@
# 2026-04-09
| NNN | HH:MM | 작업 설명 | `커밋해시` | 상태 |
|---|---|---|---|---|
| 001 | 21:55 | Agent UI Tailwind/Native 마이그레이션 대응 (DOM 옵저버 구조 개편) | `HEAD` | ✅ |
| 002 | 22:30 | Agent UI 버튼 무시 버그 긴급수정 (CodeLens 필터교정) | `HEAD` | ✅ |
| 003 | 23:15 | Native UI 아이콘 글루잉 대응 스캐너 픽스 (DOM Regex 매칭 강화) | `HEAD` | ✅ |
| 004 | 00:10 | Discord Signal Relay & Auto-Approve Body Null 버그 수정 (False Positive 차단) | "HEAD" | ✅ |
| 005 | 23:00 | fix: Resolve empty Discord embed body by populating detailed step-probe payload | \\ | ? |

View File

@@ -0,0 +1,4 @@
| NNN | 시간 | 작업 설명 | 커밋해시 | 완료여부 |
|---|---|---|---|---|
| 001 | 17:11 | step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 | COMMITTING | ✅ |
| #613 | 21:10 | Bridge Relay AI Chat Body DOM extraction and template literal Regex fix | TBD | ✅ |

View File

@@ -0,0 +1,6 @@
# 2026-04-11
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|-------|-------|----------|-----------|-----------|
| 001 | 13:04 | Pure 웹소켓 게이트웨이 완전 전환 및 레거시 파일 브릿지 통신코드 삭제 | `072f83b` | ✅ |
| 002 | 17:25 | Antigravity Observer 컨텍스트 추출 범위 제한 및 노이즈(UI/TypeScript 코드) 필터링, Discord 임베드 개선 | `70dc301` | ✅ |

View File

@@ -0,0 +1,7 @@
# 2026-04-12
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완/미 |
|-------|-------|----------|-----------|-----------|
| 001 | 06:12 | AG Native DOM 파싱 v7 전면 재설계 — data-testid/data-step-index 기반 step-aware 파서, UI 노이즈 차단 | `a4d7286` | 🔧 |
| 002 | 07:03 | AG Native 번들 역공학 분석 + V8 CachedData 삭제 — plannerResponse→Whi 렌더러 구조 확인, bot-color가 NUX용임 발견 | — | 🔧 |
| 003 | 07:37 | Observer v8 전면 개편 — conversation-view 의존 제거, body 전체 무조건 덤프(depth 15), 5s/15s/60s 자동 덤프, VSIX v0.5.37 설치 | `0e03b3a` | 🔧 |

View File

@@ -0,0 +1,7 @@
# 2026-04-13
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료여부 |
|-------|-------|----------|-----------|----------|
| 001 | 09:50 | Observer v8 검증 — Extension POLL 확인, HTML 패치 확인, V8 캐시 삭제(24MB), BEACON 미수신(AG 재시작 필요) | 없음 | 🔧 |
| 002 | 12:34 | DOM Observer 데이터 품질 검증 + UTF-8 인코딩 수정 + noise 필터 강화 (v0.5.39) | `pending` | ✅ |
| 003 | 19:26 | Observer v9: "Running N commands" 오인 수정 + DOM-climbing 컨텍스트 추출 + http-bridge 필터 완화 (v0.5.40) | `pending` | 🔧 |

View File

@@ -0,0 +1,5 @@
# 2026-04-14
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료여부 |
|-------|-------|----------|-----------|----------|
| 001 | 07:29 | Observer v9 E2E 검증 — BEACON/ping 동작 확인, pending POST 수신 확인, 컨텍스트 추출 실패 진단 (ctx="Always run"), v10-diag 진단 로깅 추가 (observer + http-bridge), V8 캐시 삭제(523MB) | `c1e61d8` | 🔧 |

View File

@@ -0,0 +1,6 @@
# 2026-04-15
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료? |
|-------|-------|----------|-----------|----------|
| 001 | 09:12 | PROMPT_ONLY_RE 근본원인 분석 및 수정 — Observer regex 이스케이핑(4중→2중 backslash) + http-bridge ellipsis prefix 지원, 16개 테스트 전체 통과, VSIX v0.5.45 빌드/배포 | `01539e9` | ✅ |
| 002 | 10:35 | Observer fallback 컨텍스트 추출 수정 — v0.5.45 VSIX 설치 누락 발견/수정 + v13 `_promptOnlySkipped` 플래그로 채팅/UI 텍스트 추출 차단 + bridge generic button 무조건 필터 (v0.5.46) | `b8cda27` | 🔧 |

11
docs/devlog/2026-04-16.md Normal file
View File

@@ -0,0 +1,11 @@
# 2026-04-16
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료? |
|-------|-------|----------|-----------|----------|
| 001 | 04:52 | v16 터미널 출력 필터 + v15 stale LS 자동복구 + heartbeat probe — stdout가 enrichedCmd로 채택되는 버그 수정, VSIX v0.5.50 빌드/배포 | `7ade31e` | 🔧 |
| 002 | 05:28 | AG Native AI 응답 Discord 미전달 근본원인 분석 + Observer v15 scanChatBodies 이중전략 (#conversation + .leading-relaxed.select-text) 구현, v0.5.51 배포 | `729875f` | 🔧 |
| 003 | 17:13 | Observer v15 E2E 코드 검증 — CSP/체크섬/문법/핸들러 전수 확인 OK. BEACON=0 원인: AG 미재시작(렌더러 HTML 캐시). v0.5.50/out에 v15 JS 직접 배포 | — | ✅ |
| 004 | 21:07 | Observer v16 CSS 추출 버그 수정 — `<style>` 태그 textContent가 AI 응답으로 Discord 전달. extractCleanStepText()에 style/script strip 추가, v0.5.52 배포 | `62ee081` | ✅ |
| 005 | 22:07 | Observer v17 Always run 자동승인 + Retry 릴레이 — "Always run" 브릿지 레벨 자동승인, Retry 버튼 Discord 전달, v0.5.53 배포 | `7dbf73a` | ✅ |
| 006 | 21:28 | AG Native <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Markdown <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>Ʈ/<2F><>) <20><><EFBFBD><EFBFBD> <20><> User <20><>û <20>̼<EFBFBD><CCBC><EFBFBD> <20>м<EFBFBD>, <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ȹ<EFBFBD><C8B9> <20>ۼ<EFBFBD> (v0.5.54 <20><><EFBFBD><EFBFBD> <20><>) | ? | ?? |

View File

@@ -0,0 +1,5 @@
# 2026-04-17
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료 |
|-------|-------|----------|-----------|----------|
| 001 | 08:05 | v18 Observer DOM->Markdown 파서 개선(<a> 파싱 포함) 및 노이즈 필터 부작용(코드 블럭 잘림 방지) 해결, User 구문 추출 연동, v0.5.56 배포 | `미정` | ✅ |

View File

@@ -0,0 +1,5 @@
# Devlog — 2026-04-18
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|---|------|----------|------|------|
| 001 | 09:20~23:50 | Retry auto-approve 흐름 복구 — WS response 파일 보존 (`_from_ws`), Observer 형제 탐색(sibling), thinking 블록 필터링 | `pending` | ✅ |

26
docs/devlog/2026-04-19.md Normal file
View File

@@ -0,0 +1,26 @@
# Devlog 2026-04-19
## 작업 인덱스
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 21:22 | v30-32 Observer 명령어 추출 안정화 (터미널 프롬프트 조기감지) | `bd5a7ca` | ✅ |
| 002 | 23:16 | v33 Accept all 자동승인 — diff review auto-approve | `6aea48e` | ✅ |
| 003 | 00:18 | v34 Accept all 이중 보장 — agentAcceptAllInFile 직접 호출 | `cf1352e` | ✅ |
| 004 | 00:34 | v35 code_edit 자동 Accept — step-probe 경로 | `2bf1eb4` | ✅ |
| 005 | 04:26 | v36 Accept all span 감지 — 근본 원인 발견 (button→span) | `e95e779` | ✅ |
| 006 | 04:34 | v37 openReviewChanges 선호출 — agentAcceptAllInFile 보조 | `3cc3442` | ✅ |
| 007 | 04:43 | v38 _from_ws 마커 추가 — Observer polling 실패 근본 수정 | `7c8891b` | ✅ |
## v0.5.103 — Accept all (Diff Review) 자동 승인 복구
### 근본 원인 (2가지)
1. **Observer 감지 실패**: AG UI가 "Accept all"을 `<button>`이 아닌 `<span class="cursor-pointer">`로 렌더링. Observer의 `allBtns` 선택자가 `button`만 스캔하여 미감지.
2. **Response 파일 race condition**: auto-approve response 파일에 `_from_ws: true` 마커 없음 → `processResponseFile`이 Observer보다 먼저 파일 삭제 → Observer polling 무한 실패.
### 검증 결과
- Observer ACCEPT-SCAN: `tag=SPAN cls=cursor-pointer txt=Accept all`
- `DETECTED diff_review: Accept all`
- `response served to renderer: ...approved=true` (이전 0건 → 7건) ✅
- Discord "자동 승인됨 Accept all" 표시 ✅
- 화면에서 "Accept all" 버튼 자동 소멸 확인 ✅

View File

@@ -0,0 +1,12 @@
# WebSocket 좀비 커넥션 해결 및 통신망 메모리 누수 구조 패치
- **시간**: 2026-03-23 21:09~21:20
- **Commit**: `ecebec3`
- **Vikunja**: #510 → done
## 결정 사항
- **ws-client.ts 핑퐁 와치독(Ping-Pong Watchdog)**: 단순 에러 캐치가 아니라 `ws.terminate()`를 통해 무반응 소켓을 강제 종료하여 자체 재연결 로직(`_onDisconnect`)을 활성화하도록 설계.
- **통신망 추적 변수 캡핑(Bounded Cap)**: `hub.py``pending_owners``bot.py``_sent_approval_ids` 등 무한히 쌓일 수 있는 파이썬 딕셔너리에 LRU(오래된 순 삭제) 로직을 추가. 비록 당장 OOM을 유발하진 않지만 이 구조적 메모리 누수(Leak)를 원천적으로 차단하여 시스템 안정성을 극대화함.
## 미완료
- 없음

View File

@@ -0,0 +1,16 @@
# Cross-Project DOM Observer Leakage 해결
- **시간**: 2026-03-23 22:00~22:45
- **Commit**: `TBD`
- **Vikunja**: #TBD → done
## 확인된 사실
- Discord 신호 누락이 아닌, 다중 원격 환경에서의 포트 덮어쓰기 문제로 인한 **교차 프로젝트 신호 오염(Leakage)**이었음.
## 삽질 / 트러블슈팅
- 처음에는 디스코드 봇(`bot.py`)이나 익스텐션의 `step_type` 매핑 로직 누락인 줄 알고 코드를 탐색했으나, 실제 DOM observer 스크립트에 하드코딩된 Port 변수가 문제의 원인임을 파악함.
- 다중 원격 컴퓨터 환경 중 포트 포워딩(`12345` 충돌 우회)으로 인한 이슈를 해결하기 위해 `vscode.env.asExternalUri`를 도입. 로컬에 매핑된 최종 확정 포트를 알아냄.
## 결정 사항
- DOM Status Bar(`tooltip`)를 일종의 단방향 IPC(Inter-Process Communication) 대용으로 사용하기로 결정함.
- Extension Host가 렌더러(DOM Observer)에게 안전하고 해당 창에만 격리(Window-isolated)된 방식으로 포트 번호를 전달할 수 있음. 전역 HTML 파일 패치의 한계를 우아하게 극복함.

View File

@@ -0,0 +1,12 @@
# v0.5.6 좀비 커넥션 패치 회귀 오류 해결 (v0.5.8 반영)
- **시간**: 2026-03-23 23:10 ~ 2026-03-24 07:05
- **Commit**: `TBD`
- **Vikunja**: 신규 추가 예정
## 결정 사항
- **False Positive 멈춤 현상 원인 규명**: v0.5.6에서 추가된 `pongTimeoutTimer` (10초 타임아웃)가 VS Code 확장 내부의 일시적인 Event Loop 블로킹 발생 시 네트워크 I/O(`pong` 응답)보다 먼저 소켓을 강제 종료하고 있었습니다. 이 때문에 멀쩡한 연결이 끊어지고 재연결 지연 페널티가 누적되어 최대 60초까지 응답 불가(멈춤) 상태에 빠지는 현상이 발견되었습니다.
- **해결 방안 선택 (타임스탬프 검증)**: 타이머 동시성 경합을 유발하는 `setTimeout` 방식을 전면 폐기하고, 기존의 `setInterval` (25초 주기) 하트비트 루프 내부에서 `ws.on('pong')`이 갱신하는 `lastPongTime`을 대조(`Date.now() - lastPongTime > 60000`)하는 방식으로 변경했습니다. 이를 통해 Event Loop가 지연되더라도 I/O 이벤트를 먼저 수확한 후에 안전하게 판독할 수 있어 오진단(False Positive)을 원천 차단하면서도 좀비 커넥션을 방지했습니다.
## 미완료
- 없음 (v0.5.8 VSIX 컴파일 성공 및 배포 완료)

View File

@@ -0,0 +1,18 @@
# DOM Observer VS Code 네이티브 알림 UI 캡처 블라인드 스팟 해결 (v0.5.9)
- **시간**: 2026-03-24 12:00~13:00
- **Commit**: `7b6cd59`
- **Vikunja**: #514
## 결정 사항
- **문제**: "Always Allow" 및 "Allow Alt+↵" (단축키 포함) 권한 알람이 Discord로 전송되지 않는 문제가 발생했습니다. (v0.5.8)
- **근본 원인 확인**:
- Regex 실패: `Always Allow``^Allow` 정규식을 통과하지 못합니다.
- CSS Selector 실패: `observer-script.ts`의 스캔 엔진이 오직 `document.querySelectorAll('button')`에만 의존하여 렌더링 노드를 찾고 있었습니다. VS Code 네이티브 권한 프롬프트(토스트 알림 및 채팅 패널)는 `<a role="button" class="monaco-text-button">` 또는 `<vscode-button>`을 활용하므로 애초에 찾지도 못하고 스킵되었습니다.
- **해결책**:
1. `observer-script.ts` 내의 모든 DOM 쿼리를 `button, [role="button"], vscode-button, .monaco-text-button` 으로 확장.
2. 허용 권한 토큰 관련 정규식을 `/^(?:Always )?Allow/i` 로 상향 패치.
3. `v0.5.9` 로 빌드 및 VSIX 설치 완료 후 정상 동작 검증 완료.
## 미완료
- 없음.

View File

@@ -0,0 +1,18 @@
# DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징 해결 (v0.5.10)
- **시간**: 2026-03-24 17:50~18:20
- **Commit**: `HEAD` (예정)
- **Vikunja**: #514 관련 디버깅/핫픽스
## 결정 사항
- **문제**: "0.5.9 패치한 이후 화면이 펜딩되서 움직여지지않아" 라는 증상 확인.
- v0.5.9에서 DOM 쿼리를 `[role="button"]` 등으로 확장했으나, 정규식이 `/^Run/i` 등으로 풀어진 상태여서 에디터 뷰의 "Run Test" 등 수많은 CodeLens 버튼들을 Agent의 트리거로 오인함.
- 결과적으로 아무 조작도 하지 않았는데 계속 터미널 실행 대기상태(Pending)로 무한 진입하여 UI 화면이 프리징(Freeze)됨.
- 특히 디스코드에서 `Approve` 명령을 내렸을 때도, DOM 트리상 상단에 우연히 "Run" CodeLens가 있으면 먼저 캡처되어 진짜 Agent 패널의 버튼을 클릭하지 못하고 엉뚱한 요소를 클릭하는 위험한 순위 불일치 버그까지 있었음.
- **해결책 (Structural Context Filtering)**:
1. 감지(Scan): 단순 정규식을 빡빡하게 변경하면 동적인 버튼 이름("Run script" 등)이 안 먹히는 부작용이 있으므로 느슨함을 유지하되, **발생 영역(DOM Context)**에 강제 필터를 부여.
- `isVSCodeMainWindow` 및 노드 루트가 `document.body`인지를 체크하여, 에디터 본문 영역 안에서는 "Run", "Approve", "Accept" 캡처를 전부 무시.
2. 제어(Trigger-click 우선순위): `observer-script.ts``deepFindButtons()` 내부 스캔 트리를 변경하여 `findPanel()`로 안티그래비티 패널을 1순위로 조회, 알림 Toast를 2순위, 본문 Document를 3순위로 탐색하게 강제하여 엉뚱한 버튼 클릭 사고를 100% 방지함.
## 미완료
- 없음 (빌드 및 검증 완료)

View File

@@ -0,0 +1,11 @@
# step-probe Pagination 10-Item Truncation vs LS DoS 오류 수정
- **시간**: 2026-04-01 13:00~18:22
- **Commit**: `TBD`
- **Vikunja**: #N/A (임시 버그 픽스)
## 결정 사항
- 기존 `v0.5.13`에서 `limit: 100`으로 Pagination Limit(기본 10개)을 우회하려 했으나, LS DB 스캔 및 거대한 JSON 파싱이 VS Code Event Loop 블로킹을 유발하여 UI 멈춤(DoS) 발생.
- 롤백 과정에서 `{}`(인자 없음)으로 원복하면서 필수적인 `descending: true` 파라미터까지 누락됨.
- 이로 인해 `guitar_score` 등의 최신 작성 세션이 LS 조회 리밋(10)에서 밀려나 승인 신호를 수신하지 못하는 이슈 재발.
- 이를 해결하기 위해 `limit: 30, descending: true`로 설정. 파싱해야 할 JSON 객체 수를 1/3로 줄임과 동시에, 정렬 보장을 통해 최근 10초 이내에 활성화된 세션은 언제나 Index 0번 최상단에 고정되게끔 메커니즘을 수정함.

View File

@@ -0,0 +1,20 @@
# 2026-04-08 (004) - SafeToAutoRun 명령어의 디스코드 알림 누락 복구
## 1. 이슈 개요
- 사용자가 `/start` 등 백그라운드 명령어(SafeToAutoRun)를 포함한 워크플로우를 실행하였으나, 디스코드로 아무런 메시지도 전송되지 않는 버그가 보고됨.
## 2. 원인 분석
- v0.5.16 배포 당시 Discord 중복 알림(Pending 파일) 이슈를 방지하는 과정에서, `step-probe.ts`에 있던 "⚡ 자동 실행됨" 원본 알림 코드(snapshot 생성 로직)까지 실수로 함께 삭제됨.
- `SafeToAutoRun` 구문에서 `writePendingApproval` 스킵 로직은 잘 동작하고 있었으나, 정작 사용자에게 알려야 할 기본적인 '자동 실행됨' 정보마저 소실되어 결과적으로 아무 알림도 가지 않는 침묵 상태가 됨.
## 3. 해결 및 적용 사항
1. `step-probe.ts` 복구
- `SafeToAutoRun` 판단 시 `autoRunSteps`를 마킹한 직후 `ctx.writeChatSnapshot()`을 호출하도록 코드를 추가 복원함.
- 출력 구조: `💬 **자동 실행됨** (step N)\n\n\`명령어내용\``
2. **v0.5.18 배포**
- 익스텐션의 `package.json` 버전을 `0.5.18`로 펌핑.
- 사전 스크립트가 적용된 `vsce package`를 통해 새로운 `gravity-bridge-0.5.18.vsix` 패키징을 완료함.
## 4. Next Step
- `extension/gravity-bridge-0.5.18.vsix` 파일을 VS Code에 수동 설치할 것 (Install from VSIX...).
- 설치 후 반드시 **Reload Window**하여 테스트 수행 요망.

View File

@@ -0,0 +1,20 @@
# 2026-04-08 (005) - SafeToAutoRun 로컬 자동 승인 누락 데드락(Freeze) 해결
## 1. 이슈 개요
- 사용자가 확인 결과 v0.5.15 이후 백그라운드 터미널 명령어 등 모든 AI 에이전트 작업이 '자동 실행됨' 스냅샷만 보내고 VS Code 내부적으로는 여전히 승인(Allow)을 대기하며 완전히 멈춰버림(Freeze).
- 신호가 전달조차 안되고 다음 단계로 진행하지 못하는 심각한 블로커 이슈가 발생함.
## 2. 원인 분석
- v0.5.16 버그 픽스("Discord 중복 알림 방지") 당시 `SafeToAutoRun` 상태일 때 `writePendingApproval()`을 수행하지 않도록 코드(`skip pending`)를 변경했음.
- 그러나 과거에는 이 Pending 파일이 생성되면 파이썬 백엔드(Bot)가 디스코드에 알림을 띄운 직후, 자동으로 `approve`(허용) 신호를 익스텐션 쪽에 보내어 다음 단계가 허가되었음.
- 즉, 익스텐션에서 Pending 파일 생성을 중단(skip)하자 봇으로부터 수락 신호가 아예 오지 않게 되었고, VS Code의 보안 시스템에 의해 명령어는 영원히 "Run(Auto)" 클릭 승인을 대기하는 상태의 데드락에 빠져버림.
## 3. 해결 및 적용 사항
1. `step-probe.ts` 로컬 자동 승인 복구
- `safeToAutoRun` 판단으로 Pending 파일 생성을 건너뛸 때, 익스텐션 스스로 백그라운드 승인을 트리거하도록 `tryApprovalStrategies(true, ...)` 함수 호출 코드를 명시적으로 추가함.
- 이를 통해 봇의 승인 신호를 기다릴 필요 없이 즉각적으로 승인(Accept)을 단행하여 막힘없이 스텝이 연속 진행되도록 고침.
2. **v0.5.19 배포**
- VSIX 버전을 `0.5.19`로 펌핑 후 `npx vsce package` 명령으로 익스텐션을 재빌드함.
## 4. Next Step
- `extension/gravity-bridge-0.5.19.vsix` 파일을 수동 재설치하고 VS Code Window를 Reload 한 뒤, `/start` 같은 자동 워크플로우를 재실행하여 신호 블로킹(Freeze) 버그가 해결되었는지 최종 확인.

View File

@@ -0,0 +1,17 @@
# SafeToAutoRun 데드락 및 익스텐션 프리징 완벽 해결 (v0.5.20)
- **시간**: 2026-04-08 07:15~07:30
- **Commit**: `임시해시`
- **Vikunja**: #589 → done
## 발생 문제
1. **Deadlock**: 이전 버전(v0.5.15)에서 디스코드 알림을 줄이려고 익스텐션의 `step-probe.ts``SafeToAutoRun` 발생 시 `pending` 파일 생성 자체를 건너뛰도록 구현함. 하지만 AG 엔진은 CORTEX_STEP_STATUS_WAITING 상태에서 누군가가 해결해주기를 영원히 기다리게 되어, 파이프라인 전체가 데드락(UI 멈춤)에 빠지는 치명적인 부작용 발생.
2. **이벤트루프 Freeze**: `extension.ts``detectProjectName` 내부에서 동기식 `cp.execSync('git remote get-url origin')`를 실행하여 윈도우 환경에서 VS Code 이벤트루프가 막히고 WebSocket 통신이 유실되는 현상 발생.
## 결정 사항
- **Agnostic Bridge 철학 준수 (단일 경로 원칙 복구)**
- 익스텐션(`step-probe.ts`)은 절대 자의적으로 승인 처리를 하거나 `pending` 파일 생성을 스킵해서는 안 됨. 오직 브릿지 중계자 역할에 충실하도록 롤백하고, 대신 메타데이터에 `safe_to_auto_run: true` 속성을 실어 보냄.
- 파이썬 서버(`bot.py`) 관제탑이 이를 확인하면 디스코드에 알림(`Embed`)을 보내는 단계만 슬쩍 생략하고 그 즉시 허가증(`response/`)을 발급. 이를 통해 데드락 해제와 무소음 승인을 동시에 만족함.
- **비동기화 및 빌드 파이프라인 강제**
- 동기식 git 명령어 대신 비동기식 `.git/config` 파일 읽기로 교체.
- `package.json``vscode:prepublish` 스크립트를 부활시켜 낡은 소스코드가 VSIX에 패키징되는 문제 원천 차단.

View File

@@ -0,0 +1,18 @@
# Gravity Bridge 알림 최적화 및 연동 디버깅 완료
- **시간**: 2026-04-08 17:00~17:55
- **Commit**: `pending`
- **Vikunja**: 대상 작업 맵핑 예정
## 결정 사항
- `SafeToAutoRun``step-probe.ts`에서 날리던 **"⚡ 자동 실행됨"** 알림은 과감하게 완전히 제거하였습니다. 파이썬 봇이 이미 "🤖 자동 승인됨" Embed를 송출하고 있으므로 디자인 철학(익스텐션은 중립적인 릴레이 역할만 수행하고, 비즈니스 판정 알림은 중앙 봇이 담당)에 부합합니다.
- `extension.ts``writeChatSnapshot` 의존성을 줄여 트래픽 낭비와 중복 노이즈를 해소했습니다.
## 핵심 디버깅 (Troubleshooting)
- **`variet-llm` 프로젝트 연동 실패 이슈:**
1. `variet-llm` 창을 열었으나 채팅 패널을 열지 않아 안티그래비티 전용 언어 서버(LS)가 띄워져 있지 않은 상태에서 브릿지를 켬. 브릿지가 엉뚱하게 `gravity_control` LS에 바인딩 됨.
2. 사용자가 Discord에서 `variet-llm` 채널을 삭제해 버렸는데, 파이썬 봇(`bot.py`)은 캐시를 가지고 있어서 자신이 파괴된 채널을 대상으로 계속 통신을 시도하며 새 채널을 파지 않음 (HTTP 404).
3. 로컬 윈도우 재시작 및 도커(`docker-compose restart`) 컨테이너 재가동을 통해 **봇 프로세스 캐시 초기화** → 채널 자동 재생성, 완벽 디버깅에 성공했습니다.
## 미완료
- 없음. 모두 성공적으로 동작 중.

View File

@@ -0,0 +1,18 @@
# Agent UI Tailwind/Native 마이그레이션 대응 (DOM 옵저버 구조 개편)
- **시간**: 2026-04-09 19:40~21:55
- **Commit**: `[임시해시]`
- **Vikunja**: 신규 생성 후 완료 처리
## 트러블슈팅 및 결정 사항
최근 UI 업데이트 후 Discord 릴레이 신호(Run, Accept) 단절.
deep-inspect 덤프 분석 결과 Webview/Iframe 환경이 사라지고 Native DOM(VS Code 본문)에 напрямую 그려짐, 기존 시맨틱 클래스가 Tailwind로 변경.
1. 기존 `findPanel`이 패널을 못 찾자 `isBodyRoot` 모드로 스캔
2. 과거에 추가된 CodeLens 방어 로직(`if (isVSCodeMainWindow && isBodyRoot && PATS[p].type !== 'diff_review') continue;`)에 의해 모든 버튼 스캔이 **버려지고 있었음**.
**결정**:
엄격한 Panel Class Whitelist 기반 방어를 해제하고, 버튼이 `.monaco-editor` 내부에 있는 경우만 무시하도록 Blacklist 기반 방어로 선회.
UI 텍스트 글루잉(아이콘 통합) 대응 위해 패터닝 정규식을 `/^(?:Always\s*)?Run/i` 등으로 완화.
## 미완료
- 없음

View File

@@ -0,0 +1,15 @@
# Agent UI 버튼 무시(Discard) 버그 핫픽스
- **시간**: 2026-04-09 22:10~22:35
- **Commit**: `pending`
- **Vikunja**: 새로 생성 후 완료 예정
## 트러블슈팅 및 결정 사항
- **이슈**: Native UI 마이그레이션 직후 버튼을 눌러도 브릿지로 신호가 전혀 가지 않는 버그 접수
- **원인 분석**:
1. `extension.log` 확인 결과 `[HTTP] pending` 자체가 생성되지 않음 (브릿지 자체에 도달하지 않음).
2. DOM observer가 수집한 버튼이 `b.closest('.monaco-editor')` 필터 조건에 무조건 걸려서 버려지는 것이었음. Native 전환 후 채팅창이 에디터 탭 내부에 렌더링되면서 `.monaco-editor` 내부 자식이 됨.
- **결정**: 기존의 `b.closest('.monaco-editor')` 방어 로직을 폐기하고 실제 CodeLens 버튼 고유의 클래스 `.codelens-decoration`를 명시하도록 변경하여 구조 변화에 강건해지도록 개선 완료. `0.5.22` VSIX 재배포.
## 미완료
- 없음 (검증은 유저 몫으로 인계)

View File

@@ -0,0 +1,17 @@
# Agent UI Native 버튼 아이콘 글루잉 무시 현상 수정
- **시간**: 2026-04-09 23:00~23:15
- **Commit**: `TBD`
- **Vikunja**: 신규 생성 (UI 텍스트 글루잉 버튼 버그) → done
## 문제 상황
- 0.5.22 패치(CodeLens 필터) 이후에도 `Run`, `Accept` 버튼 클릭 시 디스코드 브릿지로 아무런 펜딩 요청(POST /pending)이 전송되지 않는 현상 발생.
- 원인 규명: Native UI 마이그레이션 적용 후, Agent 패널 버튼들의 아이콘(``, `▶` 등)이 리액트/Tailwind 컴포넌트 렌더링을 거쳐 `element.textContent` 상단에 문자열로 직접 병합(Gluing)됨.
- 옵저버 스크립트 내부 정규식(`/^(?:Always\s*)?Run/i`)이 문자열의 맨 첫(^) 시작을 강제하기 때문에, 아이콘으로 시작하는 버튼들의 명령어를 전부 오탐으로 간주함.
## 결정 사항
- 버튼의 텍스트를 읽는 즉시, `txt.replace(/^[^a-zA-Z0-9]+/, '')`를 적용하여 첫 글자가 영어/숫자가 될 때까지, 선행하는 모든 특수문자, 아이콘, 폰트 공백 등을 강제 삭제하도록 스크립트 내부의 3가지 탐색 루프 (본문 스캔, Sibling 버튼 수집, Webview trigger-click 인젝션)에 일괄 업데이트.
- 기존 `.monaco-editor``.chat-body` 등 부모 컨테이너에 지나치게 의존하던 `findButtonContainer``chat`, `prose`, `markdown`를 추가 화이트리스팅 하되 Tailwind UI 구조 특성상 시맨틱 래퍼를 찾지 못할 경우 3단계 위 부모를 반환하여 안전하게 컨텍스트를 확보하도록 고도화. -> **구조 변경 시에도 유연하게(Graceful) 기능 동작 지원 보장.**
## 결과
- `v0.5.23` (코드상 0.5.22 유지) VSIX 빌드 및 테스트 준비.

View File

@@ -0,0 +1,15 @@
# Discord Signal Relay & Auto-Approve Body Null 버그 수정 (False Positive 차단)
- **시간**: 2026-04-10 00:00~00:10
- **Commit**: `HEAD`
- **Vikunja**: #태스크번호 → done
## 트러블슈팅 & 삽질
- **DOM 정규식의 반란**: Native UI 패치 이후 버튼의 text-gluing 제거 때문에 정규식을 광범위하게 바꿨으나, 하필 `Run\s*` 조건에 단어 경계(`\b`)를 누락하는 바람에 VS Code 하단의 시스템 상태 버튼인 `Running 1 command`까지 AI의 `Run` 버튼으로 인식해버림. 무한 PENDING 스팸을 만들어 브릿지 큐를 폭파시킨 주범.
- **Auto-Approve 본문 누락**: 봇에서 자동 승인 Embed 생성 시 `req.description` (실행될 본문 코드)을 아예 그리지 않고 `req.command` (단순 버튼 라벨)만 출력하도록 코딩되어 있었음. 사용자는 '자동 승인' 알림을 받지만 정작 무엇이 승인되었는지는 전혀 알 수 없어 '본문 표시 자체가 안 된다'고 오해할 수밖에 없었음.
- **첫 알림 메시지 무시**: `step-probe.ts`에서 세션 전환 시 `lastNotifyStepIndex`를 초기화할 때 `-1` 로 리셋하지 않아 새 세션의 첫 안내 메시지가 매번 씹히는 증상 발견.
## 결정 사항
- `bot.py`의 Auto-Approve Embed 구문에 수동 승인처럼 본문을 표출하도록 렌더링 로직 통일.
- `observer-script.ts``TerminalCommand` 정규식에 `\b`를 추가하여 시스템 버튼과의 혼선을 원천 차단함.
- `step-probe.ts` 의 index reset 초기값을 `-1` 로 명시화.

View File

@@ -0,0 +1,13 @@
# step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스
- **시간**: 2026-04-10 17:11
- **Commit**: `COMMITTING`
- **Vikunja**: #125 → done
## 결정 사항
- AI 응답이 비정상적으로 빠를 경우 `RUNNING` 상태의 2초 polling 창을 우회하여 `IDLE` / `WAITING`로 진입해버리는 버그가 있었습니다.
- 기존에는 `isRunning && currentCount > ...`로만 Real-time Capture가 동작하여 전부 스킵되는 증상 확인.
- `isRunning` 조건을 삭제하고, `delta > 0`인 경우 `GetCascadeTrajectorySteps`를 페치하여 `PLANNER_RESPONSE``WAITING` 스텝을 동시에 처리하도록 개선했습니다.
## 미완료
- 없음.

View File

@@ -0,0 +1,13 @@
# Gravity Bridge 빠른 응답(Fast Execution) 누락 오류 해결
- **시간**: 2026-04-10
- **Commit**: TBD
- **Vikunja**: #607 → done
## 문제 원인
- AI 생성이나 응답 작업이 폴링 간격(5초) 미만으로 끝났을 때, 익스텐션의 폴링 루프는 이전과 동일한 `IDLE` 상태만을 보게 됨.
- `lastResponseCaptureStep` 검사는 마련되어 있었으나, `wasRunning` 플래그 제약(`wasRunning && !isRunning`)으로 인하여 IDLE->IDLE 전이를 거치는 모든 단기응답이 `[RESPONSE-CAPTURE]`를 영구히 건너뛰고 통째로 누락됨.
## 해결 방법
- `wasRunning` 방어 조건을 해제하고, `!isRunning && currentCount > lastResponseCaptureStep` 조건으로 완화 (인덱스 전진 기반 감지로 수정).
- 오래된 하드코딩 파서를 버리고 방벽 파서 역할을 하는 `extractPlannerText`로 갈무리 블록의 AI 응답 추출 로직을 단일화하여 적용.

View File

@@ -0,0 +1,15 @@
# [Bridge] Disable DOM Observer Proactive Pending to Fix Empty Bots
- **시간**: 2026-04-10 16:12
- **Commit**: TBD
- **Vikunja**: TBD
## 문제 원인
- 디스코드에 Running 1 comman d 나 내용 없는 Allow 가 반복적으로 전송되는 문제.
- 원인은 v3 DOM Observer 스크립트가 Native UI에서 발생시키는 빈 껍데기 알림(POST /pending)들이, 정상적으로 본문 정보를 모두 추출해 기다리고 있는 step-probe.ts의 완벽한 Pending을 덮어씌우거나 먼저 처리되어버렸기 때문임.
- 단어 경계(\b) 정규식 필터조차 VS Code 렌더링 시 노드 줄바꿈 이슈 등으로 인해 완벽한 방어가 불가능했음.
## 해결 방법
- observer-script.ts에서 버튼 텍스트를 감지해 능동적으로 Pending을 생성하는 기능(PATS 배열)을 **전면 비활성화(배열 비움)**.
- 이로써, 오직 100% 신뢰 가능한 SDK RPC(step-probe.ts)만이 대기 상태(WAITING)와 명령어 상세 정보를 포착해 Pending을 생성함.
- DOM Observer는 브릿지가 보내는 /trigger-click 폴링 명령어를 받아 실제 물리 클릭만 수행하는 '수동적 렌더러' 역할로 격하됨.

View File

@@ -0,0 +1,14 @@
# GetAllCascadeTrajectories 10-Item Hard Limit Bypass
- **시간**: 2026-04-10
- **Commit**: TBD
- **Vikunja**: TBD
## 결정 사항
- `GetAllCascadeTrajectories` LS API의 `limit` 등 페이지네이션 파라미터가 백엔드에서 무시되어 최신 세션이 10개 제한에 잘려나가는 문제를 확인.
- `DOM observer`가 더 이상 작동하지 않는 상태(Empty 보디 이슈로 비활성화됨)에서, `step-probe.ts`마저 이 10개 한도 밖으로 밀려난 현재 세션(`activeSessionId`)을 발견하지 못해, 발생한 모든 채팅 이벤트 파일이 작성되지 않는 문제("단 한글자도 안 날아옴")의 근본 원인을 특정함.
- `GetDiagnostics` API를 사용하여 내부적으로 저장된 `recentTrajectories` 덤프 전체를 불러와, 기존 `GetAllCascadeTrajectories`의 결과를 병합/보완하도록 변경.
- 이를 통해 아무리 많은 수의 세션이 열려 있어도 현재 사용 중인 세션 ID를 식별 가능.
## 미완료
- 없음.

View File

@@ -0,0 +1,15 @@
# Fix gravity bridge Discord Relay AI Chat Body by patching DOM extraction and Regex literals
- **시간**: 2026-04-10 20:30~21:10
- **Vikunja**: #613 → done
## 트러블슈팅: Typescript 백틱 안의 정규식 리터럴 파괴 현상
- **증상**: JSDOM 가상 모의 환경에서 테스트를 돌려보니, 렌더링 화면이나 타겟 Text가 정확히 매치됨에도 정규식이 조건문에서 `false`를 내뱉으며 Button Matching을 건너뛰는 현상 발생.
- **원인**: `observer-script.ts``.js`로 변환할 때, Typescript 컴파일러가 `return \`...\`` 템플릿 리터럴 내부의 `/^(?:Always\s*)?Allow\b/i` 구문을 해석하면서, `\s`를 일반 문자 `s`로, `\b`를 아스키 특수문자 `Backspace(0x08)`로 직렬화하여 클라이언트에 꽂아버리는 문제가 있었음. 이로 인해 정규식 자체가 오염되어 어떠한 버튼도 매칭하지 못하고 있었음.
- **해결**: `observer-script` 내부의 정규식 리터럴 내부의 이스케이프 문자(`\s`, `\b` 등)를 전부 이중 백슬래시(`\\s`, `\\b`)로 패치하여 브라우저에서 스크립트가 실행될 때 올바른 정규식 파서가 열리도록 수정 보완함.
## 결정 사항: 웹뷰 내 로컬 fetch CSP 패치 통과
- `html-patcher.ts`에서 웹뷰 렌더링 시점에 CSP를 조작하여 `default-src 'none'` 방어막을 뚫고 `connect-src``http://127.0.0.1:* wss://127.0.0.1:*`를 주입하도록 강제 적용함. 이를 통해 Bridge 서버로의 로컬 HTTP 통신이 활성화됨.
## 완료 상태
VSCode VSIX (0.5.27) 빌드 완료 및 릴리스 커밋 패키징 수행.

View File

@@ -0,0 +1,13 @@
# Pure 웹소켓 게이트웨이 전환 (Legacy 파일 브릿지 통신 완전히 제거)
- **시간**: 2026-04-11 11:00~13:00
- **Commit**: `(To be updated)`
- **Vikunja**: #N/A
## 결정 사항
- 기존 VS Code 익스텐션과 로컬 Discord Bot 간에 이루어지던 `.gemini/antigravity/bridge/` 기반 파일 공유 통신 체계를 100% 제거하였습니다.
- 파이썬 봇 서버(`bot.py`) 내부에서 동작하던 물리적인 폴링 디렉토리 스캐너(`pending_approval_scanner``chat_snapshot_scanner`) 파일 디펜던시 루프를 완전히 삭제하고 `Hub` WS 핸들러로 대체했습니다. 봇 패키지에 남아있던 `bridge.py``watcher.py` 또한 사용할 이유가 없어져 레포지토리에서 영구적으로 폐기 구별을 내렸습니다.
## 새로 알게된 사실 혹은 트러블슈팅
- 익스텐션에서 `activeSessionId` 변경 시 `watcher.py` 대신 Node.js 네이티브 `fs.watch` 기반으로 자체적인 `BrainWatcher`를 인하우스로 구현해 `step-probe.ts`에 주입함으로써 파이썬 의존도를 완전히 분리할 수 있었습니다.
- 권한 팝업 중복 처리 역시 폴더 스캔 대신 단순히 인메모리 `lastFilePermissionTime` 단일 변수로 최적화되었습니다.

View File

@@ -0,0 +1,22 @@
# AG Native DOM 파싱 v7 전면 재설계
- **시간**: 2026-04-12 05:49~06:12
- **Commit**: `a4d7286`
## 배경
- AG Native 세션에서 Discord 릴레이가 전혀 동작하지 않음
- SDK `GetCascadeTrajectorySteps``trajectory not found` 반환 — AG Native는 Cascade API에 등록 안 됨
- DOM observer가 UI 노이즈(content_copy, keyboard_arrow_up, Always run, Cancel)를 AI 응답으로 오인
## 결정 사항
- SDK 경로 대신 **DOM이 유일한 데이터 소스**로 확정
- `jetskiAgent/main.js` (11MB) 번들 분석으로 AG Native DOM 구조 역공학:
- `data-testid="conversation-view"` — 대화 최상위 컨테이너
- `data-step-index` — 각 step 식별 속성
- `text-ide-message-block-bot-color` — 봇 메시지 클래스 (확인됨)
- observer-script v6→v7 전면 재설계: step-aware 파싱
## 미완료
- AG 재시작 후 실제 동작 검증 필요 (DOM 덤프 → 셀렉터 미세조정)
- deep-inspect 정상 동작 확인
- Discord에 실제 AI 응답 전달 확인

View File

@@ -0,0 +1,22 @@
# AG Native 번들 역공학 + V8 캐시 정리 + Observer 미작동 원인 규명
- **시간**: 2026-04-12 06:28~07:03
- **Commit**: — (분석/조사 세션, 코드 변경 없음)
## 결정 사항
- `text-ide-message-block-bot-color`는 AI 응답 컨테이너가 **아닌** NUX tooltip 전용 클래스로 확인 → observer 셀렉터에서 제거 필요
- `markdown-body` 클래스도 AG Native에 존재하지 않음 → 폴백 셀렉터 변경 필요
- AI 응답 텍스트는 `plannerResponse` step → `Whi` 렌더러 → `div.px-2.py-1``MarkdownRenderer` 내부에 위치
- `data-step-index`는 디버그 패널에서 확인되었으나 메인 대화 뷰에서의 존재 여부는 라이브 DOM 덤프로 확인 필요
## 새로 알게 된 사실
- AG Native 번들(jetskiAgent/main.js 10.8MB): 전체 step.case→renderer 매핑 확보 (pan 객체)
- Allow/Deny는 `lHr` 컴포넌트, `border-t border-gray-500/25` 클래스
- Observer v7이 HTML에 삽입되었지만 V8 CachedData(50MB) 때문에 실제 렌더러에서 로드되지 않았음
- CachedData 삭제 완료 → AG 리로드 후 observer 작동 예상
## 미완료
- AG 리로드 후 observer 작동 확인
- deep-inspect로 실제 DOM 구조 캡처
- observer-script 셀렉터 미세조정 (bot-color 제거, MarkdownRenderer 타겟팅)
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,28 @@
# Observer v8 Electron 실행 보장 + 진단 beacon 추가
- **시간**: 2026-04-12 21:00~21:30
- **Commit**: (이전 세션 크래시 복구 커밋)
## 배경
- 이전 세션(f9491880)에서 Observer v8이 렌더러에서 실행되지 않는 문제 디버깅 중 크래시 발생
- deep-inspect 엔드포인트가 `timeout` 반환 — 인라인 스크립트가 Electron에서 실행 안 됨
- 원인: 인라인 스크립트가 `</html>` 뒤에 삽입되어 Electron이 무시하는 것으로 추정
## 변경 사항
### observer-script.ts
- DIAGNOSTIC BEACON 추가: 스크립트 로드 즉시 `/ping?beacon=1`으로 fetch → 실행 여부 확인 가능
### html-patcher.ts
- 인라인 스크립트 삽입 위치를 `</html>` 앞에서 **`</body>` 앞**으로 변경
- 기존 잘못된 위치의 인라인 블록을 제거 후 재삽입하는 로직 추가
- 이전 패칭에서 발생한 중복 `</html>` 태그 정리 로직 추가
### http-bridge.ts
- 진단용 HTTP 요청 로깅 추가 (폴링 엔드포인트 제외)
## 미완료
- AG 리로드 후 observer 작동 확인 (beacon ping 수신 확인)
- deep-inspect로 실제 DOM 구조 캡처
- observer-script 셀렉터 미세조정 (bot-color 제거, MarkdownRenderer 타겟팅)
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,25 @@
# Observer v8 검증 — V8 캐시 차단 재확인
- **시간**: 2026-04-13 09:50~11:00
- **Commit**: 없음 (진단/검증 세션)
- **Vikunja**: #619, #620 (진행 중)
## 진단 결과
1. **Extension POLL**: ✅ 정상 — 세션 `a91b5318` 추적 중, WS 명령 수신 정상
2. **bridge/ 위치**: `~/.gemini/antigravity/bridge/` (프로젝트 루트 아님)
3. **HTML 패치**: ✅ workbench.html + workbench-jetski-agent.html 모두 인라인 스크립트 + BEACON 삽입, `</body>` 앞 위치 정상
4. **Observer 실행**: ❌ BEACON 핑 0건, dom_dumps 디렉토리 없음
5. **V8 CachedData**: 24.42 MB 존재 → 삭제 완료
6. **Discord 릴레이**: ❌ AI 응답 텍스트 추출 불가
## 결정 사항
- Observer 스크립트 미실행의 원인은 V8 CachedData가 패치된 HTML 로드를 차단하는 known-issue와 동일 패턴
- AG가 09:41에 재시작되었지만, 이전 세션에서 삭제한 V8 캐시가 AG 시작과 함께 다시 생성됨
- 해결: V8 캐시 삭제 → AG 재시작 순서 필수 (이번 세션에서 캐시 삭제 완료)
## 미완료
- AG 재시작 후 BEACON 핑 수신 확인
- DOM 덤프 분석: 실제 DOM 구조에서 AI 응답 셀렉터 검증
- 셀렉터 미세조정: bot-color 제거, MarkdownRenderer 타겟팅
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,27 @@
# DOM Observer 데이터 품질 검증 + UTF-8/noise 수정
- **시간**: 2026-04-13 12:34~12:52
- **Commit**: `pending`
- **Vikunja**: #619, #620 (진행 중)
## 검증 결과
- DOM Observer v8 **동작 확인**: `pending/`에 45개 시그널 생성됨 (`source: "dom_observer"`)
- 버튼 분류 정상: `command`(30), `permission`(15)
- 명령어/conversation_id/버튼(Allow/Deny/Cancel) 추출 정상
- **한글 인코딩 깨짐** 발견: description 필드에 `[AI 본문 요약]``[AI <20> <20>]`
## 변경 사항 (v0.5.39)
### http-bridge.ts
- 모든 POST 핸들러에 `req.setEncoding('utf8')` 추가
- Node.js HTTP 서버의 Buffer→string latin1 기본 인코딩으로 인한 multi-byte UTF-8 손실 수정
### observer-script.ts
- `cleanLines()`에 인라인 pre-strip 추가: Material 아이콘명 18종을 regex로 `\n`으로 치환
- `Thought for Xs` 패턴 인라인 제거 추가
- `codeText` 추출에 `cleanLines()` 적용 (이전 미적용)
## 미완료
- AG 재시작 후 v0.5.39 적용 검증 (한글 정상 출력 확인)
- DOM dump 추출 검증
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,28 @@
# DOM Observer 컨텍스트 추출 수정 — v9 (v0.5.40)
- **시간**: 2026-04-13 19:26~
- **Commit**: `pending`
- **Vikunja**: #619, #620 (진행 중)
## 문제
Discord 승인 요청에 내용이 비어있음:
- command = "Running2 commands" (그룹 헤더 버튼을 잘못 캡처)
- description = 비어있거나 UI 노이즈만 포함
- buttons = "Running2 commands / Always run" (잘못된 구조)
## 변경 사항
### observer-script.ts (v8 → v9)
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 3레벨로 확대, 그룹 헤더 스킵, 텍스트 기반 dedup 추가
5. `matchedType` 판별에서 `/Running\d/` 패턴 제거
### http-bridge.ts
6. "Run/Always run" 필터에 `ctx.activeSessionId` 체크 추가 — step-probe가 세션 미추적 시 DOM observer 신호 허용
## 미완료
- AG 재시작 후 v0.5.40 적용 검증
- Discord E2E 검증 (실제 명령어/코드 내용 표시 확인)

View File

@@ -0,0 +1,49 @@
# Observer v9 E2E 검증 + v10-diag 진단 추가
- **시간**: 2026-04-14 07:29~07:36
- **Commit**: `pending`
- **Vikunja**: #task-619 → 진행중
## 검증 결과
### ✅ 동작 확인 (정상)
1. **Observer v9 스크립트 실행**: `/ping` 수신 확인 (22:30:13~)
2. **BEACON ping**: `/ping?beacon=1&t=...` 형태로 GET /ping 수신
3. **DOM dump**: `dump_html_5.json` (7858 bytes) 정상 저장
4. **WS Hub 연결**: `wsConnected: true`
5. **세션 감지**: `activeSessionId: 39c51225` (현재 세션)
6. **pending POST**: `/pending``1776119428450_gnpw` 등 다수 수신
### ❌ 실패: 컨텍스트 추출
- 모든 pending에서 `cmd="Always run"`, `btns=1`, `ctx="Always run"`
- `extractContextFromNearby()` 실패: pre/code 요소를 찾지 못함
- `collectSiblingButtons()` 실패: 1개 버튼만 감지 ("Cancel" 미감지)
- `sessionStalled: true` + `lastPendingStepIndex: -1` (trajectory not found)
### 원인 분석 (추정)
- AG Native UI의 "Always run" 버튼 근처에 `<pre>`, `<code>`, `[class*="terminal"]` 요소가 없음
- 실제 명령어 텍스트가 다른 요소 유형(span, div 등)에 담겨있을 가능성
- DOM 구조가 이전 세션에서 확인한 것과 다를 수 있음 (Settings/Launchpad dump vs 대화 dump)
## 수정 사항 (v10-diag)
### observer-script.ts
- `extractContextFromNearby()`: 각 depth에서 tag/class/codeEls 수를 `_debugTrail`에 기록
- depth ≤ 5에서 `span, div, p` 요소의 실제 텍스트도 trail에 포함
- 성공 시 `CONTEXT-OK`, 실패 시 `CONTEXT-FAIL` 로그 출력
- pending payload에 `_debug_trail` 필드 추가
### http-bridge.ts
- `_handlePending()`에서 `_debug_trail` 필드를 `[HTTP-DIAG] trail:` 로그로 출력
## 미완료
1. **AG 재시작 필요** — v10-diag 빌드 완료 + V8 캐시 삭제됨, AG 재시작 후 trail 분석 필요
2. **trail 분석 후 대응**:
- pre/code 대신 실제 명령어가 담긴 요소 유형 파악
- `extractContextFromNearby()`의 셀렉터 확장 (span/div 등)
- `collectSiblingButtons()` 범위 확장 검토
3. **"trajectory not found" 에러** — AG Native 세션이 Cascade trajectory API에 등록되지 않는 근본 문제 (known-issues에 이미 기록)
## 결정 사항
- 진단을 먼저 진행하는 것이 올바른 접근 — blind fix 대신 실제 DOM 구조를 trail로 확인한 후 정확한 셀렉터 수정
- trail 데이터가 extension.log에 기록되므로 AG 재시작 후 즉시 확인 가능

View File

@@ -0,0 +1,27 @@
# PROMPT_ONLY_RE 근본원인 수정 (v0.5.45)
- **시간**: 2026-04-15 09:12~09:57
- **Commit**: `01539e9`
- **Vikunja**: #619 → 진행중
## 결정 사항
### Template Literal 안의 Regex 리터럴 이스케이핑 규칙
**혼동 포인트**: `observer-script.ts`의 전체 코드는 TypeScript template literal 안에 있다. 이 안에서 regex 리터럴(`/pattern/`)을 쓸 때:
- `\\s` (TS 소스 2-backslash) → template 출력 `\s`**JS에서 invalid escape → 원본 보존** → regex `\s` = whitespace class ✅
- `\\\\s` (TS 소스 4-backslash) → template 출력 `\\s`**JS에서 valid escape `\s`** → regex `\s` = whitespace class ✅
**결론**: 2중과 4중 **둘 다 작동**하지만, 4중이 의도적이고 명시적. 그러나 PROMPT_ONLY_RE는 **기존 4중에서 실패하고 있었으므로** 실제 원인은 다른 곳에 있었음 — `(.*[\\/>»$#]\\\\s*)` 패턴 자체가 `>` 다음에 `\\s*` 매칭이 아닌 `\\\\s*` 리터럴 매칭이 되고 있었던 것.
### http-bridge PROMPT_ONLY_RE 단순화
- 기존: `/^[\s\\\/]*[\w_.-]+\s*[>»$#]\s*$/``…`(U+2026 ellipsis) prefix 미지원
- 변경: `/^.*[>»$#]\s*$/` — prompt marker로 끝나는 모든 텍스트 스킵
- 트레이드오프: `echo >` 같은 극단적 edge case에서 false positive → 1% 미만 확률, 허용
## 미완료
- AG 재시작 후 v0.5.45 실제 동작 확인 필요 (현재 v0.5.44 메모리 로드 상태)
- `Running N commands` 4-backslash 패턴은 정상 동작 확인됨 — 그대로 유지

View File

@@ -0,0 +1,30 @@
# Observer fallback 컨텍스트 추출 수정 (v0.5.46)
- **시간**: 2026-04-15 10:35~11:02
- **Commit**: `pending`
- **Vikunja**: #619 → 진행중
## 결정 사항
### `_promptOnlySkipped` 플래그 설계
**문제**: v0.5.45에서 PROMPT_ONLY_RE가 code/pre 요소의 프롬프트 텍스트를 정상 스킵했으나, `extractContextFromNearby()`**fallback 경로**(span/div/p 텍스트 수집)가 DOM 트리를 올라가면서 채팅 본문, UI 라벨, AI 응답을 명령어로 잘못 추출.
**해결 접근**: code 요소가 존재하지만 **모두** PROMPT_ONLY_RE로 스킵된 경우 → 이 터미널 블록에는 실행할 명령어가 없다고 판단 → fallback span/div/p 수집을 통째로 비활성화.
**대안 검토**:
- ❌ fallback 텍스트에 CJK/자연어 필터 추가 → false negative 위험 (한국어 명령어 경로명 등)
- ❌ fallback 수집 depth 제한 → DOM 구조가 바뀌면 다시 깨짐
-**prompt-only 스킵과 fallback 비활성화 연동** → 가장 간결하고 확실
### VSIX 설치 누락 발견
이전 세션(fd78c28e)에서 v0.5.45 VSIX를 빌드했으나 **설치를 하지 않았음**. extensions.json 확인 결과 v0.5.43이 설치되어 있었음. 원인: 이전 세션에서 `code --install-extension` 실행 없이 AG 재시작만 수행.
→ known-issues에 "빌드 후 즉시 install 확인 필수" 주의사항 추가
## 미완료
- AG 재시작 후 v0.5.46 실제 동작 검증 필요
- Discord에 빈 프롬프트/채팅 텍스트가 전송되지 않는지 확인
- 검증 완료 후 devlog에 커밋 해시 업데이트 + Vikunja #619 완료 처리

View File

@@ -0,0 +1,38 @@
# v16 터미널 출력 필터 + v15 Stale LS 자동복구 + Heartbeat Probe (v0.5.50)
- **시간**: 2026-04-16 04:52~05:00
- **Commit**: `7ade31e`
- **Vikunja**: #619 → 진행중
## 결정 사항
### 터미널 출력(stdout)이 명령어로 추출되는 버그 수정 (v16)
**증상**: Discord에 `cmd="No extension.log found"`, `cmd="AG CLI not found..."`, `cmd="Log found: C:\..."` 등 터미널의 **출력** 텍스트가 명령어로 전송됨.
**원인**: Observer의 `extractContextFromNearby()`가 code 블록 2개를 찾음:
1. ci=0: 프롬프트+명령어 (`…\gravity_control > $log = ...`) → JUNK_CODE_RE로 스킵
2. ci=1: 터미널 출력 (`No extension.log found`) → 유효한 code로 판단 → description에 포함
http-bridge enrichment에서 description에 prompt marker(`>`)가 없으면 rawDesc 전체를 enrichedCmd로 채택하여 Discord로 전송.
**해결**: `promptMatch` 실패 시 (description에 `>` 없음) → 터미널 OUTPUT으로 판단하여 즉시 필터. 실제 명령어는 항상 `…\project > command` 형식의 프롬프트를 포함.
### step-probe v15 — Stale LS 자동감지 + Heartbeat Probe
- **Stale LS**: 모든 세션이 5분 이상 오래되면 주기적으로 `fixLSConnection()` 시도
- **Heartbeat Probe**: 매 10 polls마다 `GetCascadeTrajectorySteps`를 직접 호출하여 summary API가 frozen일 때도 step 변화 감지
- **fixLSConnection() fallback**: `--workspace_id` 없는 LS 프로세스(AG 재시작 직후 주로 발생)도 fallback으로 매칭
## 검증 결과
- ✅ Observer v14 동작 중 — POST /pending 신호 정상
- ✅ Generic button 필터 작동 — "Always run" desc="Always run" → 필터됨
- ✅ Command enrichment 작동 — "Always run" → "git diff --stat" 등 정상 추출
- ✅ 다수 명령어(git, cmd /c, Get-Content, code, Remove-Item 등) 정상 추출 확인
- ❌→✅ 터미널 출력 텍스트 누출 버그 발견 → v16에서 수정
## 미완료
- AG 재시작 후 v0.5.50 실제 동작 확인 필요
- v15 stale LS 자동복구 + heartbeat probe 실동작 확인 (장시간 세션 필요)

View File

@@ -0,0 +1,24 @@
# AG Native AI 응답 Discord 릴레이 구현 (Observer v15)
- **시간**: 2026-04-16 04:52~05:28
- **Commit**: `729875f`
- **Vikunja**: #632 → 진행중
## 문제 분석
AG Native 세션에서 AI 대화 응답이 Discord에 전혀 전달되지 않는 근본원인을 규명:
1. **SDK 경로 차단**: `GetCascadeTrajectorySteps(cascadeId)``trajectory not found`. AG Native는 Cascade trajectory API에 미등록 → stepCount=1 고정, delta=0 → RT-CAPTURE 진입 불가
2. **DOM 경로 차단**: `scanChatBodies()``conversation-view`, `data-step-index` 등 Cascade 전용 셀렉터 사용 → AG Native DOM에 전무 → 즉시 return
## 결정 사항
- SDK 경로는 AG 구조적 한계로 사용 불가 → **DOM 경로를 AG Native에 맞게 확장**
- AG Native DOM 분석 결과: `#conversation` (id), `.leading-relaxed.select-text` (AI 응답 영역) 확인
- 기존 Cascade 경로도 유지하여 호환성 보장 (이중 전략)
## 미완료
- **AG Reload Window 필요**: v15 Observer가 workbench.html에 패치되려면 AG 재시작 필수
- **실동작 검증**: Discord에 AI 응답 텍스트가 실제로 수신되는지 end-to-end 확인
- **enrichment 오탐 edge case**: 로그 텍스트 내 `>` 문자가 prompt marker로 오인되는 1건 (빈도 낮음, v17에서 수정 검토)

View File

@@ -0,0 +1,17 @@
# DOM Observer 마크다운 구조 복원 및 사용자 메시지 연동 (v0.5.56)
### 목표
DOM Observer(`observer-script.ts`)가 AI 채팅을 `innerText`로 추출하며 잃어버리는 마크다운 서식을 복원하고, 사용자(User) 메시지도 포착하여 함께 Discord 봇으로 보내기 (#634 이슈).
### 변경 사항
1. **`convertNodeToMarkdown` 파서 확장**:
- AI 채팅창의 DOM Tree를 순회하며 `<h1>`~`<h4>`, `<p>`, `<ul>`, `<ol>`, `<li>`, `<strong>`, `<em>`, `<code>`, `<pre>`, `<blockquote>` 등 대부분의 마크다운 요소를 파싱하는 로직 도입.
- 추가로 `<a>` 태그(Link) 속성을 지원하여 `[text](href)` 형태로 복원하도록 개선.
2. **파괴적인 `cleanLines()` 노이즈 필터 제거**:
- 이전에 사용되던 `cleanLines()``}[공백]`이나 `import` 같은 코드를 UI 노이즈로 오인하여 삭제(Drop)하는 심각한 이슈를 발견. 전체 마크다운 문자열에는 해당 필터를 적용하지 않고 정규식을 통해 `Thought for X s` 형태의 메시지만 지우도록 수정.
3. **User 메시지 대상 추가**:
- `scanChatBodies()`의 탐색 Selector에 `.text-ide-message-block-user-color`, `.bg-ide-message-block-user-background` 등을 추가하여 사용자 메시지 블록도 대상에 포함.
- 데이터 전송 시 `role: 'user'` 정보를 보내고, `http-bridge.ts`에서 이를 구분하여 헤더를 `🧑‍💻 **[DOM 추출] 사용자 요청**`로 지정해 Discord로 릴레이.
### 결과
`v0.5.56` VSIX 배포 준비 완료 (v0.5.54/55 빌드는 테스트 과정 중 건너뜀). AG Native에서 확장 설치 캐시를 리셋하거나 직접 VSIX를 설치하면 적용됨.

View File

@@ -0,0 +1,31 @@
# Retry Auto-Approve 흐름 복구 및 Observer 고도화
- **시간**: 2026-04-18 09:20~23:50
- **Commit**: `pending`
- **Vikunja**: 신규 생성 예정
## 결정 사항
### 1. `_from_ws` 마커 기반 response 파일 보존
- **문제**: WS 응답 핸들러가 response 파일 작성 → processResponseFile이 300ms 후 삭제 → Observer pollResponseGroup 실패
- **선택**: response 파일에 `_from_ws: true` 마커 추가, processResponseFile에서 스킵
- **이유**: pending 파일 생성을 추가하는 것보다 단순하고, WS 핸들러에서 이미 tryApprovalStrategies를 실행하므로 중복 실행 방지도 함께 해결
### 2. 형제(sibling) DOM 탐색
- **문제**: "Always run" 버튼의 조상(parentElement) 탐색으로는 `pre.font-mono` 도달 불가 (footer.parentElement가 null)
- **선택**: 각 depth에서 `node.parentElement.children`을 순회하여 형제 요소의 code 블록 탐색
- **이유**: AG Native DOM 구조에서 명령어는 footer의 형제 요소에 있으므로 조상 탐색만으로는 구조적으로 불가
### 3. Thinking 블록 필터링
- **문제**: AI의 내부 사고 과정이 Discord에 릴레이됨
- **선택**: `max-h-[200px]` 조상 확인으로 thinking 블록 식별
- **이유**: thinking 블록은 접힌 상태에서 max-height가 200px로 제한되는 특징이 있음
## 시행착오
1. depth 5→10 증가만으로 해결 시도 → 실패 (조상이 아닌 형제에 명령어가 있었음)
2. Observer HTML 변경 후 Reload Window만 실행 → 실패 (AG 2번 재시작 필요)
3. response 파일이 삭제되는 원인을 clickTrigger 타이밍으로 오인 → 실제는 processResponseFile의 isDomObserver 판별 실패
## 미완료
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패)
- [ ] Observer pollResponseGroup이 시작되지 않는 케이스 (POST /pending 이전에 trigger-click 소비)

View File

@@ -2,14 +2,16 @@
## 시작하기
### 1. 봇 실행
### 1. 서버 (Docker Gateway)
```batch
start_bot.bat
```
또는:
```powershell
C:\ProgramData\miniforge3\envs\gravity_control\python.exe main.py
서버에서 Bot + Hub + Gateway를 Docker로 실행:
```bash
git clone https://git.variet.net/Variet/gravity_control.git
cd gravity_control
cp .env.example .env # DISCORD_TOKEN, DISCORD_GUILD_ID 등 설정
docker compose up -d
docker compose logs -f # 로그 확인
```
### 2. Extension 설치
@@ -21,6 +23,34 @@ cmd /c npx vsce package
# 생성된 .vsix 파일을 VS Code에서 설치
```
### 3. Extension 설정
VS Code `settings.json`에 Hub 연결 정보 추가:
```json
{
"gravityBridge.hubUrl": "wss://ag.variet.net/ws",
"gravityBridge.registrationCode": "<GRAVITY_REGISTRATION_CODE 값>"
}
```
> **참고**: 환경변수 `GRAVITY_HUB_URL`, `GRAVITY_REGISTRATION_CODE`로도 설정 가능하나, VS Code 시작 전에 설정되어야 합니다. `settings.json` 사용을 권장합니다.
---
## 아키텍처
```
[AG IDE] ← Extension ──WS──→ Hub (서버) ←→ Bot ←→ Discord
│ │
└── step_probe └── pending_owners
(WAITING 감지) (응답 라우팅)
```
- **Extension → Hub**: WebSocket으로 pending, chat_snapshot, register 전송
- **Hub → Extension**: WebSocket으로 command, response 실시간 전송
- **File bridge**: WS 미연결 시 폴백 (로컬 `~/.gemini/antigravity/bridge/`)
---
## Discord 명령어
@@ -31,6 +61,7 @@ cmd /c npx vsce package
|--------|------|
| `!auto` | 자동 승인 토글 (on/off 반복) |
| `!stop` | AG 에이전트 중단 |
| `!N 텍스트` | N번 PC 인스턴스에만 전달 (예: `!2 진행해`) |
| 그 외 텍스트 | AG에 직접 메시지 전달 |
### 슬래시 명령어
@@ -51,103 +82,45 @@ Discord에서 `!auto` 를 입력할 때마다 on↔off 토글됩니다.
- **OFF (기본)**: 승인 요청마다 Discord에 ✅/❌ 버튼 표시 → 클릭하여 수동 승인
- **ON**: 승인 요청 시 자동으로 승인 → Discord에 `🤖 자동 승인됨` 표시
```
사용자: !auto
봇: 🟢 자동 승인 모드
프로젝트: gravity_control
모든 승인 요청이 자동으로 승인됩니다
사용자: !auto
봇: 🔴 수동 승인 모드
프로젝트: gravity_control
모든 승인 요청이 수동 확인이 필요합니다
```
### 자동 승인 시 Discord 표시
```
🤖 자동 승인됨
┌─────────────────────────────┐
│ run_command: npm run build │
└─────────────────────────────┘
auto-approve | 1741678...
```
> **주의**: 봇 재시작 시 auto-approve 상태는 초기화됩니다 (기본 OFF).
---
## 아키텍처
```
[AG IDE] ← Extension → bridge/ ← Bot → Discord
│ │
└── step_probe └── pending 스캔
(WAITING 감지) (자동 승인 처리)
```
### Bridge 프로토콜
```
~/.gemini/antigravity/bridge/
├── pending/ Extension → Bot (승인 요청)
├── response/ Bot → Extension (승인 결과)
├── commands/ Bot → Extension (사용자 명령)
└── register/ Extension → Bot (세션 매핑)
```
### Bot Mode
## Bot Mode
| 모드 | 설정 | 설명 |
|------|------|------|
| `local` (기본) | `BOT_MODE=local` | 로컬 파일시스템 bridge 사용 |
| `remote` (미래) | `BOT_MODE=remote` | HTTP로 원격 bridge 폴링 (Collector 모드) |
| `gateway` | `BOT_MODE=gateway` | 서버에서 Discord 통신 + HTTP API (Docker용) |
| `gateway` | `BOT_MODE=gateway` | 서버: Bot + Hub WS + Gateway HTTP API (Docker) |
| `local` | `BOT_MODE=local` | 로컬: 파일 bridge 전용 (Hub 없이 단독 실행) |
| `remote` | `BOT_MODE=remote` | ~~Collector 모드~~ **(deprecated — WS Hub로 대체됨)** |
---
## 설정 (.env)
```env
DISCORD_TOKEN=xxx # Discord 봇 토큰 (필수)
DISCORD_GUILD_ID=xxx # Discord 서버 ID (필수)
BRAIN_PATH= # AG 브레인 경로 (기본: ~/.gemini/antigravity/brain)
BOT_MODE=local # 봇 모드 (local/remote)
REMOTE_BRIDGE_URL= # 원격 브릿지 URL (remote 모드 전용)
DEBOUNCE_SECONDS=2 # 이벤트 디바운스 (초)
DISCORD_TOKEN=xxx # Discord 봇 토큰 (필수)
DISCORD_GUILD_ID=xxx # Discord 서버 ID (필수)
BRAIN_PATH= # AG 브레인 경로 (기본: ~/.gemini/antigravity/brain)
BOT_MODE=gateway # 봇 모드 (gateway/local)
GATEWAY_PORT=8585 # Gateway 포트
GATEWAY_API_KEY=xxx # Gateway API 인증 키
GRAVITY_REGISTRATION_CODE=xxx # Extension WS 인증 코드
```
---
## Docker 배포 (Gateway)
서버에서 Gateway 봇을 Docker로 실행:
```
[로컬 PC] [서버 Docker]
Extension → bridge/ ← 로컬 Bot ──HTTP──→ Gateway :8585 ←→ Discord
(Collector)
```
### 서버에서 실행
```bash
git clone https://git.variet.net/Variet/gravity_control.git
cd gravity_control
cp .env.example .env # DISCORD_TOKEN, DISCORD_GUILD_ID 입력
docker compose up -d
docker compose logs -f # 로그 확인
```
### Gateway API
## Gateway API
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | `/health` | 헬스체크 |
| POST | `/api/pending` | Collector → 승인 요청 |
| GET | `/api/response/{rid}` | Collector ← 승인 응답 |
| POST | `/api/chat` | Collector → 채팅 스냅샷 |
| GET | `/api/commands/{project}` | Collector ← 명령 폴링 |
| GET | `/ws` | Extension WebSocket 연결 |
| GET | `/hub/status` | Hub 연결 상태 |
| POST | `/api/pending` | 승인 요청 (API/Collector) |
| GET | `/api/response/{rid}` | 승인 응답 조회 |
| POST | `/api/chat` | 채팅 스냅샷 전송 |
| GET | `/api/commands/{project}` | 명령 폴링 |
---
@@ -155,6 +128,8 @@ docker compose logs -f # 로그 확인
| 증상 | 해결 |
|------|------|
| 승인 클릭해도 AG 반응 없음 | Hub WS 연결 확인: `/hub/status` → Extension이 connected인지 |
| `!auto` 했는데 자동 승인 안 됨 | Extension VSIX 재빌드 + 재설치 필요 |
| 봇 재시작 후 auto가 꺼져있음 | 정상 — `!auto`로 다시 켜기 |
| Python 못 찾음 | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` 사용 |
| Discord 메시지 이중 전달 | 로컬 Collector (`python main.py` BOT_MODE=remote) 실행 여부 확인 — 종료 필요 |

View File

@@ -1,5 +1,6 @@
src/**
node_modules/**
!node_modules/ws/**
*.ts
!out/**
tsconfig.json

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,58 +1,380 @@
{
"name": "gravity-bridge",
"version": "0.3.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gravity-bridge",
"version": "0.3.8",
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"engines": {
"vscode": "^1.100.0"
}
},
"node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz",
"integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"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"
"name": "gravity-bridge",
"version": "0.5.103",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gravity-bridge",
"version": "0.5.103",
"dependencies": {
"cheerio": "^1.2.0",
"ws": "^8.19.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"engines": {
"vscode": "^1.100.0"
}
},
"node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz",
"integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==",
"dev": true,
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/cheerio": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"encoding-sniffer": "^0.2.1",
"htmlparser2": "^10.1.0",
"parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^7.19.0",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=20.18.1"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/htmlparser2": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"entities": "^7.0.1"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"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"
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -1,75 +1,90 @@
{
"name": "gravity-bridge",
"displayName": "Gravity Bridge",
"description": "Antigravity ↔ Discord 브리지 연동 확장",
"version": "0.3.14",
"publisher": "variet",
"engines": {
"vscode": "^1.100.0"
},
"categories": [
"Other",
"Chat"
"name": "gravity-bridge",
"displayName": "Gravity Bridge",
"description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.103",
"publisher": "variet",
"engines": {
"vscode": "^1.100.0"
},
"categories": [
"Other",
"Chat"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./out/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./ && node -e \"const fs=require('fs'),p=require('path');const s=p.join('src','sdk'),d=p.join('out','sdk');if(fs.existsSync(s)){fs.mkdirSync(d,{recursive:true});fs.readdirSync(s).forEach(f=>fs.copyFileSync(p.join(s,f),p.join(d,f)));console.log('SDK copied to out/sdk')};\"",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"contributes": {
"chatParticipants": [
{
"id": "gravity-bridge.gravity",
"name": "gravity",
"fullName": "Gravity Bridge",
"description": "?<3F>???<3F>스?<3F><EFBFBD>?Discord<72>??<3F>송 + AI ?<3F>어",
"isSticky": false
}
],
"activationEvents": [
"onStartupFinished"
"commands": [
{
"command": "gravityBridge.start",
"title": "Gravity Bridge: Start"
},
{
"command": "gravityBridge.stop",
"title": "Gravity Bridge: Stop"
},
{
"command": "gravityBridge.approve",
"title": "Gravity Bridge: Approve Pending"
},
{
"command": "gravityBridge.reject",
"title": "Gravity Bridge: Reject Pending"
},
{
"command": "gravityBridge.connect",
"title": "Gravity Bridge: Connect Session"
}
],
"main": "./out/extension.js",
"scripts": {
"compile": "tsc -p ./ && node -e \"const fs=require('fs'),p=require('path');const s=p.join('src','sdk'),d=p.join('out','sdk');if(fs.existsSync(s)){fs.mkdirSync(d,{recursive:true});fs.readdirSync(s).forEach(f=>fs.copyFileSync(p.join(s,f),p.join(d,f)));console.log('SDK copied to out/sdk')};\"",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.3.0"
},
"contributes": {
"chatParticipants": [
{
"id": "gravity-bridge.gravity",
"name": "gravity",
"fullName": "Gravity Bridge",
"description": "대화 히스토리를 Discord로 전송 + AI 제어",
"isSticky": false
}
],
"commands": [
{
"command": "gravityBridge.start",
"title": "Gravity Bridge: Start"
},
{
"command": "gravityBridge.stop",
"title": "Gravity Bridge: Stop"
},
{
"command": "gravityBridge.approve",
"title": "Gravity Bridge: Approve Pending"
},
{
"command": "gravityBridge.reject",
"title": "Gravity Bridge: Reject Pending"
},
{
"command": "gravityBridge.connect",
"title": "Gravity Bridge: Connect Session"
}
],
"configuration": {
"title": "Gravity Bridge",
"properties": {
"gravityBridge.bridgePath": {
"type": "string",
"default": "",
"description": "Bridge 디렉토리 경로 (기본: ~/.gemini/antigravity/bridge)"
},
"gravityBridge.projectName": {
"type": "string",
"default": "",
"description": "프로젝트 이름 (기본: git remote 레포명)"
}
}
"configuration": {
"title": "Gravity Bridge",
"properties": {
"gravityBridge.bridgePath": {
"type": "string",
"default": "",
"description": "Bridge ?<3F>렉?<3F>리 경로 (기본: ~/.gemini/antigravity/bridge)"
},
"gravityBridge.projectName": {
"type": "string",
"default": "",
"description": "?<3F>로?<3F>트 ?<3F>름 (기본: git remote ?<3F><EFBFBD>?"
},
"gravityBridge.hubUrl": {
"type": "string",
"default": "",
"description": "WebSocket Hub URL (?? wss://your-server.com/ws)"
},
"gravityBridge.registrationCode": {
"type": "string",
"default": "",
"description": "Hub ?<3F>록 코드 (?<3F>버?<3F>서 발급)"
}
}
}
}
},
"dependencies": {
"cheerio": "^1.2.0",
"ws": "^8.19.0"
}
}

View File

@@ -0,0 +1,98 @@
const fs = require('fs');
const path = require('path');
// Try all available dumps
const bridgePath = path.join(process.env.USERPROFILE, '.gemini/antigravity/bridge');
const dumpFiles = ['dump_html_1.json', 'dump_html_2.json', 'dump_html_3.json', 'dump_html_4.json', 'dump_html_5.json', 'deep-inspect-result.json', 'deep-inspect-manual.json'];
function printTree(node, indent, maxDepth) {
if (!node || indent > maxDepth) return;
const tag = (node.tag || node.tagName || '?').toLowerCase();
const clsArr = (node.cls || node.className || '').split(' ').filter(c => c.length > 0);
const text = (node.text || node.textContent || '').substring(0, 50).replace(/[\n\r]+/g, ' ');
const childCount = (node.children || []).length;
let line = ' '.repeat(indent) + tag;
if (clsArr.length > 0) line += '.' + clsArr[0];
if (childCount) line += ' [' + childCount + ']';
if (text && childCount === 0) line += ' = "' + text + '"';
console.log(line);
if (node.children) {
for (const c of node.children) printTree(c, indent + 1, maxDepth);
}
}
// Find the conversation area in each dump
function findConvo(node, depth) {
if (!node || depth > 20) return null;
const cls = node.cls || node.className || '';
if (cls.includes('bg-agent-convo-background') || cls.includes('agent-convo')) return node;
if (node.children) {
for (const c of node.children) {
const r = findConvo(c, depth + 1);
if (r) return r;
}
}
return null;
}
// Find buttons (Run, Allow, Always run, Accept)
function findButtons(node, depth, results) {
if (!node || depth > 25) return;
const tag = (node.tag || node.tagName || '').toLowerCase();
const text = (node.text || node.textContent || '');
if (tag === 'button' && /Run|Allow|Accept|Always/i.test(text) && text.length < 50) {
results.push({ text: text.trim(), depth });
}
if (node.children) {
for (const c of node.children) findButtons(c, depth + 1, results);
}
}
// Find pre/code blocks near buttons
function findCodeBlocks(node, depth, results) {
if (!node || depth > 25) return;
const tag = (node.tag || node.tagName || '').toLowerCase();
const cls = node.cls || node.className || '';
if ((tag === 'pre' || tag === 'code') && cls.includes('font-mono')) {
const text = (node.text || node.textContent || '').substring(0, 80);
results.push({ tag, cls: cls.substring(0, 50), text, depth });
}
if (node.children) {
for (const c of node.children) findCodeBlocks(c, depth + 1, results);
}
}
for (const df of dumpFiles) {
const fp = path.join(bridgePath, df);
if (!fs.existsSync(fp)) continue;
try {
const dump = JSON.parse(fs.readFileSync(fp, 'utf8'));
const root = dump.bodyTree || dump.body || dump;
console.log('\n=== ' + df + ' ===');
// Find conversation area
const convo = findConvo(root, 0);
if (convo) {
console.log('>> Conversation area found, tree (depth 12):');
printTree(convo, 0, 12);
}
// Find buttons
const btns = [];
findButtons(root, 0, btns);
if (btns.length > 0) {
console.log('>> Buttons found:');
for (const b of btns) console.log(' d' + b.depth + ': ' + b.text);
}
// Find code blocks
const codes = [];
findCodeBlocks(root, 0, codes);
if (codes.length > 0) {
console.log('>> Code blocks (font-mono):');
for (const c of codes) console.log(' d' + c.depth + ': <' + c.tag + '> cls=' + c.cls + ' text="' + c.text + '"');
}
} catch (e) {
console.log('ERROR reading ' + df + ': ' + e.message);
}
}

View File

@@ -0,0 +1,28 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const dump = JSON.parse(fs.readFileSync(
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
));
const bodyStr = JSON.stringify(dump.body);
// Find all unique tag names
const tagMatches = bodyStr.match(/"tag":"[a-z0-9]+"/g) || [];
const uniqueTags = [...new Set(tagMatches)];
console.log('=== Unique DOM tags ===');
console.log(uniqueTags.sort().join('\n'));
// Check for pipe characters (markdown table syntax)
console.log('\n=== Pipe | in text content ===');
const pipeMatches = [...bodyStr.matchAll(/"text":"[^"]*\|[^"]*"/g)];
console.log(`Found ${pipeMatches.length} text nodes with pipe |`);
pipeMatches.slice(0, 5).forEach(m => console.log(' ', m[0].substring(0, 120)));
// Check for table-related class names
console.log('\n=== Table-related classes ===');
const classMatches = bodyStr.match(/"cls":"[^"]*"/g) || [];
const tableClasses = classMatches.filter(c => /table|grid|cell|col|row/i.test(c));
console.log(`Found ${tableClasses.length} table-related classes`);
[...new Set(tableClasses)].slice(0, 10).forEach(c => console.log(' ', c));

View File

@@ -0,0 +1,2 @@
# diff_review detection test v2
test_value = "hello"

View File

@@ -0,0 +1,37 @@
// List all channels in the guild
const https = require('https');
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
const GUILD_ID = '1478722210460991662';
function apiGet(path) {
return new Promise((resolve, reject) => {
const opts = {
hostname: 'discord.com',
path: `/api/v10${path}`,
headers: { 'Authorization': `Bot ${TOKEN}` }
};
https.get(opts, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve(JSON.parse(data)); } catch { resolve(data); }
});
}).on('error', reject);
});
}
async function main() {
const channels = await apiGet(`/guilds/${GUILD_ID}/channels`);
if (!Array.isArray(channels)) {
console.log('Error:', channels);
return;
}
console.log(`Total channels: ${channels.length}\n`);
channels.sort((a,b) => (a.position||0) - (b.position||0));
channels.forEach(c => {
const type = ['TEXT','DM','VOICE','GROUP_DM','CATEGORY','ANNOUNCE','','','','','','THREAD','THREAD','THREAD','','FORUM','MEDIA'][c.type] || c.type;
console.log(`${c.id} | ${type.padEnd(10)} | #${c.name}`);
});
}
main().catch(e => console.error(e));

View File

@@ -0,0 +1,55 @@
// Read latest Discord messages from ag-gravity_control channel
const https = require('https');
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
const CHANNEL_ID = '1483082084540223663';
function apiGet(path) {
return new Promise((resolve, reject) => {
const opts = {
hostname: 'discord.com',
path: `/api/v10${path}`,
headers: { 'Authorization': `Bot ${TOKEN}` }
};
https.get(opts, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve(JSON.parse(data)); } catch { resolve(data); }
});
}).on('error', reject);
});
}
async function main() {
const limit = process.argv[2] || 15;
const msgs = await apiGet(`/channels/${CHANNEL_ID}/messages?limit=${limit}`);
if (!Array.isArray(msgs)) {
console.log('Error:', JSON.stringify(msgs));
return;
}
console.log(`=== #ag-gravity_control — Last ${msgs.length} messages ===\n`);
msgs.reverse().forEach(m => {
const time = new Date(m.timestamp).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', second: '2-digit' });
const author = m.author?.username || '?';
if (m.embeds?.length > 0) {
m.embeds.forEach(e => {
const title = e.title || '(no title)';
const desc = (e.description || '').substring(0, 300);
const colorHex = e.color ? `#${e.color.toString(16).padStart(6, '0')}` : 'default';
const footer = e.footer?.text || '';
console.log(`[${time}] 📦 EMBED [${colorHex}] ${title}`);
if (desc) console.log(` ${desc.replace(/\n/g, '\n ')}`);
if (footer) console.log(` 📎 ${footer}`);
});
} else if (m.content) {
const content = m.content.substring(0, 300);
console.log(`[${time}] 💬 ${author}: ${content}`);
}
console.log('');
});
}
main().catch(e => console.error(e));

View File

@@ -0,0 +1,29 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const d = JSON.parse(fs.readFileSync(
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
));
const s = JSON.stringify(d.body);
console.log('title:', d.quickInfo.title);
console.log('Has id=conversation:', s.includes('"id":"conversation"'));
console.log('Has agent-side-panel:', s.includes('antigravity-agent-side-panel'));
// Find message-block patterns
const mb = [...s.matchAll(/message-block/g)];
console.log('message-block occurrences:', mb.length);
// Find user-related class patterns
const userPatterns = ['user-color', 'user-background', 'user-message', 'user-query', 'user-input', 'human'];
userPatterns.forEach(p => {
const cnt = [...s.matchAll(new RegExp(p, 'gi'))].length;
if (cnt > 0) console.log(` ${p}: ${cnt} occurrences`);
});
// Show all unique classes that include 'message' or 'chat' or 'conversation'
const clsMatches = [...s.matchAll(/"cls":"([^"]*(?:message|chat|conversation|query|user|human)[^"]*)"/gi)];
console.log('\nClasses with message/chat/conversation/user/human:');
const uniq = [...new Set(clsMatches.map(m => m[1]))];
uniq.forEach(c => console.log(' ', c.substring(0, 120)));

View File

@@ -0,0 +1,26 @@
const fs = require('fs');
const path = require('path');
const dump = JSON.parse(fs.readFileSync(
path.join(process.env.USERPROFILE, '.gemini/antigravity/bridge/deep-inspect-result.json'), 'utf8'
));
function printTree(node, indent, maxDepth) {
if (!node || indent > maxDepth) return;
const tag = (node.tag || '?').toLowerCase();
const clsArr = (node.cls || '').split(' ').filter(c => c.length > 0);
const clsShort = clsArr.slice(0, 3).join(' ');
const text = (node.text || '').substring(0, 40).replace(/[\n\r]+/g, ' ');
const childCount = (node.children || []).length;
let line = ' '.repeat(indent) + tag;
if (clsShort) line += '.' + clsArr[0];
if (childCount) line += ' [' + childCount + ' children]';
if (text && childCount === 0) line += ' = "' + text + '"';
console.log(line);
if (node.children) {
for (const c of node.children) printTree(c, indent + 1, maxDepth);
}
}
console.log('=== FULL DOM TREE (depth 8) ===');
printTree(dump.bodyTree || dump.body, 0, 8);

View File

@@ -0,0 +1,22 @@
// Test: call agentAcceptAllInFile via extension's HTTP bridge trigger-click
// This simulates what the approval handler does
const http = require('http');
const PORT = 34332; // from observer setup log
// Write a trigger-click file to make Observer click "Accept all"
const fs = require('fs');
const path = require('path');
const bridgePath = path.join(process.env.USERPROFILE, '.gemini', 'antigravity', 'bridge');
// Check if there's a trigger_click.json
const triggerFile = path.join(bridgePath, 'trigger_click.json');
console.log('Writing trigger_click.json for accept...');
fs.writeFileSync(triggerFile, JSON.stringify({ action: 'approve', type: 'diff_review', ts: Date.now() }), 'utf-8');
console.log('Done. Check if Accept all was clicked.');
// Also check extension log for recent entries
const logFile = path.join(bridgePath, 'extension.log');
const lines = fs.readFileSync(logFile, 'utf-8').split('\n');
const recent = lines.slice(-5);
recent.forEach(l => console.log(l.substring(0, 200)));

View File

@@ -0,0 +1,114 @@
// Final simulation: exact v0.5.96 flow with realistic DOM
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
try { new Function(s); console.log('SYNTAX: OK'); } catch(e) { console.log('SYNTAX ERROR:', e.message); process.exit(1); }
let promptRe = /[\u003e\u00bb\u276f]\s+(.+)/;
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
let iconFilterRe = /^(content_copy|content_paste|play_arrow|check_circle|chevron_|keyboard_arrow|more_horiz|more_vert|expand_|alternate_email|arrow_drop)/;
// Realistic scenario: "Running command" div has siblings including a copy button
// The actual DOM probably has a structure like:
// div "Running command"
// span/div with the copy icon (textContent = "> content_copy" or just "content_copy")
// div with the actual prompt+command
// div with the buttons
function v31_simulate(name, siblings) {
console.log('\n=== ' + name + ' ===');
// Step 1: Find "Running command" header
let rcIdx = -1;
for (let i = 0; i < siblings.length; i++) {
let t = siblings[i].trim();
if (t === 'Running command' || (t.indexOf('Running command') !== -1 && t.length < 30)) {
rcIdx = i;
break;
}
}
if (rcIdx < 0) { console.log(' NO RC HEADER'); return; }
// Step 2: Collect candidates (filter icons and buttons)
let cands = [];
for (let i = 0; i < siblings.length; i++) {
if (i === rcIdx) continue;
let t = siblings[i].trim();
if (t.length < 5) continue;
if (iconFilterRe.test(t)) { console.log(' FILTER icon: "' + t.substring(0,40) + '"'); continue; }
if (/^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i.test(t)) { console.log(' FILTER btn: "' + t.substring(0,40) + '"'); continue; }
if (t.indexOf('Always run') !== -1 && t.indexOf('Cancel') !== -1) { console.log(' FILTER btn-bar: "' + t.substring(0,40) + '"'); continue; }
cands.push(t);
console.log(' CANDIDATE: "' + t.substring(0,60) + '" (len=' + t.length + ')');
}
// Step 3: Sort by length (longest first)
cands.sort((a,b) => b.length - a.length);
// Step 4: Extract command from best candidate
for (let cand of cands) {
let m = promptRe.exec(cand);
if (m && m[1].trim().length > 3) {
let cmdV = m[1].trim().replace(stripRe, '').trim();
if (cmdV.length < 3) { console.log(' SKIP (too short after strip): "' + cmdV + '"'); continue; }
if (/^(Always|Run|Allow|Cancel|Deny)/i.test(cmdV)) continue;
console.log(' EXTRACTED: "' + cmdV + '"');
return;
}
if (cand.length > 10 && /[\u276f\u003e]/.test(cand)) {
let raw = cand.replace(stripRe, '').trim();
console.log(' EXTRACTED (raw): "' + raw + '"');
return;
}
}
console.log(' NO MATCH - will fallback to "Always run"');
}
// Scenario A: What ACTUALLY happened (3 siblings, "content_copy" mixed in command text)
v31_simulate('A: Icon in command text', [
'Running command',
'\u276f gravity_control > Start-Sleep 12; $logFile content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Scenario B: Copy button as separate small div
v31_simulate('B: Icon as separate div + command div', [
'Running command',
'> content_copy',
'\u276f gravity_control > npm run compile',
'Always run Cancel'
]);
// Scenario C: Just "content_copy" standalone (no >)
v31_simulate('C: Standalone icon + command', [
'Running command',
'content_copy',
'\u276f gravity_control > git push origin main',
'Always run Cancel'
]);
// Scenario D: Multiple icons mixed
v31_simulate('D: Multiple icons + command', [
'Running command',
'play_arrow',
'> content_copy',
'\u276f gravity_control > node -e "console.log(1)" content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Scenario E: Edge - no command, only prompt
v31_simulate('E: Prompt only', [
'Running command',
'\u276f gravity_control > ',
'Always run Cancel'
]);
// Scenario F: The v0.5.95 cmdV=content_copy case
// This implies regex matched "content_copy" from a "> content_copy" sibling
// and there was no longer sibling
v31_simulate('F: Only icon sibling (worst case)', [
'Running command',
'> content_copy',
'Always run Cancel'
]);
console.log('\n=== SIMULATION COMPLETE ===');

View File

@@ -0,0 +1,43 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Extract the regex strings
let junkMatch = s.match(/JUNK_CODE_RE\s*=\s*(\/[^;]+)/);
let promptMatch = s.match(/PROMPT_ONLY_RE\s*=\s*(\/[^;]+)/);
console.log('JUNK_CODE_RE:', junkMatch[1].substring(0, 100));
console.log('PROMPT_ONLY_RE:', promptMatch[1]);
// Use eval to construct the actual regexes
let JUNK = eval(junkMatch[1]);
let PROMPT = eval(promptMatch[1]);
let tests = [
['\u276f gravity_control > ', 'prompt only (no command)'],
['\u276f extension > ', 'prompt only (extension)'],
['\u276f gravity_control > $logFile = Join-Path $env:USERPROFILE', 'PS var assignment'],
['\u276f extension > npm.cmd run compile', 'npm compile'],
['\u276f gravity_control > Start-Sleep 12', 'Start-Sleep'],
['\u276f gravity_control > git add -A; git commit -m "test"', 'git commit'],
['\u276f gravity_control > node -e "const {gen}=require()"', 'node with require'],
['\u276f gravity_control > Get-Content file.txt', 'Get-Content'],
['\u276f gravity_control > npm.cmd version patch', 'npm version'],
['function test() { return 1; }', 'JS function (should be JUNK)'],
['const x = require("fs")', 'JS const (should be JUNK)'],
['import { foo } from "bar"', 'JS import (should be JUNK)'],
];
console.log('\n=== CODE ELEMENT FILTER ANALYSIS ===');
for (let [text, desc] of tests) {
let isJunk = JUNK.test(text);
let isPrompt = PROMPT.test(text.trim());
let junkPart = isJunk ? text.match(JUNK)[0] : null;
let status;
if (isPrompt) status = 'SKIP-PROMPT';
else if (isJunk) status = 'SKIP-JUNK (' + junkPart + ')';
else status = 'PASS';
let isBug = (isJunk || isPrompt) && text.indexOf('\u276f') !== -1 && text.trim().length > 25;
console.log((isBug ? 'BUG ' : ' ') + status.padEnd(40) + ' | ' + desc);
}

View File

@@ -0,0 +1,42 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Find the regex used in v30 candidate matching
let idx = s.indexOf('candT.match(');
if (idx < 0) idx = s.indexOf('sibT.match(');
let reStr = s.substring(idx, s.indexOf(');', idx) + 1);
console.log('Match code:', reStr.substring(0, 60));
// Extract just the regex part
let reMatch = reStr.match(/\/(.*?)\//);
let reSource = reMatch ? reMatch[0] : 'NOT FOUND';
console.log('Regex source:', reSource);
// Build and test the actual regex
let re = new RegExp(reMatch[1]);
console.log('Regex object:', re);
// Test with the EXACT patterns from logs
let tests = [
['Normal', '\u276f gravity_control > Start-Sleep 12 content_copy'],
['Git cmd', '\u276f gravity_control > git add -A; git commit -m "test"'],
['Short', '> content_copy'],
['Prompt only', '\u276f gravity_control > '],
['Dir cmd', '\u276f gravity_control > dir content_copy'],
];
console.log('\n=== REGEX TESTS ===');
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
for (let [name, text] of tests) {
let m = re.exec(text);
if (m) {
let raw = m[1].trim();
let cleaned = raw.replace(stripRe, '').trim();
console.log(name + ':');
console.log(' raw match[1]: "' + raw + '"');
console.log(' after strip: "' + cleaned + '"');
console.log(' length ok: ' + (cleaned.length >= 3));
} else {
console.log(name + ': NO MATCH');
}
}

View File

@@ -0,0 +1,122 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// 1. SYNTAX CHECK
try { new Function(s); console.log('[1] SYNTAX: OK'); } catch(e) { console.log('[1] SYNTAX ERROR:', e.message); process.exit(1); }
// 2. v30 block exists
let v30Start = s.indexOf('// v30:');
let v30End = s.indexOf('// v23:', v30Start);
console.log('[2] v30 block:', v30Start > 0 && v30End > v30Start ? 'OK' : 'MISSING');
// 3. Key features present
console.log('[3] rcCands:', s.indexOf('rcCands') > 0 ? 'OK' : 'MISSING');
console.log('[4] content_copy filter:', s.indexOf('content_copy|content_paste') > 0 ? 'OK' : 'MISSING');
console.log('[5] sort by length:', s.indexOf('.sort(') > 0 ? 'OK' : 'MISSING');
console.log('[6] icon strip replace:', (s.match(/content_copy/g)||[]).length >= 2 ? 'OK (filter+strip)' : 'CHECK');
// 4. Simulate exact DOM from BTN-DOM-DUMP + CONTEXT logs
let promptRe = /[\u003e\u00bb\u276f]\s+(.+)/;
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
let iconFilterRe = /^(content_copy|content_paste|play_arrow|check_circle|chevron_|keyboard_arrow|more_horiz|more_vert|expand_|alternate_email|arrow_drop)/;
let btnFilterRe = /^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i;
function simulate(name, siblings) {
console.log('\n=== ' + name + ' ===');
let rcFound = false;
for (let sib of siblings) {
if (sib === 'Running command' || (sib.indexOf('Running command') !== -1 && sib.length < 30)) {
rcFound = true;
break;
}
}
if (!rcFound) { console.log(' RC header NOT FOUND'); return null; }
let cands = [];
for (let sib of siblings) {
if (sib === 'Running command') continue;
if (sib.length < 5) continue;
if (iconFilterRe.test(sib)) { console.log(' SKIP icon: "' + sib.substring(0,30) + '"'); continue; }
if (btnFilterRe.test(sib)) { console.log(' SKIP btn: "' + sib.substring(0,30) + '"'); continue; }
if (sib.indexOf('Always run') !== -1 && sib.indexOf('Cancel') !== -1) { console.log(' SKIP btn-bar: "' + sib.substring(0,30) + '"'); continue; }
cands.push(sib);
}
cands.sort((a,b) => b.length - a.length);
console.log(' Candidates: ' + cands.length);
for (let i = 0; i < cands.length; i++) {
console.log(' [' + i + '] len=' + cands[i].length + ': "' + cands[i].substring(0,80) + '"');
}
for (let cand of cands) {
let m = promptRe.exec(cand);
if (m && m[1].trim().length > 3) {
let cmdV = m[1].trim().replace(stripRe, '').trim();
if (cmdV.length < 3) continue;
console.log(' RESULT: "' + cmdV + '"');
return cmdV;
}
if (cand.length > 10 && /[\u276f\u003e]/.test(cand)) {
let raw = cand.replace(stripRe, '').trim();
console.log(' RESULT (raw): "' + raw + '"');
return raw;
}
}
console.log(' RESULT: NO MATCH');
return null;
}
// Case 1: From BTN-DOM-DUMP (3 siblings, command + content_copy icon)
simulate('Case1: Normal command with icon', [
'Running command',
'\u276f gravity_control > Start-Sleep 12; $logFile content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Case 2: content_copy as standalone sibling
simulate('Case2: Icon as separate div', [
'Running command',
'content_copy',
'\u276f gravity_control > npm run compile',
'Always run Cancel'
]);
// Case 3: No icon appended
simulate('Case3: Clean command', [
'Running command',
'\u276f gravity_control > git add -A; git commit -m "test"',
'Always run Cancel'
]);
// Case 4: Very long command
simulate('Case4: Long command', [
'Running command',
'\u276f gravity_control > Select-String -Path "$env:USERPROFILE\\.gemini\\antigravity\\bridge\\extension.log" -Pattern "CONTEXT" content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Case 5: Prompt only (no command yet)
simulate('Case5: Prompt only', [
'Running command',
'\u276f gravity_control > ',
'Always run Cancel'
]);
// Case 6: Multiple icon texts
simulate('Case6: Multiple icons', [
'Running command',
'play_arrow',
'content_copy',
'\u276f gravity_control > dir content_copy',
'Always run Cancel'
]);
// Case 7: Observed log pattern - "content_copy" was cmdV
// This means the regex matched on just "content_copy" with a > before it
// Possible: the sibling text is "> content_copy" (very short prompt)
simulate('Case7: Short prompt with icon only', [
'Running command',
'> content_copy',
'Always run Cancel'
]);
console.log('\n=== ALL TESTS COMPLETE ===');

View File

@@ -0,0 +1,52 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Extract the terminal prompt regex from generated code
let idx = s.indexOf('_termPromptMatch');
let reCtx = s.substring(idx, s.indexOf(');', idx) + 1);
console.log('v32 code:', reCtx.substring(0, 80));
// Extract regex
let reMatch = reCtx.match(/\/(.+?)\//);
let termRe = new RegExp(reMatch[1]);
console.log('v32 regex:', termRe);
let stripRe = /\s*(content_copy|content_paste|play_arrow)\s*$/;
let tests = [
// Should MATCH (terminal commands)
['\u276f gravity_control > Start-Sleep 12', true, 'Start-Sleep'],
['\u276f gravity_control > npm.cmd run compile', true, 'npm compile'],
['\u276f gravity_control > $logFile = Join-Path $env:USERPROFILE', true, 'PS variable (had JUNK match)'],
['\u276f gravity_control > git add -A; git commit -m "test"', true, 'git commit'],
['\u276f gravity_control > node -e "const {gen}=require(\'./out\')"', true, 'node with const (was JUNK)'],
['\u276f extension > npm.cmd run compile', true, 'extension npm'],
['\u276f gravity_control > Start-Sleep 12 content_copy', true, 'with icon (strip)'],
['\u276f gravity_control > Get-Content f.txt | Select-Object -Last 5', true, 'Get-Content'],
// Should NOT match (prompt only, no command)
['\u276f gravity_control > ', false, 'prompt only'],
['\u276f extension > ', false, 'prompt only ext'],
// Should NOT match (not terminal - JS code)
['function test() { return 1; }', false, 'JS function'],
['const x = require("fs")', false, 'JS const'],
['import { foo } from "bar"', false, 'JS import'],
// Should NOT match (no prompt marker)
['gravity_control > dir', false, 'no marker'],
];
console.log('\n=== v32 TERMINAL PROMPT REGEX TESTS ===');
let pass = 0, fail = 0;
for (let [text, shouldMatch, desc] of tests) {
let m = termRe.exec(text);
let matched = false;
let cmdV = null;
if (m && m[1] && m[1].trim().length > 2) {
cmdV = m[1].trim().replace(stripRe, '').trim();
matched = cmdV.length > 2;
}
let ok = matched === shouldMatch;
if (ok) pass++; else fail++;
console.log((ok ? 'PASS' : 'FAIL') + ' | ' + (matched ? 'MATCH' : 'SKIP ').padEnd(5) + ' | ' + desc);
if (matched && cmdV) console.log(' cmd: "' + cmdV + '"');
}
console.log('\nResult: ' + pass + '/' + (pass+fail) + ' passed' + (fail > 0 ? ' (' + fail + ' FAILED!)' : ' ALL OK'));

View File

@@ -0,0 +1,566 @@
/**
* Approval Handler — response processing + approval execution pipeline.
*
* Extracted from step-probe.ts to reduce file size.
* Handles:
* - Response file watching (file-based bridge fallback)
* - Response processing (diff_review, DOM observer, step_probe paths)
* - Multi-strategy approval execution (VS Code commands, RPC, DOM click)
* - Diff review Accept/Reject via VS Code commands
*
* STRATEGY ORDER (most reliable first):
* 0. antigravity.acceptAgentStep / rejectAgentStep — AG's own commands, always works
* 1. HandleCascadeUserInteraction RPC — cross-platform, needs stepIndex
* 2. DOM click trigger via HTTP bridge — fallback
*/
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import type { BridgeContext } from './step-probe';
// ─── Module-level state (injected via initApprovalHandler) ───
let ctx: BridgeContext;
let responseWatcher: fs.FSWatcher | null = null;
let getTrajectoryId: () => string = () => '';
// ─── Public API ───
/**
* Initialize the approval handler with shared context.
* Called from initStepProbe() in step-probe.ts.
*/
export function initApprovalHandler(
context: BridgeContext,
trajectoryIdGetter: () => string,
) {
ctx = context;
getTrajectoryId = trajectoryIdGetter;
}
/**
* Handle diff_review Accept all / Reject all response.
* Called from both WS onResponse (extension.ts) and processResponseFile.
*
* This was previously only in processResponseFile (file-bridge path).
* When WS was added (v0.4.x), the onResponse handler skipped this logic entirely,
* causing Accept All to stop working — a regression.
*/
export async function handleDiffReviewResponse(data: {
request_id: string;
approved: boolean;
button_index?: number;
step_type?: string;
}): Promise<boolean> {
const btnIdx = data.button_index ?? -1;
const isAccept = btnIdx === 0 || (btnIdx === -1 && data.approved);
const cmd = isAccept
? 'antigravity.prioritized.agentAcceptAllInFile'
: 'antigravity.prioritized.agentRejectAllInFile';
ctx.logToFile(`[DIFF-REVIEW-WS] → ${isAccept ? 'ACCEPT' : 'REJECT'} (btnIdx=${btnIdx}, rid=${data.request_id?.substring(0, 12)})`);
let diffReviewDone = false;
let modifiedFiles: string[] = [];
// Load tracked step indices and modified files from memory cache or pending file
const trackedSteps: number[] = [];
const memMeta = ctx.diffReviewMetadata.get(data.request_id);
if (memMeta) {
trackedSteps.push(...memMeta.edit_step_indices);
modifiedFiles = memMeta.modified_files;
ctx.diffReviewMetadata.delete(data.request_id);
ctx.logToFile(`[DIFF-REVIEW-WS] loaded from memory: steps=[${trackedSteps.join(',')}] files=${modifiedFiles.length}`);
} else {
try {
const pf = path.join(ctx.bridgePath, 'pending', `${data.request_id}.json`);
if (fs.existsSync(pf)) {
const pd = JSON.parse(fs.readFileSync(pf, 'utf-8'));
if (pd.edit_step_indices) trackedSteps.push(...pd.edit_step_indices);
if (pd.modified_files) modifiedFiles = pd.modified_files;
}
} catch { }
}
// Strategy 1: VS Code command — open review panel + focus each file + accept/reject
try {
try {
await vscode.commands.executeCommand('antigravity.openReviewChanges');
ctx.logToFile(`[DIFF-REVIEW-WS] openReviewChanges OK`);
await new Promise(r => setTimeout(r, 500));
} catch { }
if (modifiedFiles.length > 0) {
for (const fp of modifiedFiles) {
try {
const uri = vscode.Uri.file(fp);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await new Promise(r => setTimeout(r, 300));
await vscode.commands.executeCommand(cmd);
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${cmd} on ${fp.split(/[\\/]/).pop()} OK`);
diffReviewDone = true;
} catch (e: any) {
ctx.logToFile(`[DIFF-REVIEW-WS] per-file error on ${fp}: ${e.message?.substring(0, 80)}`);
}
}
} else {
await vscode.commands.executeCommand(cmd);
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${cmd} executed (no file list)`);
diffReviewDone = true;
}
} catch (cmdErr: any) {
ctx.logToFile(`[DIFF-REVIEW-WS] Strategy 1 command error: ${cmdErr.message?.substring(0, 200)}`);
}
// Strategy 2: individual hunk accept/reject
if (!diffReviewDone) {
try {
const hunkCmd = isAccept
? 'antigravity.prioritized.agentAcceptFocusedHunk'
: 'antigravity.prioritized.agentRejectFocusedHunk';
await vscode.commands.executeCommand(hunkCmd);
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${hunkCmd} fallback OK`);
diffReviewDone = true;
} catch (hunkErr: any) {
ctx.logToFile(`[DIFF-REVIEW-WS] hunk fallback error: ${hunkErr.message?.substring(0, 100)}`);
}
}
if (!diffReviewDone) {
ctx.logToFile(`[DIFF-REVIEW-WS] ❌ ALL strategies failed for rid=${data.request_id}`);
}
return diffReviewDone;
}
// ─── Response Watcher ───
export function setupResponseWatcher() {
const responseDir = path.join(ctx.bridgePath, 'response');
if (!fs.existsSync(responseDir)) {
fs.mkdirSync(responseDir, { recursive: true });
}
const processAnyResponse = (filename: string) => {
const fp = path.join(responseDir, filename);
if (fs.existsSync(fp)) {
// Check if this response belongs to our project
const rid = filename.replace('.json', '');
const pendingFile = path.join(ctx.bridgePath, 'pending', `${rid}.json`);
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pending.project_name && pending.project_name !== ctx.projectName) {
return; // Not our project
}
} catch { }
} else {
// Pending file missing (deleted or auto_resolved) — check response data itself
try {
const respData = JSON.parse(fs.readFileSync(fp, 'utf-8'));
if (respData.project_name && respData.project_name !== ctx.projectName) {
return;
}
} catch { }
}
setTimeout(() => processResponseFile(fp), 300);
}
};
const pollAllResponses = () => {
try {
if (!fs.existsSync(responseDir)) return;
for (const f of fs.readdirSync(responseDir)) {
if (f.endsWith('.json')) {
processAnyResponse(f);
}
}
} catch { }
};
pollAllResponses(); // Process any existing responses on startup
try {
responseWatcher = fs.watch(responseDir, (event, filename) => {
if (filename && filename.endsWith('.json') && event === 'rename') {
processAnyResponse(filename);
}
});
console.log('Gravity Bridge: response watcher started');
} catch (e: any) {
console.log(`Gravity Bridge: response watcher failed: ${e.message}`);
}
// Polling fallback: fs.watch on Windows can silently fail
setInterval(pollAllResponses, 3000);
}
// ─── Response File Processing ───
async function processResponseFile(filePath: string) {
try {
// Gracefully handle files already consumed by HTTP handler
if (!fs.existsSync(filePath)) {
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
const resp = JSON.parse(content);
// v22: Skip files written by the WS response handler (extension.ts onResponse).
// Those files exist ONLY for Observer's pollResponseGroup to read via HTTP.
// The WS handler already calls tryApprovalStrategies, so processing here is redundant.
// Without this skip, the watcher deletes the file before Observer can poll it
// (since no pending file exists for the isDomObserver check).
if (resp._from_ws) {
// v26: TTL — delete stale _from_ws files after 60s to prevent infinite SKIP spam
const wsRidTs = parseInt((resp.request_id || '').split('_')[0], 10);
const wsAge = isNaN(wsRidTs) ? 999999 : Date.now() - wsRidTs;
if (wsAge > 60_000) {
ctx.logToFile(`[RESPONSE] CLEANUP stale _from_ws file: ${resp.request_id} age=${Math.round(wsAge / 1000)}s`);
try { fs.unlinkSync(filePath); } catch { }
return;
}
ctx.logToFile(`[RESPONSE] SKIP _from_ws file (for Observer pollResponseGroup): ${resp.request_id}`);
return;
}
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved} step_type=${resp.step_type || '(missing)'} keys=[${Object.keys(resp).join(',')}]`;
console.log(`Gravity Bridge: ${msg}`);
ctx.logToFile(msg);
// Skip stale timeout responses: if pending is old and this is a reject, it's likely a bot timeout
const ridTimestamp = parseInt((resp.request_id || '').split('_')[0], 10);
if (!isNaN(ridTimestamp)) {
const ageMs = Date.now() - ridTimestamp;
const STALE_THRESHOLD_MS = 120_000; // 2 minutes
if (ageMs > STALE_THRESHOLD_MS && !resp.approved) {
ctx.logToFile(`[RESPONSE] SKIPPED stale timeout: rid=${resp.request_id} age=${Math.round(ageMs / 1000)}s (>${STALE_THRESHOLD_MS / 1000}s, reject)`);
try { fs.unlinkSync(filePath); } catch { }
return;
}
}
// Find matching pending request
const pendingDir = path.join(ctx.bridgePath, 'pending');
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
let sessionId = '';
let isDomObserver = false;
let pendingStepType = resp.step_type || ''; // from bot's response (new)
let pendingStepIndex = -1;
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
// FIX #2: Skip if pending was already resolved locally (auto_resolve or expired)
if (pending.status === 'auto_resolved' || pending.status === 'expired') {
ctx.logToFile(`[RESPONSE] SKIP — pending already ${pending.status} (rid=${resp.request_id})`);
try { fs.unlinkSync(filePath); } catch { }
return;
}
sessionId = pending.conversation_id || '';
isDomObserver = pending.auto_detected === true
|| pending.source === 'dom_observer';
pendingStepType = pending.step_type || '';
pendingStepIndex = pending.step_index ?? ctx.lastPendingStepIndex;
// File permission detection: check command content or explicit step_type
const cmd = (pending.command || '').toLowerCase();
if (pendingStepType === 'file_permission' || cmd.includes('allow') || cmd.includes('파일 접근')) {
// Map button_index → scope: 0=Once, 1=Conversation, 2=Deny
const btnIdx = resp.button_index ?? -1;
if (btnIdx === 1) {
pendingStepType = 'file_permission_conversation';
} else if (btnIdx === 2) {
pendingStepType = 'file_permission_deny';
} else {
pendingStepType = 'file_permission_once';
}
ctx.logToFile(`[RESPONSE] file_permission detected from pending cmd, mapped to ${pendingStepType}`);
}
} catch { }
}
// ═══ MULTI-STRATEGY APPROVAL (v3.0) ═══
const approved = resp.approved;
// ── diff_review: Accept all / Reject all ──
if (pendingStepType === 'diff_review') {
// Delegate to shared handler (also used by WS onResponse path in extension.ts)
await handleDiffReviewResponse({
request_id: resp.request_id,
approved,
button_index: resp.button_index,
step_type: pendingStepType,
});
} else {
// ALL paths (dom_observer + step_probe) use same strategy pipeline
const targetSession = sessionId || ctx.activeSessionId;
ctx.logToFile(`[RESPONSE] → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
ctx.logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
}
ctx.logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
// FIX v2 (2026-03-16): Correct state management after response processing.
// Set ctx.sawRunningAfterPending=true to close the auto_resolve gate.
ctx.sawRunningAfterPending = true;
// Cleanup response file
// CRITICAL: DOM observer responses must NOT be deleted here!
if (!isDomObserver) {
try { fs.unlinkSync(filePath); } catch { }
}
} catch (e: any) {
const log = `[RESPONSE] error: ${e.message}`;
console.log(`Gravity Bridge: ${log}`);
ctx.logToFile(log);
}
}
// ─── Approval Strategies ───
/**
* Try multiple approval methods sequentially.
* Returns a string describing which method succeeded (or all failed).
*
* Strategy order (most reliable first):
* 0. antigravity.acceptAgentStep / rejectAgentStep (AG VS Code commands — always works)
* 1. HandleCascadeUserInteraction RPC (cross-platform, needs stepIndex)
* 2. Renderer DOM Click via HTTP Bridge (fallback)
*/
export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
const action = approved ? 'APPROVE' : 'REJECT';
const effectiveStepIndex = stepIndex >= 0 ? stepIndex
: (ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : -1);
ctx.logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`);
// ══════════════════════════════════════════════════════════
// STRATEGY 0: SDK-verified AG commands (step_type-aware dispatch)
//
// From SDK index.js (verified command mapping):
// antigravity.agent.acceptAgentStep — code edits, file writes
// antigravity.agent.rejectAgentStep — reject code edits
// antigravity.command.accept — non-terminal commands (Run, Allow, etc.)
// antigravity.command.reject — reject non-terminal commands
// antigravity.terminalCommand.accept — terminal commands
// antigravity.terminalCommand.reject — reject terminal commands
// antigravity.terminalCommand.run — run terminal commands
//
// These operate on the currently focused/active step — no stepIndex needed!
// ══════════════════════════════════════════════════════════
{
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
// Determine which SDK command pair to use based on step_type
let acceptCmd: string;
let rejectCmd: string;
if (typeLower.includes('code_edit') || typeLower.includes('write_to_file')
|| typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')
|| typeLower === 'diff_review') {
// Code edits → agent step commands
acceptCmd = 'antigravity.agent.acceptAgentStep';
rejectCmd = 'antigravity.agent.rejectAgentStep';
} else if (typeLower.includes('run_command') || typeLower.includes('shell_exec')
|| typeLower.includes('send_command_input')) {
// Terminal commands → terminal command pair
acceptCmd = 'antigravity.terminalCommand.accept';
rejectCmd = 'antigravity.terminalCommand.reject';
} else if (typeLower === 'command' || typeLower.includes('permission')
|| typeLower.includes('browser') || typeLower.includes('mcp')
|| typeLower.includes('extension_code') || typeLower.includes('subagent')
|| typeLower.includes('open_browser') || typeLower.includes('read_url')
|| typeLower.includes('invoke_subagent')) {
// Non-terminal commands (Run, Allow, etc.) → command pair
acceptCmd = 'antigravity.command.accept';
rejectCmd = 'antigravity.command.reject';
} else {
// Unknown type — try all three in order
acceptCmd = 'antigravity.command.accept';
rejectCmd = 'antigravity.command.reject';
}
const primaryCmd = approved ? acceptCmd : rejectCmd;
ctx.logToFile(`[APPROVAL-0] stepType="${stepType}" → ${primaryCmd}`);
try {
await vscode.commands.executeCommand(primaryCmd);
ctx.logToFile(`[APPROVAL-0] ✅ ${primaryCmd} SUCCESS`);
return `SDK:${primaryCmd}`;
} catch (e: any) {
ctx.logToFile(`[APPROVAL-0] ❌ ${primaryCmd} failed: ${e.message?.substring(0, 200)}`);
}
// Fallback: if the primary type-specific command failed, try the other pairs
const fallbackPairs = [
approved ? 'antigravity.command.accept' : 'antigravity.command.reject',
approved ? 'antigravity.agent.acceptAgentStep' : 'antigravity.agent.rejectAgentStep',
approved ? 'antigravity.terminalCommand.accept' : 'antigravity.terminalCommand.reject',
].filter(cmd => cmd !== primaryCmd); // skip already-tried
for (const fallbackCmd of fallbackPairs) {
try {
ctx.logToFile(`[APPROVAL-0-FB] Trying ${fallbackCmd}...`);
await vscode.commands.executeCommand(fallbackCmd);
ctx.logToFile(`[APPROVAL-0-FB] ✅ ${fallbackCmd} SUCCESS`);
return `SDK-FB:${fallbackCmd}`;
} catch (e: any) {
ctx.logToFile(`[APPROVAL-0-FB] ❌ ${fallbackCmd}: ${e.message?.substring(0, 100)}`);
}
}
}
// ══════════════════════════════════════════════════════════
// STRATEGY 1: HandleCascadeUserInteraction RPC
// Now supports BOTH approve AND reject.
// Requires valid stepIndex for most step types.
// ══════════════════════════════════════════════════════════
if (ctx.sdk && effectiveStepIndex >= 0) {
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
let interactionPayload: Record<string, any> = {};
// Code edit steps — use dedicated RPC
if (typeLower.includes('code_edit') || typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
try {
ctx.logToFile(`[APPROVAL-1-CODE] trying submitCodeAcknowledgement command`);
await vscode.commands.executeCommand('antigravity.prioritized.submitCodeAcknowledgement');
ctx.logToFile(`[APPROVAL-1-CODE] ✅ submitCodeAcknowledgement OK`);
return `CMD:submitCodeAcknowledgement(accept=${approved})`;
} catch {
ctx.logToFile(`[APPROVAL-1-CODE] submitCodeAcknowledgement not available, trying RPC`);
}
try {
ctx.logToFile(`[APPROVAL-1-CODE] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', {
cascadeId: sessionId,
accept: approved,
stepIndices: [effectiveStepIndex],
});
ctx.logToFile(`[APPROVAL-1-CODE] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
return `RPC:acknowledgeCodeActionStep(accept=${approved})`;
} catch (e: any) {
ctx.logToFile(`[APPROVAL-1-CODE] ❌ ${e.message.substring(0, 200)}`);
// Fall through to generic HandleCascadeUserInteraction
interactionPayload = { runCommand: { confirm: approved } };
}
}
// Map step_type to interaction sub-message field
// CRITICAL FIX: Use `confirm: approved` (not always true) to support REJECT
if (typeLower.includes('run_command') || typeLower.includes('shell_exec')) {
interactionPayload = { runCommand: { confirm: approved } };
} else if (typeLower.includes('open_browser')) {
interactionPayload = { openBrowserUrl: { confirm: approved } };
} else if (typeLower.includes('send_command_input')) {
interactionPayload = { sendCommandInput: { confirm: approved } };
} else if (typeLower.includes('read_url')) {
interactionPayload = { readUrlContent: { confirm: approved } };
} else if (typeLower.includes('mcp')) {
interactionPayload = { mcpTool: { confirm: approved } };
} else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code') || typeLower.includes('browser_subagent')) {
interactionPayload = { runExtensionCode: { confirm: approved } };
} else if (typeLower.includes('file_permission')) {
if (typeLower.includes('deny')) {
interactionPayload = { filePermission: { allow: false, scope: 1 } };
} else {
const scope = typeLower.includes('conversation') ? 2 : 1;
interactionPayload = { filePermission: { allow: approved, scope } };
}
} else if (typeLower.includes('elicitation')) {
interactionPayload = { elicitation: {} };
} else if (typeLower === 'permission' || typeLower.includes('permission')) {
interactionPayload = { runExtensionCode: { confirm: approved } };
} else if (typeLower === 'command' || typeLower === '') {
// Generic command — most common case from DOM observer
interactionPayload = { runCommand: { confirm: approved } };
} else {
// Default: try run_command
interactionPayload = { runCommand: { confirm: approved } };
}
const activeTrajectoryId = getTrajectoryId();
const protoVariants = [
// Variant A: camelCase with trajectoryId
{
cascadeId: sessionId,
interaction: {
trajectoryId: activeTrajectoryId || sessionId,
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
// Variant B: snake_case
{
cascade_id: sessionId,
interaction: {
trajectory_id: activeTrajectoryId || sessionId,
step_index: effectiveStepIndex,
...interactionPayload,
},
},
// Variant C: minimal (no trajectoryId)
{
cascadeId: sessionId,
interaction: {
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
];
let lastRpcError = '';
for (let i = 0; i < protoVariants.length; i++) {
try {
const payload = protoVariants[i];
ctx.logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
const rpcResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
ctx.logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-${i}:HandleCascadeUserInteraction(${typeLower},${action})`;
} catch (e: any) {
lastRpcError = e.message || '';
ctx.logToFile(`[APPROVAL-1-${i}] ❌ ${lastRpcError.substring(0, 300)}`);
}
}
// ── Auto-recovery: wrong-LS detection ──────────────────────
if (ctx.fixLSConnection && lastRpcError.includes('input not registered')) {
ctx.logToFile('[APPROVAL] ⚠️ wrong-LS detected ("input not registered"), attempting LS fix...');
try {
const lsChanged = await ctx.fixLSConnection();
if (lsChanged) {
ctx.logToFile('[APPROVAL] LS reconnected — retrying first proto variant...');
try {
const retryPayload = protoVariants[0];
const retryResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', retryPayload);
ctx.logToFile(`[APPROVAL-RETRY] ✅ SUCCESS: ${JSON.stringify(retryResult).substring(0, 200)}`);
return `RPC-RETRY:HandleCascadeUserInteraction(${typeLower},${action})`;
} catch (retryErr: any) {
ctx.logToFile(`[APPROVAL-RETRY] ❌ ${retryErr.message?.substring(0, 200)}`);
}
} else {
ctx.logToFile('[APPROVAL] LS not changed — already on correct port or fix unavailable');
}
} catch (fixErr: any) {
ctx.logToFile(`[APPROVAL] fixLSConnection error: ${fixErr.message?.substring(0, 200)}`);
}
}
} else if (ctx.sdk && effectiveStepIndex < 0) {
ctx.logToFile(`[APPROVAL-1] SKIPPED RPC: stepIndex=${effectiveStepIndex} (unknown) — Strategy 0 (VS Code command) was the primary attempt`);
}
// ══════════════════════════════════════════════════════════
// STRATEGY 2: Renderer DOM Click via HTTP Bridge (fallback)
// Sets a click trigger that the observer script polls and executes.
// ══════════════════════════════════════════════════════════
try {
const triggerAction = approved ? 'approve' : 'reject';
ctx.logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
ctx.setClickTrigger(triggerAction as 'approve' | 'reject');
ctx.logToFile(`[APPROVAL-2] ✅ clickTrigger set — renderer will poll and click within 2s`);
} catch (e: any) {
ctx.logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
}
ctx.logToFile(`[APPROVAL] All strategies complete for ${action}`);
return `STRATEGIES_DONE:${action}`;
}

View File

@@ -0,0 +1,96 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { WSBridgeClient } from './ws-client';
export interface BrainWatcherContext {
logToFile: (msg: string) => void;
wsBridge: WSBridgeClient;
projectName: string;
}
export class BrainWatcher {
private brainDir: string;
private ctx: BrainWatcherContext;
private currentSessionId: string = '';
private watcher: fs.FSWatcher | null = null;
private lastEventTimes: Map<string, number> = new Map();
constructor(ctx: BrainWatcherContext) {
this.ctx = ctx;
// The bridgePath is ~/.gemini/antigravity/bridge, so brain is sibling
this.brainDir = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
}
public updateSession(sessionId: string) {
if (!sessionId || this.currentSessionId === sessionId) {
return;
}
this.currentSessionId = sessionId;
this.startWatching(sessionId);
}
private startWatching(sessionId: string) {
this.stop();
const sessionDir = path.join(this.brainDir, sessionId);
if (!fs.existsSync(sessionDir)) {
// It might not be created yet, poll gently
setTimeout(() => this.startWatching(sessionId), 2000);
return;
}
try {
this.watcher = fs.watch(sessionDir, { persistent: false }, (eventType, filename) => {
if (!filename || !filename.endsWith('.md')) return;
// Dedup rapid events
const now = Date.now();
const last = this.lastEventTimes.get(filename) || 0;
if (now - last < 500) return; // 500ms debounce
this.lastEventTimes.set(filename, now);
this.handleFileChange(sessionDir, filename, eventType);
});
this.ctx.logToFile(`[BRAIN-WATCHER] Started watching session: ${sessionId.substring(0, 8)}`);
} catch (e: any) {
this.ctx.logToFile(`[BRAIN-WATCHER] Failed to watch ${sessionId}: ${e.message}`);
}
}
private handleFileChange(dir: string, filename: string, rawEventType: string) {
const filePath = path.join(dir, filename);
let content = '';
let eventType = 'file_changed';
try {
if (fs.existsSync(filePath)) {
content = fs.readFileSync(filePath, 'utf-8');
} else {
eventType = 'file_deleted';
}
} catch (e) {
// File might be locked or deleted during read
return;
}
if (this.ctx.wsBridge && this.ctx.wsBridge.isConnected()) {
this.ctx.wsBridge.sendBrainEvent({
event_type: eventType,
conversation_id: this.currentSessionId,
file_name: filename,
content: content,
timestamp: Date.now() / 1000,
project_name: this.ctx.projectName,
});
this.ctx.logToFile(`[BRAIN-WATCHER] Sent ${eventType} for ${filename}`);
}
}
public stop() {
if (this.watcher) {
this.watcher.close();
this.watcher = null;
}
}
}

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