# dsh-github-companion

DeepSeek Harness 上**完整**的 GitHub 集成包：**33 个原生 agent 工具** + **进程内权限门** + **按需加载的用法技能** + **独立安装的成本纪律 companion skill**——一次装齐。**完全不依赖 `gh` CLI**。

[English](README.md) | 中文

> 已在 DeepSeek Harness `0.1.2-rc.1` 与 `0.1.5-rc.1` 上验证（见 `peerDependencies` 声明）；
> 宿主破坏性版本发布后会在此更新兼容结论。

## 包内三个挂载点

| 挂载点 | 作用 |
|---|---|
| `github-companion` | 在 `ctx.tools` 上注册 33 个 `github_*` 工具。读/发现恒开；写/克隆/建仓按开关启用。 |
| `github-companion-gate` | 宿主 `tools/pre-execute` 权限缝的工作示例。三种模式、三种动作、完全权限下 fail-open。 |
| `github-companion-usage` | 把仓库根的 `SKILL.md`（能力地图、结果约定、工作流配方、故障速查）作为一个按需 agent skill 加载。 |

加上用户手动安装到 `~/.dsh/skills/dsh-github-companion/` 的 **discipline companion skill**（含 `references/{cost-discipline,release-flows,incident-playbook,repo-hardening}.md`），负责插件强制不了的事：推送纪律、tag 时机、预算反应、CI 模板。

## 它能做什么？

用大白话跟 agent 说需求就行，工具由它自己挑：

| 你说… | agent 会… | 背后调用的工具 |
|---|---|---|
| "总结一下我 star 的项目这周有什么更新" | 遍历 star 列表，逐仓查最新版本和提交，汇总成周报 | `list_starred` · `latest_release` · `list_commits` |
| "某某仓库最近有什么新东西？" | 拉最新 release 说明、近期提交、热门 issue | `latest_release` · `list_commits` · `list_issues` |
| "把这个本地文件夹上传成一个私有仓库" | 建仓，然后一次性原子提交全部文件 | `create_repository` · `push_files` |
| "改掉 README 里的错别字，发个 v1.2.1" | 在 main 上改文件并打 tag 发版 | `push_files` · `create_release` |
| "我的 fork 哪些落后上游了？" | 逐个对比上游并报告，你点头后才同步 | `list_forks` · `sync_fork` |
| "给上游提个 issue 反馈这个 bug" | 写好标题正文，经你确认后提交 | `create_issue` |
| "看看某仓库的某个源码文件怎么实现的" | 直接读公开代码、搜代码，不用离开对话 | `get_file_contents` · `search_code` |

**安全模型** —— 读工具恒开；一切写操作（issue/分支/文件/发版/建仓）都经过权限门，先弹审批再执行；新建仓库**强制私有**；令牌不会进入子进程或日志（本地克隆工具开启时除外）。
没有令牌也能匿名只读公开数据（60 次/小时）。

## 为什么不包装 `gh` CLI

所有工具都直连 GitHub REST + GraphQL（fetch / undici）。`github_clone_repository` 唯一开 subprocess 调的是 `git`，不是 `gh`。选择直连的具体权衡：

| 维度 | 直连 REST（这个插件） | 包装 `gh` CLI |
|---|---|---|
| 外部依赖 | 零，跨平台一致 | 必须 PATH 有 `gh`，版本要对齐 |
| 多文件原子提交 | blob → tree → commit → ref 一次完成 | `gh` 无等价物——最接近是一次 `git push` 推一个文件 |
| Actions 成本防护 | 推送前 `GET /actions/runs?status=in_progress` 主动拦截 | `gh run list` 能做但多一次 shell round-trip |
| 限流处理 | 自家重试策略 + 读 backoff 头，可配置 | 依赖 `gh` 的 stderr 文本，脆弱 |
| 沙箱/Windows | 纯 JS，确定性强 | `gh.exe` 不一定在 headless / 沙箱里 |
| 可测性 | `fetchImpl` mock | subprocess 黑盒 |
| 排错 | 与 `curl -H "Authorization: Bearer …"` 同代码路径 | 要翻译 gh 输出格式 |
| GraphQL | 原生（`github_graphql` 走 REST 不到的字段） | `gh api graphql` 能做但要 shell 转义 |

