diff --git a/Agent.md b/Agent.md
index d82b0022..3685e5b2 100644
--- a/Agent.md
+++ b/Agent.md
@@ -93,7 +93,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```
-Python: `uv run pytest tests/ -v` (542) — import check: `uv run python -c "from emrg.client.app import run_client"`
+Python: `uv run pytest tests/ -v` (548) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (91: 22 daemon_client + 22 app-commands + 22 renderer smoke + 15 i18n + 7 integration + 3 commands) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)
diff --git a/README.cn.md b/README.cn.md
index 39828c69..ed5fb055 100644
--- a/README.cn.md
+++ b/README.cn.md
@@ -274,7 +274,7 @@ EMRG 不只是追赶——它自己追上来。
git clone https://github.com/argszero/emrg.git
cd emrg
uv sync # 安装依赖
-uv run pytest tests/ -v # 跑测试(当前 542 项)
+uv run pytest tests/ -v # 跑测试(当前 548 项)
uv run python -m emrg # 启动 TUI
# CI 含 actionlint workflow 门禁(#444):workflow 解析错误在 PR 即失败
diff --git a/README.md b/README.md
index 6d8fe2c7..cdd78b9f 100644
--- a/README.md
+++ b/README.md
@@ -273,7 +273,7 @@ EMRG doesn't just keep up — it catches up on its own.
git clone https://github.com/argszero/emrg.git
cd emrg
uv sync # install deps
-uv run pytest tests/ -v # run tests (currently 542 items)
+uv run pytest tests/ -v # run tests (currently 548 items)
uv run python -m emrg # launch TUI
# CI includes actionlint workflow gate (#444): workflow parse errors fail PR CI
diff --git a/emrg/gui/daemon_client.js b/emrg/gui/daemon_client.js
index dd27983a..7528221a 100644
--- a/emrg/gui/daemon_client.js
+++ b/emrg/gui/daemon_client.js
@@ -47,6 +47,7 @@ const RESPONSE_TYPES = {
evolution_summary: "evolution_summary", // WorkBuddy P3:自进化可见化
github_connect: "github_connect_result", // Windows GCM rant Stage 2:PAT 授权(daemon.py github_connect)
github_disconnect: "github_disconnect_result", // Windows GCM rant Stage 2:断开(daemon.py github_disconnect)
+ github_connect_web: "github_connect_web_result", // Stage 2b:device flow(daemon.py github_connect_web)
};
class DaemonClient {
diff --git a/emrg/gui/main.js b/emrg/gui/main.js
index 06404e5a..d6fb7c7e 100644
--- a/emrg/gui/main.js
+++ b/emrg/gui/main.js
@@ -460,6 +460,19 @@ vision = false
return { ok: Boolean(frame.ok), error: frame.error || null };
});
+ ipcMain.handle("emrg:githubConnectWeb", async () => {
+ // Windows GCM rant Stage 2b:device flow 启动(daemon github_connect_web)
+ const frame = await client.sendCommandAndWait("github_connect_web", {}, 15000);
+ return { ok: Boolean(frame.ok), code: frame.code || null, url: frame.url || null, error: frame.error || null };
+ });
+
+ ipcMain.handle("emrg:openExternal", async (_e, { url }) => {
+ // Windows GCM rant Stage 2b:打开 device flow 授权页(默认浏览器)
+ if (typeof url !== "string" || !/^https:\/\//.test(url)) return { ok: false };
+ await shell.openExternal(url);
+ return { ok: true };
+ });
+
ipcMain.handle("emrg:setModel", async (_e, { model }) => {
await client.sendCommandAndWait("set_model", { model }, 5000);
return { ok: true };
diff --git a/emrg/gui/preload.js b/emrg/gui/preload.js
index 945225e8..1ccc6678 100644
--- a/emrg/gui/preload.js
+++ b/emrg/gui/preload.js
@@ -30,6 +30,8 @@ const api = {
githubStatus: () => ipcRenderer.invoke("emrg:githubStatus"),
githubConnect: (payload) => ipcRenderer.invoke("emrg:githubConnect", payload),
githubDisconnect: () => ipcRenderer.invoke("emrg:githubDisconnect"),
+ githubConnectWeb: () => ipcRenderer.invoke("emrg:githubConnectWeb"),
+ openExternal: (payload) => ipcRenderer.invoke("emrg:openExternal", payload),
listModels: () => ipcRenderer.invoke("emrg:listModels"),
openFile: (payload) => ipcRenderer.invoke("emrg:openFile", payload),
saveSettings: (payload) => ipcRenderer.invoke("emrg:saveSettings", payload),
diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html
index 166739ea..a19c6b83 100644
--- a/emrg/gui/renderer/index.html
+++ b/emrg/gui/renderer/index.html
@@ -294,6 +294,20 @@