如果未来真有部署需要 `gh`（例如 GHES 防火墙只放 SSH 不放 REST），加一个 `useGhCli` 兜底开关即可，默认仍然走 REST。

## 快速上手

```bash
dsh plugin add dsh-github-companion        # 装完重启 DSH
```

1. 打开 DSH 设置 → **GitHub** 区块
2. 粘贴令牌（[凭证](#凭证)一节有一键申请链接）→ 保存
3. 随口问一句 *"我最近 star 了什么？"* ——答得上来就配置完成

## 包内三个 cordis 挂载点

| 入口 | 插件 name | 用途 |
|---|---|---|
| `dsh-github-companion` | `github-companion` | 向 `ctx.tools` 注册 GitHub REST 工具 |
| `dsh-github-companion/gate` | `github-companion-gate` | 针对 `github_*` 工具的 `tools/pre-execute` 权限门示例 |
| `dsh-github-companion/skill` | `github-companion-usage` | 注册 `dsh-github-companion` 按需技能 |

### 工具清单（共 33 个，按开关注册）

**只读/发现** — 恒开：`github_get_me`、`github_get_repository`、`github_get_file_contents`、`github_list_commits`、`github_search_repositories`、`github_search_code`、`github_search_issues`、`github_list_issues`、`github_get_issue`、`github_list_releases`、`github_latest_release`、`github_list_starred`、`github_list_forks`、`github_list_watched`、`github_list_notifications`、`github_get_file_tree`（一次调用递归列出整棵目录树）、`github_list_my_repositories`（唯一包含你自己私有仓的清单工具）、`github_list_languages`、`github_list_contributors`、`github_list_tags`、`github_get_commit_activity`（近一年周度活跃度，含统计冷缓存处理）

**Issue 写操作** — 可开关（`enableIssueWrites`，默认开）：`github_create_issue`、`github_update_issue`、`github_add_issue_comment`

**Git 数据写操作** — 可开关（`enableGitDataTools`，默认**关**）：`github_list_pull_requests`、`github_get_pull_request`、`github_create_branch`、`github_create_or_update_file`、`github_push_files`、`github_create_pull_request`、`github_create_release`、`github_sync_fork`

**仓库自动创建** — 可开关（`enableRepoCreation`，默认**关**）：`github_create_repository` —— 一律创建**私有**仓库（工具不提供公开选项，公开请到网页手动操作）；需令牌的 Administration (rw) 权限。

规范返回值为 JSON 安全对象，带顶层 `ok` 字段。GitHub 领域错误（404/401/403/422…）以 `{ ok: false, status, message }` 返回而非抛出，模型可以编程化处理；只有网络故障才表现为工具错误。

## 凭证

配置只保存环境变量**名**（`credentialRef`，默认 `GITHUB_TOKEN`）。实际值通过 harness 凭证缝（`ctx.credentials`）每次请求前解析——在任意 provider 层设置该变量（本地 provider 读取 `~/.dsh/.env` 的 env 层，或在 shell 里 export），随时轮换，下一次请求即生效，无需重启。未配置 token 时工具以匿名模式访问公开仓库（核心限速 60 次/小时）；`github_get_me` 会如实报告该状态。

token 永不进入子进程环境或日志——唯一的、可开关的例外：`github_clone_repository` 会把 token 经环境变量注入的认证头交给单个本地 git 子进程（不进命令行参数、URL、`.git/config` 或日志），子进程随操作结束消亡，token 在 harness 之外零残留。

### 第一步 · 申请令牌（二选一）

**方式 A · 一键经典令牌（新手推荐）**

1. 登录 GitHub 后打开这个链接，插件所需的全部权限已自动预选：
   **https://github.com/settings/tokens/new?scopes=repo,workflow&description=dsh-github-companion**
2. 按需选择有效期（不选也行）
3. 拉到页面底部，点 **Generate token**
4. 复制生成的值（`ghp_` 开头），粘贴进插件设置卡片并保存

权衡：经典 `repo` 权限是账户级（全部仓库可读写），不能限定个别仓库；在意这点就用方式 B。

**方式 B · 细粒度令牌（可按仓库控制）**

申请入口：**https://github.com/settings/personal-access-tokens/new**（GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens）：

1. 填名称、选有效期
2. Repository access：**All repositories**——以后新建的仓库自动纳入覆盖，无需回来改令牌
3. 按下表勾选权限
4. 生成并复制（`github_pat_` 开头）

### 第二步 · 权限勾选对照（方式 B）

| 权限 | 用途 |
|---|---|
| Metadata **R** | API 必需 |
| Contents **RW** | 读文件、提交、分支、上传项目 |
| Issues **RW** | Issue 的创建/更新/评论 |
| Pull requests **RW** | PR 相关工具 |
| Workflows **RW** | 上传含工作流文件的项目时需要 |
| Administration **RW** | 自动新建仓库开关需要 |

推荐基线：前五项。只有打开建仓开关才追加 Administration RW；之后修改权限不会改变令牌值。

### 第三步 · 把令牌交给插件

- **设置界面**（推荐）：DSH 设置 → GitHub 区块 → 粘贴到 Token 输入框 → 保存。立即生效；界面永不回显已保存的值
- **环境变量**：定义 `GITHUB_TOKEN`（如写在 `~/.dsh/.env`），设置界面留空。轮换后下一次请求即生效

验证：问 agent 任意关于你 GitHub 账号的问题——`github_get_me` 应返回 `authenticated: true`（匿名模式返回 `false` 且只能读公开数据）。

> **存储说明**：经设置界面保存的 PAT 会以**明文**落在本机插件 entry config 中（DSH 0.1.7 上即 profile patch）。`role('secret')` 保护的是网络传输与界面回显，不是磁盘文件。若在意明文落盘，请改用环境变量方式（`credentialRef`），设置界面留空即可。

## 设置界面

两个条目的设置即 entry config（v1.1.0 起 / DSH 0.1.7：由导出的 Config schema 投影进设置界面；0.1.5 上为同名 settings 命名空间，字段一致）：

- `github-tools`：`enableIssueWrites`、`enableGitDataTools`、`enableRepoCreation`、`enableCloneTools`、`workspaceRoot`（**默认克隆目录**——克隆落点优先级：当前会话的工作区 → 此目录 → 询问；目录不存在时自动创建。agent 通过 `github_get_me` 读取该路径与状态）、`proxyUrl`（访问 api.github.com 的可选 HTTP(S) 代理 ——Node 的 fetch 不读系统代理，需要时在此显式填写）、`token`（内联 PAT，`role('secret')`）、**Actions 成本防护**开关 `actionsGuardEnabled` + `actionsGuardTagCooldownMinutes` （见下文 [Actions 成本防护](#actions-成本防护)）
- `github-permission-gate`（0.1.7 entry id；0.1.5 命名空间为 `github-gate`）：`mode`（`off|writes|all`）、`action`（`ask|deny`）、`excludeTools`（设置卡片以胶囊形式增删，附常用只读工具建议）

默认值来自导出的 Config schema。0.1.7 上修改触发 entry 重载后生效；0.1.5 上经 settings watch 实时生效。

设置页的「GitHub」区块由本包自带的浏览器半（`client.js`，经 `dsh.client` manifest 暴露）贡献，读写走官方设置接口（0.1.7 上由 `dsh-settings-scope-compat` 垫片把旧 `settingsScope` 面重建在 `configForms` 之上）。若删除该垫片，0.1.7 仍可用原生设置界面（Config schema 自动投影），仅自定义卡片不可用。

不经设置 UI 的等效做法：直接在 profile 的 bundle patch 行上写 `config:`：

```yaml
- id: github-tools
  name: dsh-github-companion
  config:
    enableGitDataTools: true
    actionsGuardEnabled: true            # 默认 true
    actionsGuardTagCooldownMinutes: 30   # 默认 30；0 = 关闭
- id: github-permission-gate
  name: dsh-github-companion/gate
  config:
    mode: all
```

## 安装

```bash
# 已发布包（bundle 通道，重启后生效）
dsh plugin add dsh-github-companion

# 从本仓库的本地检出安装
dsh plugin add <你的检出目录>/dsh-github-companion   # 或 github:owner/repo#<sha>
```

然后设置 token；不需要权限门时，可从 profile 的 `cordis.patch.yml` 删掉 gate 那一行。

一个 bundle 一步装——v0.9.0 起包内自带按需加载的 agent 技能 `github-companion-usage`（能力开关地图、结果约定、令牌权限速查、工作流配方与故障手册），取代原先独立的 `dsh-github-guide` bundle。从旧版迁移：删掉 profile bundle 清单里的 `dsh-github-guide` 行及其 `node_modules` 链接（技能现在由主包以 `dsh-github-companion/skill` 入口提供）——两个挂载并存会把同名技能注册两次。

### Actions 成本防护

私有仓的 Actions 分钟由 push 触发的 workflow run 在 `in_progress` 状态消耗，**按 job 计费且向上取整**。2026-08-28 的事故展示了烧穿速度：一条配置错误的 $0 预算把 Actions 锁死，僵尸排队烧掉约 1500 分钟而没干任何活。v0.8.1 起插件在其能控制的唯一推送面上加了硬防护：

- **`github_push_files`**：目标仓已有 `in_progress` 的 workflow run 时拒绝，返回 `push_guard_in_progress`（HTTP 409）并列出运行中的 run——往已计费的 job 上叠推送，只会浪费已经花掉的分钟数
- **`github_create_release`**：同样的 in-progress 预检，外加同名 tag 冷却（`release_tag_cooldown`，HTTP 429，默认 30 分钟），防止调试发版流水线时疯狂重打同一个 tag。冷却窗口只在创建成功后计时
- **fail-open 设计**：预检 API 自身报错（403/404/网络断）时放行——防护自身的故障绝不能阻塞工作。Billing 接口有意不查：细粒度 PAT 在那里是 403，任何基于额度查询的熔断都是假防护。真正的断路器是 GitHub 侧的 **停用预算**（作者自跑 $20/月 Actions 预算；推理过程见配套技能）

配置方式（设置界面 `github-tools` 命名空间，或 `~/.dsh/settings.yaml` 用户层）：

```yaml
github-tools:
  actionsGuardEnabled: true            # 默认 true
  actionsGuardTagCooldownMinutes: 30   # 默认 30；0 = 关闭
```

`actionsGuardRefuseOnInProgress`（默认 true）仅组合层可调。**本地 `git push` 完全不经过插件进程**，无法被拦截——这层防护请安装下面的配套技能补齐。

### 配套实践技能（用户级，跨工作区）

仓库里 `skill/dsh-github-companion/` 是一个**用户级技能**——复制到 `~/.dsh/skills/dsh-github-companion` 后所有 DSH 工作区自动加载（不像会话记忆，它不随工作区切换丢失，也不随插件升级被覆盖）：

```
~/.dsh/skills/dsh-github-companion/
├── SKILL.md                        # 路由器：按任务告诉你读哪个分册
└── references/
    ├── cost-discipline.md          # 任何 push/tag/Release 之前必读
    ├── release-flows.md            # 发版链路、tag 重点火、草稿
    ├── incident-playbook.md        # CI 红 / run 卡死 / 预算报错
    └── repo-hardening.md           # 新私有仓 CI 加固模板
```

它补充插件导出的 `github-companion-usage` 技能（工具地图与配方）管不到的部分：**成本纪律**（一轮一推、禁 `push && push --tags`、先绿后 tag）、**发版链路**（`publish-self` → `create-tag` → 自动 Release）、**故障手册**（npm ci 无限 peer 循环、预算锁死、卡死 run 取消）与 **CI 加固模板**。SKILL.md 只是路由器——agent 按任务只读需要的分册，上下文最小化。

## 配置（cordis.yml 插入行）

```yaml
- insert:
    - id: github-tools
      name: dsh-github-companion
      config:
        credentialRef: GITHUB_TOKEN
        apiBaseUrl: https://api.github.com
        requestTimeoutMs: 30000
        maxRetries: 1
        maxPerPage: 30
        maxFileBytes: 262144
        enableIssueWrites: true
        enableGitDataTools: false
        enableRepoCreation: false
        enableCloneTools: false
        actionsGuardEnabled: true
        actionsGuardRefuseOnInProgress: true
        actionsGuardTagCooldownMinutes: 30
        workspaceRoot: ''
        proxyUrl: ''
    - id: github-permission-gate
      name: dsh-github-companion/gate
      config:
        mode: writes
        action: ask
        excludeTools: [github_search_code, …]  # 11 项作者默认
    - id: dsh-github-usage
      name: dsh-github-companion/skill
```

`id:` 是 loader 跟踪的 **bundle 标识**，沿用历史命名（`github-tools` / `github-permission-gate` / `dsh-github-usage`）；v1.1.0 起 / DSH 0.1.7 上它同时是设置的寻址键（entry id 即设置界面键）。只有 `name:`（模块路径）跟随新包名走。

## 开发

```bash
npm install
npm run typecheck
npm test
npm run test:coverage
npm run build
node scripts/verify-load.mjs
```

贡献约定与 PR 流程见 [CONTRIBUTING.md](CONTRIBUTING.md)；安全披露走 [SECURITY.md](SECURITY.md)。

## 测试

四层，由快到全（不涉及任何付费服务——这里排的只是耗时和搭建成本）：

```bash
# L1 — 离线：类型 + 83 个单测（client/tools/gate，mock fetch）
npm run typecheck && npm test

# L2 — 包能被 profile 的 link 布局加载
dsh plugin --profile <scratch> add <你的检出目录>/dsh-github-companion
cd ~/.dsh/profiles/<scratch>
dsh --profile <scratch> --dump-config
node <你的检出目录>/dsh-github-companion/scripts/verify-load.mjs

# L3 — 真连 GitHub API 冒烟（只读；匿名即可）
node scripts/smoke-live.mjs

# L4 — 认证+写操作：用一个一次性仓库

# L5 — agent 级端到端：装进日常 profile 后直接问
```

## 启动安全与恢复（实测结论）

插件出问题时会发生什么，全部在真实 harness 启动上验证过：

| 场景 | 结果 |
|---|---|
| 配置行健康 | 正常启动 |
| schema 非法配置 | 整个 profile 拒绝启动，错误精确指到行 id 和字段 |
| 配置合法但 `apply()` 抛错 | 同样拒绝启动，堆栈点名插件 |
| 工具 `execute()` 运行时抛错 | 被工具注册表包含为 `isError` 结果，harness 继续运行 |

坏安装的爆炸半径是"这个 profile 在修好/删掉该行之前起不来"——绝不会静默变成半坏 agent。恢复手段：

```bash
dsh plugin --profile <name> remove dsh-github-companion
```

测量用的两个故障注入 overlay 在 `scripts/bad-config.patch.yml` 和 `scripts/apply-throw.patch.yml`，可对任何 scratch profile 重放。

## 许可

MIT
