Skip to main content
Glama
README.md
# Vigil

> **English / 英文版:** [README.en.md](README.en.md)

**AI 决策账本与熔断内核,带完整的五岔路统筹循环。** 让每一个 agent 的决定都可拦截、可追溯、可校准、可签名验证,让每一次派工都走路由、质量门、止损、验收、升级。

> 你的 agent 会做事,但没人记得它为什么这么做、错了谁负责、下次该改哪条规则。
> Vigil 就是补上这一层的那块东西。

```
┌─ 统筹助理 Orchestrator ──────────────────────────────────┐
│                                                          │
│   ┌→ 止损 ──够差就停──→ STOPPED                          │
│   │   路由 ──选不出人──→ 升级人工 → ESCALATED            │
│   │   干活(每一轮执行都落账)                            │
│   │   质量门 ──不过──→ 带反馈回路由                       │
│   └───验收(四项清单缺一不可)──通过──→ COMPLETED        │
└────────────────── 每个岔路都是一次内核裁决 ───────────────┘
                              ↓
                  只追加账本(哈希链 + Ed25519 签名)
                    ↓
        事后梳理 → 校准 → 按题型自动调阈值 → 预警
        导出离线验证包(第三方零依赖验证)/ OTel span

agent 想动手  →  ┌──────────────┐  →  放行 / 转人工 / 否决
                  │    Vigil     │
                  │  硬规则否决   │  →  写入只追加账本(哈希链)
                  │  灰区概率判断 │  →  事后回填标签 → 校准阈值
                  └──────────────┘
```

## 为什么需要它

现在这一层是断的:

| 你在用什么 | 它回答的问题 | 它不回答的 |
|---|---|---|
| Langfuse / Phoenix / AgentOps | 发生了什么(trace、span、成本) | 这个决定当时有依据吗 |
| OPA / Guardrails / 各种 firewall | 能不能做(规则、黑名单) | 做错的那些,规则该怎么改 |
| LLM-as-judge | 这次输出好不好 | 它说 0.9 把握的时候,实际准不准 |

中间那块空着:**决定的责任归属**。Jev 这类系统一模型的流行恰好暴露了这个缺口 —— 它给了你 150 毫秒返回的概率,却没给你账本、没给你校准、没给你仲裁。概率一旦没有校准闭环,所有阈值都是拍脑袋。

## 它不是什么

- 不是 agent 编排框架。用 Claude Code、Grok Bot、Cursor 都行,Vigil 只在动手前插一脚。
- 不是模型。它调用判断模型,不训练模型。
- 不是日志系统。日志记录发生的事,账本记录做的决定 —— 两者字段都不一样。
- 0.1 版本,骨架通了,规则库还很薄。不要直接接生产支付。

## 核心设计

### 1. 决定是一等公民,不是一条日志

一条 `DecisionRecord` 至少包含:谁做的、依据什么证据、要求几级授权、实际持有几级、确定性命中了什么、概率判断给了多少、结论是什么、事后被证明对错。缺任何一项,事后就没法复盘。

### 2. 硬约束绝不用概率

这是和大多数项目最大的分歧。**不要让模型去判断逻辑。**

- 金额上限、不可逆操作、对外发送、密钥外泄、禁止目标 —— 全部走确定性规则。精确、可测、可证明,一票否决。
- 「这活该派给谁」「这份研究够不够」「这个承诺有没有风险」—— 才交给概率模型。
- 概率模型**永远没有否决权,也没有放行权**。它只能把一个本来会放行的动作变成"需要人看一眼"。

合起来就是:**逻辑守底线,概率做灰区。**

### 2.5 五岔路统筹循环(一比一对应 Jev 的判断点)

| 岔路 | 问题 | 确定性部分 | 概率部分 |
|---|---|---|---|
| 路由 | 下一个该谁干 | 选出的执行者必须在名册里,否则直接升级 | 在名册内做选择题 |
| 质量门 | 这份产出够不够 | 检查者结构上永远不是产出人 | 独立复核打分 |
| 止损 | 继续还是停 | 重试上限、连续失败次数,数数就够 | 进展评分(低于 2 停) |
| 验收 | 算不算完成 | 四项清单:交付物及位置、独立核验记录、遗留问题声明,**缺一项模型说了也不算** | 实质满足度判断 |
| 升级 | 要不要人接手 | 涉及金额直接 critical | 风险分级(routine/urgent/critical) |

「监督过程」不是独立模块:循环里每个岔路、每轮执行都是一次被哈希链记录的裁决,`postmortem(sessionId)` 能完整回放每一步为什么这么走。

### 2.6 冲突仲裁:多 agent 结论相反时由谁定夺

| 情况 | 谁说了算 | 依据 |
|---|---|---|
| 立场一致 | 直接采纳 | 无争议 |
| 同一个 agent 的多个立场 | 取最严格的那个 | 它自己改主意不算数,取最保守的 |
| 动作本身触红线 | **否决** | 全票同意也不行 |
| 有否决立场,它有独立核验证据而对方没有 | 采信否决 | 证据优势 |
| 有否决立场,它授权等级严格更高 | 采信否决 | 权级 |
| 有否决立场,但分不出高下 | **交人工** | 证据和权级都定不了 |
| 只剩"可以继续"与"需要人看"打架 | 模型排序 | 唯一动用模型的地方 |

**一条没被驳倒的否决立场,永远不会被自动放行。** 模型没有权力把 `deny` 变成 `allow` —— 它只能帮你在"都同意放行"和"需要人看"之间排序。这是 fail-closed 在多 agent 场景的延伸。

仲裁记录和普通决策记录在账本里无法区分 —— 这正是要的:仲裁也是一次决定,同样要能被追溯和复盘。

### 3. 概率判断只能是选择题

Vigil 只向判断模型发送类型化问题(boolean / choice / score),永远不索取自由文本。它返回概率分布,不返回句子。这在结构上就堵死了"模型自由发挥越权"这条路。

### 4. fail-closed

判断服务超时、返回格式不对、返回缺字段 —— 一律判为无效并升级人工,绝不当作默认放行。`failOpen` 开关存在,但默认关闭。

### 5. 五级授权(相对原方案改了一处)

| 级别 | 含义 | 典型动作 |
|---|---|---|
| L0 | read_only | 查询、检索 |
| L1 | draft | 起草、提议 |
| L2 | reversible_write | 可撤销的写入 |
| L3 | external_or_irreversible | 对外发送、不可逆修改 |
| L4 | money | 任何涉及钱的 |

原方案把"金钱"和"不可逆"放在同一级。我们把它们拆开了:内网删库和对外发信都收不回来,破坏半径相近,可以合并;但钱出去了没有任何技术手段能挽回,必须独占一级。

## 决策循环(DecisionLoop):从"会分析的聊天机器人"到"决策系统"

> 早期版本的本质缺陷:拉取式 CLI —— 你问一句它答一句,不主动推送、不替你执行、要花钱的动作没有确认关卡、证据没查全就敢下结论。那不是决策系统,那是"带账本的聊天机器人"。

`vigil loop` 把"决策"做成**可运行、可追踪、可中断、可闭环**的状态机,而不是一段一次性的分析文字。它直接回应三条硬需求:

1. **实时知道** —— 循环的每一步都发射事件(目标 / 证据 / 裁决 / 闸门 / 执行),边跑边打印,并落盘成事件流,对话关了也能 `vigil loop --watch <id>` 回看。
2. **按逻辑自动执行 + 重要节点让你确认后闭环** —— 对外且不可逆(花钱)的动作,必须经过人工确认闸门(`--resume <id> --confirm`)才真正执行;可逆/非对外的动作自动放行。
3. **证据必须真实完整** —— 决策前必须把该领域要求的**证据维度**全跑到(权威 / 市场 / 监管 / 财务 / 风险 / 专家 / 替代 / 口碑);**缺一类即禁止裁决**,从根上堵住"只看线上价就误判超预算"的翻车。

### 四个支柱

| 支柱 | 实现 | 解决什么 |
|---|---|---|
| 实时事件 | `LoopEventBus`,每阶段 `emit` 可见事件并落盘 `events-<id>.jsonl` | 不再沉默地分析,每一步你都看得见 |
| 证据完整性闸门 | `checkCompleteness`,按决策领域枚举必备证据维度,缺一类即 `blocked`(且必备维度必须已核验) | 证据不全/未核验就裁决 = 瞎决策,直接封死 |
| 确认闸门(HITL) | `isGatedAction = external && !reversible`,命中则挂起 `awaiting_confirmation` | 花钱动作 100% 人确认才执行(对标 Operator 金融交易护栏) |
| 可暂停 / 可续跑 | 状态落盘 `loop-<id>.json`,`resume(confirmed)` 恢复 | 等确认、等降价时能挂起,不丢进度 |
| 推送通道(拉→推) | `LoopEventBus` 在 emit 同时把事件推到企微 / 邮件 / 通用 webhook(`Notifier`) | 不再只能 `vigil loop --watch` 回看:每次决策主动推到你手机 |
| 真实执行适配器 | `ActionExecutor`:默认 `DryRunExecutor`,配置 `VIGIL_EXECUTOR_WEBHOOK` 后真实触发你自己的下单 / 开户端点 | 确认闸门放行后才落地,deny / 未确认绝不执行 |
| 证据驱动裁决(Reasoning) | `buildBrief()`:把已核验的实时行情转成透明、可复核的配置方案(`DecisionBrief`),喂回仲裁与执行 | 根治"证据只打印不进决策"——裁决真正由实时数据驱动 |

### 决策领域注册表(覆盖人一生,不止购物)

证据闸门之所以能通用,是因为**证据维度是跨领域通用词汇**,每个生活领域从中挑出"本决策必备"的子集:

| 维度 | 含义 |
|---|---|
| authoritative | 官方/权威来源:政策、法规、官方定价、官方指南 |
| market | 市场/商业:价格、供应、竞品、行情、比价 |
| peer | 用户/社群真实经验:口碑、踩坑、长期持有反馈 |
| expert | 专业人士/专家:医生、律师、行业人士、顾问 |
| regulatory | 合规/政策/资质:牌照、认证、补贴、入学/入境、法律风险 |
| financial | 成本/财务影响:总拥有成本、隐性费用、现金流、利率、报销 |
| risk | 风险/安全:健康、安全、退出成本、爆雷、诈骗、副作用 |
| alternative | 替代方案对比:更好 / 更便宜 / 更稳 / 更合规的选择 |

已注册领域(`vigil domains` 可查看):

| 领域 | key | 必备维度 | 排除 |
|---|---|---|---|
| 购物消费 | purchase | authoritative / market / regulatory / financial / alternative / peer | — |
| 医疗健康 | medical | authoritative / expert / regulatory / risk / financial / alternative | market(商业推销不算证据) |
| 教育升学 | education | authoritative / expert / regulatory / financial / alternative / risk | — |
| 旅行出行 | travel | authoritative / market / regulatory / risk / financial / alternative | — |
| 融资理财 | financing | authoritative / regulatory / financial / risk / expert / alternative | market(推销方一面之词不算证据) |
| 投资理财 | invest | authoritative / regulatory / financial / risk / expert / alternative | market(卖方推销 / 喊单不算证据) |
| 求职职业 | work | authoritative / expert / financial / risk / alternative / regulatory | — |
| 居住置业 | housing | authoritative / market / regulatory / financial / risk / alternative | — |

**想加新领域(保险 / 婚恋 / 移民 / 养老……),往 `DECISION_DOMAINS` 加一项即可,证据闸门自动生效。**

### 状态机

```
goal_received → plan → evidence_gathered → [evidence_complete | evidence_gap→blocked]
             → arbitrating → decision → (deny→completed)
             → (isGatedAction ? gate→awaiting_confirmation → resume→execute : execute)
             → completed
```

### 实时证据采集(Evidence Provider):让证据自己跑

证据完整闸门要求"真实完整"。但证据不该永远是**人手填进 goal.json** —— 系统应当能自己抓取能机读的数据,抓不到的维度则**诚实标记未核验、交闸门拦截**,绝不编造"已核验"。

`src/loop/evidenceProvider.ts` 定义 `EvidenceSource` 接口与 `gatherEvidence` 编排器。**同一规范维度可挂多路实时源**(如 `invest/financial` 同时跑汇率、黄金、白银、国债、股指、加密),只要有一路成功核验,该维度即视为已覆盖。已内置的公开 API 真源(均无需 key):

| 源 | 维度 | 数据 | 沙箱可达 |
|---|---|---|---|
| `FrankfurterFxSource` / `ErApiFxSource` | financial | USD/CNY 实时汇率(ECB) | ✓ |
| `GoldSource` / `SilverSource` | financial | 现货黄金 XAU / 白银 XAG(gold-api.com) | ✓ |
| `TreasuryYieldSource` | financial | 美国 10 年期国债收益率(AlphaVantage,可用 `VIGIL_AV_KEY` 提额) | ✓ |
| `CryptoSource` | financial | BTC / ETH 行情(CoinGecko) | 公网可达,沙箱出网拦截 |
| `StockIndexSource` | financial | S&P 500 等股指(Stooq CSV) | 公网可达,沙箱出网拦截 |
| `SearchSource` | 非机读维度 | 可注入 `searchFn`(WebSearch 后端) | 取决于是否接后端 |

- `vigil evidence fetch <领域> "<问题>" [--extended]`:实时采集该领域必备维度证据,输出带 source URL + 抓取时间戳的 JSON;`--extended` 纳入加密 / 股指等公网源。
- `vigil loop <goal.json> --gather [--extended]`:证据为空或显式 `--gather` 时,先用实时源补抓(已核验的整维度沿用既有、不重复抓)再跑循环。

```bash
# 实时抓取 invest 领域证据(汇率/黄金/白银/国债 真抓,加密/股指沙箱不可达则诚实降级)
node src/cli.ts evidence fetch invest "配置20万美金全球分散" --extended
# → 已实时核验:financial(USD/CNY=6.6675 / 黄金 $4045 / 白银 $53.9 / 10Y 国债 4.99%)
# → 未核验:authoritative / regulatory / risk / expert / alternative(需人工/搜索补充)

# 用空证据骨架跑 --gather:live 抓到 financial,但其余 5 维未核验 → 闸门直接 blocked(不假决策)
node src/cli.ts loop examples/goal-invest-200k-skeleton.json --gather

# 用全维度真实核验的目标文件跑完整闭环(financial 实时抓 + 其余维度已核验)
node src/cli.ts loop examples/goal-invest-200k-demo.json --gather --extended
```

**设计底线**:能机读的实时真抓、附来源与时间戳;抓不到的维度绝不假核验,由完整性闸门拦截,强制人工/搜索补充。

### 推送通道(Notifier):把"实时知道"从拉变推

`LoopEventBus` 在每次 emit(目标 / 证据 / 裁决 / 闸门 / 执行……)的同时,会把事件**主动推送**到你配置的通道,不再只能 `vigil loop --watch` 事后回看:

- **企业微信**:`VIGIL_NOTIFY_WECOM` 配群机器人 webhook,把事件文本推到群里。
- **邮件**:`VIGIL_NOTIFY_EMAIL_API` + `VIGIL_NOTIFY_EMAIL_FROM/TO`(Resend / Mailgun / SendGrid 形态)发事件邮件。
- **通用 webhook**:`VIGIL_NOTIFY_WEBHOOK` 把整个事件 JSON POST 出去(飞书 / Slack / Discord / Zapier / Make 等任意能收 JSON 的服务)。

未配置任何通道时,总线只落盘、不推送(**不假装已通知**);推送失败只记录、绝不因通道抖动卡死决策循环。

### 真实执行适配器(Executor):确认闸门放行后才落地

闭环的"执行"由 `ActionExecutor` 负责,默认是 `DryRunExecutor`(只打印执行清单、不发生任何真实副作用)。配置 `VIGIL_EXECUTOR_WEBHOOK` 后,已确认的动作会被 **POST 到你自己的端点**(券商 / 开户系统 / 支付 / Zapier),由该端点完成真实下单或开户 —— **Vigil 只负责"确认后触发",不持有任何资金凭证**。

> 关键护栏:执行器只在**确认闸门放行之后**被调用(`deny` 永远到不了这里,未确认也到不了)。所以"真实下单 / 开户"的安全性来自闸门,而不是执行器本身。

```bash
# 配置真实执行端点 + 推送通道后,还原真实的"确认即下单"
VIGIL_EXECUTOR_WEBHOOK=https://your-broker.example.com/vigil-execute \
VIGIL_NOTIFY_WEBHOOK=https://hooks.example.com/vigil \
  node src/cli.ts loop examples/goal-invest-200k-demo.json --gather
# → 证据齐 → 仲裁 → ⏸ 确认闸门挂起(每个事件已推送到你的 webhook)

vigil loop --resume lp_xxxx --confirm
# → 确认后:解除闸门 → 触发 WebhookExecutor POST 你的端点(payload 含 decisionBrief 配置方案)→ 拿到外部订单号 → completed
```

### 证据驱动裁决(Reasoning):让实时数据真的参与决策

之前的致命短板:循环把实时采集到的行情(黄金 / 国债 / FX)**只打印、不进裁决**,决策实际由 goal 里预写的 `positions` 立场决定 —— 等于没在决策。

`src/loop/reasoning.ts` 的 `buildBrief()` 在完整性闸门放行后、仲裁之前运行,把已核验的金融证据按**透明规则**转成一份可复核的配置方案(`DecisionBrief`):

- `invest` / `financing` 领域:实时信号 → 具体仓位(债券随 10Y 收益率在 15/25/35% 三档浮动、权益取到实时标普给 30% 否则降到 20% 转现金、黄金 15% / 白银 5% 对冲、比特币仅取到实时价才给 5% 卫星仓、现金补足到 100%),每一条都引用实时数值 + 时间戳。
- **非金融领域(医疗 / 教育 / 工作 / 购物 / 旅行 / 购房…)也真决策**:当证据按 `option` 标签分组(≥2 个方案)时,生成**多方案决策矩阵**——每个方案在必备维度上的「证据完整度 × 风险红牌」排序,给出推荐方案与诚实警示。排名规则透明:`score = 证据完整度(0~1) − (明确风险红牌 ? 0.4 : 0)`;风险维度缺失(未评估)给温和惩罚 0.15;风险证据含「风险/红牌/失败/拒/极低…」关键词才计为红牌,避免"已评估风险=有风险"的误判。单方案 / 无方案时退化为证据摘要,不编造。
- **诚实边界**:抓不到的行情,对应仓位下调 / 移除并明确警示、置信度降级(实时信号 5/6→high、3-4→medium、<3→low),绝不填数字假装算过。

`DecisionBrief` 随后被 emit 为 `reasoning` 事件(实时可见)、透传进仲裁账本(`extras.decisionBrief`)、写入循环状态、并在确认后随执行上下文发给券商端点(`payload.decisionBrief`)。

```bash
# 实跑会看到 reasoning 事件里的真实配置(数据来自上面的实时采集)
node src/cli.ts loop examples/goal-invest-200k-demo.json --gather --extended
# [reasoning] 证据驱动分析:实时信号:USD/CNY=6.67 | 美10Y=4.99%
# [reasoning] 建议配置(合计 100%):美国10年期国债 35% / 标普500指数 20% / 现货黄金 15% / 现货白银 5% / 现金(美元) 25%
# [reasoning] 置信度:low   ⚠ 未取到实时标普/加密行情,仓位已下调(沙箱内部分源不可达,公网可正常抓取)
```

### 统一入口 `vigil ask`:用一句话发起决策

不用再手搓 `goal.json`。一句话描述目标,系统自动**识别领域 → 实时采集证据 → 证据驱动裁决 → 确认闸门 → 闭环**:

```bash
# 自然语言 → 自动识别为 invest 领域 → 实时抓 FX/黄金/白银/10Y → 生成配置方案
vigil ask "帮我配置20万美金做全球分散投资,股债黄金都要" --gather --extended
# 自然语言 → 自动识别为 education 领域 → 若证据不全则诚实 blocked(见 goal-edu-daughter.json 演示完整矩阵)

# 识别不出时用 --domain 强制指定;配置 VIGIL_SEARCH_URL 后,政策/专家/风险等维度自动补抓
vigil ask "女儿专升本还是竞赛免试升本科" --domain education --gather
```

领域识别为关键词匹配(投资/融资/医疗/教育/旅行/工作/购房/购物),命中第一个即返回;也可用 `--domain <领域>` 绕过。

### 搜索源接真后端(让"证据自己跑"覆盖政策/专家/风险)

机读维度(汇率/贵金属/国债/行情)已实时真抓;政策、专家、风险、替代等**非机读维度**默认无后端、`canServe=false`、保持未核验交由闸门拦截。配置真实搜索后端后,这些维度也能自动补抓:

```bash
# 设置一个接受 POST { query, dimension }、返回 { answer | snippet } 的搜索代理 / 向量库 / 爬虫
export VIGIL_SEARCH_URL=https://your-search.example.com/query
vigil ask "女儿专升本还是竞赛免试升本科" --gather   # 政策/专家/风险维度自动补抓
```

未配置时该系统**绝不假核验**——相关维度保持未核验,由完整性闸门拦截。

### 实跑:买 MacBook Neo(预算 4000,旧机不卖)

```bash
# 1) 起一个循环,加载全维度证据(见 examples/goal-neo-purchase.json)
node src/cli.ts loop examples/goal-neo-purchase.json
# 打印:6 个证据维度(权威/市场/监管/财务/替代/口碑,全部已核验)
#       → 证据完整放行 → 裁决 require_human → ⏸ 确认闸门挂起(awaiting_confirmation)

# 2) 你看过事件流、确认无误后,放行闭环
node src/cli.ts loop --resume lp_xxxx --confirm
# 打印:解除闸门 → 闭环执行(dry-run:比对最优价、锁定≤4000渠道、生成下单清单)

# 3) 随时回看某循环的每一步
node src/cli.ts loop --watch lp_xxxx
```

关键纠正:首轮只查线上价(5499)一度误判"超预算否决"。补全国补 15%(4599→3909)、教育 3999、学生补贴 + 线下 ~4030 之后,**补贴后实付压线或低于 4000,预算约束满足** —— 这正是"证据完整"闸门存在的意义。

同一套机制已用**非购买领域**验证:`examples/goal-medical.json`(冠心病择期支架手术,6 维度全核验 → 闸门挂起 → 确认闭环)、`examples/goal-financing.json`(20 万经营贷,覆盖融资领域全维度且排除"推销方一面之词" → 闸门挂起 → 确认闭环)、`examples/goal-invest-200k-live.json`(**$200K 全球分散配置**,6 维度真实核验:含实时抓取 USD/CNY 汇率 + 美联储利率 / 中国跨境合规 / 估值风险 / 资管建议 / 标的对比的 WebSearch 来源 → 仲裁 require_human → 确认闸门 → 闭环输出配置清单)。另有 `examples/goal-invest-200k-skeleton.json` 用于演示 `--gather` 在证据不全时诚实 `blocked`。**决策系统不是"买电脑专用",而是覆盖衣食住行、工作、购物、旅行、融资、投资、医疗、教育的通用框架。**

### `goal.json` 形态

```jsonc
{
  "sessionId": "buy-macbook-neo-2026-10-28",
  "question": "是否购买 MacBook Neo(办公,预算 4000)",
  "action": { "kind": "purchase_laptop", "target": "...", "reversible": false, "external": true, "moneyUsd": 563 },
  "positions": [ /* 各立场(allow/deny/require_human)+ 证据 */ ],
  "evidence": [ /* 该领域必备维度;每条含 dimension(规范维度,闸门据此判定)/channel(来源标签)/source/verified;无 dimension 时回退到 channel */ ],
  "evidenceSpec": "purchase"   // 领域 key:purchase / medical / education / travel / financing / invest / work / housing
}
```

测试覆盖:`npm test` 已含 `tests/loop.test.ts`(决策循环 / 领域注册表 / 完整性 / 未核验阻塞 / 事件落盘 / 闸门挂起 / deny 不可被执行 / resume 确认·未确认 / autoConfirm / 可逆动作自动放行)、`tests/evidenceProvider.test.ts`(实时源与采集编排)、`tests/reasoning.test.ts`(证据驱动裁决:债券随 10Y 浮动、缺失行情诚实降级、置信度分级、非金融多方案决策矩阵按完整度×风险红牌排序、风险红牌关键词判定、HttpSearchSource 接后端、无后端诚实不假核验)与 `tests/decisionSystem.test.ts`(真实数据源多路采集、推送通道企微/邮件/webhook、执行适配器 dry-run/webhook、确认闸门放行后才触发、brief 进入循环状态并随执行 payload 传递的集成测试)。

## 快速开始

要求 Node ≥ 22.18(原生支持 TypeScript,无需构建)。

```bash
git clone <this-repo> && cd vigil
node examples/demo.ts        # 单线场景:裁决、复盘、校准、预警
node examples/company.ts     # 一家 AI 公司:统筹助理带四名工人跑三个任务,
                             # 覆盖退回重做、自产自验被识破、虚假完成止损
node examples/resumable.ts   # 可续跑统筹循环:外部 agent 扮演工人,跨调用跑完
node examples/arbiter.ts     # 冲突仲裁:四个案例走完仲裁阶梯的每一级
npm test                     # 129 个测试
```

`demo` 会打印:每一步的裁决与理由、事后梳理、置信度校准、预警。

真实使用时给上判断模型的 key:

```bash
export TYPESAFE_API_KEY=tsf_xxx
```

没有 key 会退回确定性的 `MockJudge` —— 同样的输入永远同样的结论,适合写测试和复现事故。

## 接入方式

### 先扫一遍 —— `vigil scan`

治理配置不该靠人肉读文档写出来。一条命令先看清"你的 agent 到底能干什么":

```bash
vigil scan                # 扫当前目录
vigil scan /path/to/app   # 扫别的项目
vigil scan --write        # 把建议策略写进 vigil.config.json,立即生效
```

它做三件事:

**1. 盘能力。** 读 `.mcp.json`、`~/.claude.json`、`.cursor/mcp.json`、`.claude/settings.json`、
`package.json` 的 scripts、根目录的 shell 脚本,给每一项推断授权等级:

| 等级 | 含义 | 例子 |
|---|---|---|
| L0 | 只读 | `github_search_issues` |
| L1 | 草稿 | 生成提案、写初稿 |
| L2 | 可逆写入 | `filesystem`、本地构建脚本 |
| L3 | 对外或不可逆 | `slack_send_message`、`git push`、`rm -rf` |
| L4 | 资金 | `stripe`、支付类脚本 |

名字看不出来的(`internal-crm`)按 L3 保守处理并标 `?`,等你确认一次,不猜。

**2. 报风险。** 只报真正要动手的事:资金能力在工作目录里、
凭据文件(`.env`)和文件访问能力同时存在、某个 server 的根目录被设成了整个 `~`、
钩子根本没挂、账本里有 N 条升级没人批。

**3. 用历史校准。** 如果账本里已有记录,它会告诉你哪类动作被否过多少次、
高危动作是不是集中在了一个 actor 身上。这些是真实的规则验证样本。

`--write` 生成的 `vigil.config.json` 会被内核在启动时读取并立即生效
(`VIGIL_CONFIG` 可改路径)。配置文件只能**收紧**内置默认策略,删掉它就回到保守默认值。

### 规则包 —— 事故形态可以搬运

护城河不在代码,在规则库。所以规则库必须能被抄走、被搬运、被非工程师维护。

规则包是**声明式 JSON,不是代码** —— 装进流程的东西不会执行任何代码,
即使来源不可信,它最多让一条规则永远命中:

```bash
vigil rules list                              # 有哪些包
vigil rules show healthcare.patient-data      # 看细节
vigil rules check ./my-pack.json              # 校验,不安装
vigil rules install healthcare.patient-data   # 安装,下次启动生效
```

内置四个行业包:医疗患者数据、付款审批、多租户隔离、代码发布。
完整格式见 [`docs/rule-packs.md`](docs/rule-packs.md)。

**可分发,也因此可被攻击。** 包一旦能从别人那儿下载进来,
"别人写的东西"就能进你的决策链路。四道防线:

| 防线 | 做什么 |
|---|---|
| **指纹** | 索引记内容的 sha256,装之前必须对上 |
| **签名** | 有签名的包要验签;没签名的会明确警告"没人给它背书" |
| **信任锚在本地** | 索引自带的公钥**不算数** —— 能改索引就能换掉公钥和签名一起伪造。公钥必须由你显式信任(`vigil trust add`) |
| **只能收紧不能放松** | 包的策略补丁只能压低金额上限、加长禁止清单、**降低**授权。装一个恶意包放宽不了你的任何一项防护 |

```bash
vigil rules publish my-pack.json --sign   # 登记进索引并签名
vigil rules search 医疗                     # 检索
vigil rules install healthcare.patient-data # 从索引装(先过指纹与签名)
vigil trust add signer.pem 某医院           # 信任签名者的公钥
```

| 特性 | 为什么这么定 |
|---|---|
| 不用正则,只用子串和 `*` 通配 | 规则包可能来自外部,正则的 ReDoS 能卡死你的进程 |
| 规则的 `detail` 强制写清"为什么拦" | 复盘时要看得懂,规则过时时要知道改哪条 |
| `deny` 和 `escalate` 分开 | 否决是确定的;升级要占一个人,只有需要人判断时才用 |
| 包的 `policy` 只收紧、不放松 | 授权是 `grants` 的事,规则包最多收紧它 |

### 钩子 —— 让约定变成强制(推荐)

提示词里写"发邮件前请确认",模型抽风时就失效了。所以判断点必须放在**执行边界**上:

Vigil 接住 6 类生命周期点,不只是动手前:

| 事件 | 做什么 |
|---|---|
| `PreToolUse` | 裁决 —— 唯一能真正阻止动作的时机 |
| `PostToolUse` | 记录执行结果;**扫描工具输出里的凭据**(凭据顺着响应流出去,只有事后看得见) |
| `PostToolUseFailure` | 失败也落账 |
| `SessionStart` | 落账,并把本次会话的监督状态注入 agent 上下文 |
| `Stop` / `SessionEnd` / `PreCompact` | 给会话封口:累计决策数与花费 |

**紧急制动** —— 一条命令,让后面所有动作立刻全部转人工,直到你手动解除:

```bash
vigil kill --reason "agent 在批量改生产配置"   # 拉闸
vigil unkill                                    # 解闸
```

不做成"直接拒绝",是因为拒绝了连"停下来看看发生了什么"都做不到。
解除动作本身留痕(`.vigil/killed.released`),事后能查是谁解的。

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit|MultiEdit|WebFetch|mcp__.*",
        "hooks": [{ "type": "command", "command": "node /绝对路径/vigil/src/cli.ts hook claude", "timeout": 10 }]
      }
    ]
  }
}
```

放进 `~/.claude/settings.json` 即生效。三种结论映射:`allow` → 直接执行,`require_human` → 弹给用户确认,`deny` → 硬拦。

钩子自身出故障(分类失败、超时 4 秒、载荷损坏)永远退出码 0 且不阻断 agent —— 那是我们的 bug,不该由用户承担。

**上线顺序别跳**:先 `VIGIL_SHADOW=1` 跑一周,只看账本不拦截,确认误杀率可接受再开拦截。没跑过影子模式就开拦截,第二天钩子就会被关掉。

完整说明见 [`docs/hooks.md`](docs/hooks.md),配置模板见 [`examples/claude-settings.json`](examples/claude-settings.json)。

### 命令包装器 —— 任何客户端都能用

```bash
vigil run -- rm -rf build      # 退出码 3,未执行
vigil run -- npm test          # 退出码 0,正常执行
```

| 退出码 | 含义 |
|---|---|
| 0 | 放行并执行 |
| 2 | 被否决 |
| 3 | 需人工确认,未执行 |

### CLI

```bash
vigil kill [--reason "原因"]      紧急制动:此后所有动作一律转人工
vigil unkill                       解除紧急制动
vigil status                       当前账本与制动状态一览
vigil rules list|show|check|install|search|publish|verify   规则包:查看、校验、安装、检索、登记
vigil trust list|add|remove       信任的签名公钥(规则包验签用)
vigil ledger-migrate <源> <目标.db>   把 NDJSON 账本迁到 SQLite(并发用)
vigil scan [目录] --write      # 扫描 agent 能力并生成治理配置(--write 立即生效)
vigil arbitrate <文件.json|->  多 agent 结论冲突时定夺
vigil loop <goal.json> [--auto-confirm] [--gather]   决策循环:实时事件+证据完整性闸门+确认闸门+闭环执行(--gather 实时补抓证据)
vigil loop --resume <id> [--confirm]      从挂起恢复(确认后闭环执行)
vigil loop --watch <id>          实时回看某循环的事件流
vigil evidence fetch <领域> "<问题>"      实时采集该领域必备维度证据(附 source URL + 抓取时间戳)
vigil domains                  列出所有支持的决策领域(衣食住行/工作/购物/旅行/融资/医疗/教育…)
vigil decide request.json     # 或从 stdin 读:vigil decide -
vigil ledger                  # 列出所有决策
vigil verify                  # 校验哈希链 + Ed25519 签名
vigil keygen                  # 生成签名身份,之后的记录全部签名
vigil bundle                  # 导出离线验证包(第三方零依赖验证,见 verify-standalone.mjs)
vigil thresholds --write      # 从你回填的标签按题型算推荐阈值,写回配置立即生效
vigil approvals               # 列出待人工审批的升级项
vigil approve <id> approved "同意" --by human/boss   # 审批落账,绑定记录哈希
vigil export-otel spans.jsonl # 导出 OTel GenAI span,直接喂 Langfuse/Phoenix
vigil postmortem <sessionId>  # 事后梳理
vigil calibrate proceed       # 置信度校准 + 阈值扫描
vigil alerts                  # 扫描预警
vigil label <decisionId> wrong "客户否认需求"   # 回填结果
```

判断模型三选一,按优先级:

| 环境变量 | 后端 | 适用 |
|---|---|---|
| `VIGIL_LOCAL_JUDGE` | 本地决策服务(strands-decider / laya,/v1/systemone 方言) | 零成本、数据不出门、可用自有数据微调 |
| `TYPESAFE_API_KEY` | TypeSafe Jev 云端 | 不想本地跑模型 |
| 都不设 | MockJudge(确定性) | 测试、复现事故 |

### MCP(推荐)

```bash
vigil mcp
```

给 Claude Code:

```bash
claude mcp add vigil -- node /abs/path/to/vigil/src/cli.ts mcp
```

暴露 10 个工具。每个工具都带 `annotations`,含协议标准的
`readOnlyHint` / `destructiveHint`,以及我们自有的 `vigilAuthority`
(这个工具需要几级授权)与 `vigilEffect`(它会改什么)——
MCP 协议本身只有两个布尔位,表达不了五级授权。

**单点决策**(6 个):`vigil_decide`、`vigil_postmortem`、`vigil_verify`、`vigil_alerts`、`vigil_calibrate`、`vigil_label`。

**统筹循环**(3 个):`vigil_task_start`、`vigil_task_submit`、`vigil_task_status`。

**冲突仲裁**(1 个):`vigil_arbitrate`。

约定:**agent 在动手之前必须调用 `vigil_decide`,拿到 `allow` 才执行。** 这不是靠自觉,是靠 hook —— 在 `PreToolUse` 里拦。

#### 统筹循环:让外部 agent 扮演工人

原本统筹循环只能在同一个进程里跑完,外部 agent 接不上 —— 它没法中途接管工人角色。
现在它是一个**可跨调用续跑的状态机**,干活的可以是另一个进程:

```jsonc
// 1. 开任务:Vigil 决定下一个该谁干
{"name": "vigil_task_start", "arguments": {
  "task": "给客户 ACME 写一封报价邮件",
  "roster": [{"id": "bot/writer", "role": "写手"}, {"id": "bot/checker", "role": "复核"}],
  "taskId": "t1"
}}
// → {"turn": {"kind": "work", "workerId": "bot/writer", "attempt": 1,
//              "instructions": "...", "requirements": [...]}}

// 2. 外部 agent 按指令干活,然后交回
{"name": "vigil_task_submit", "arguments": {
  "taskId": "t1",
  "summary": "写好草稿",
  "claim": "报价邮件已完成",
  "deliverableUri": "drafts/quote-acme.md",
  "leftoverIssues": "",
  "evidence": [{"id": "ev_check", "producedBy": "bot/checker", "verified": true}]
}}
// → 缺项则 {"turn": {"kind": "work", "attempt": 2, "feedback": "验收未过,缺:..."}}
//   齐全则 {"turn": {"kind": "completed"}}
```

`requirements` 会把验收的四项清单直接告诉执行者,不让它猜。
`feedback` 会把上轮被拒的具体原因带到下一轮,不用瞎改。

**一个要知道的局限**:如果同一个 agent 既扮演执行者又扮演复核者,
"独立核验"就退化成形式独立 —— 账本会如实记下谁产出了证据、谁声称完成,
但真正的独立性需要两个进程。这一点我们没有假装解决。

### SDK

#### 只要裁决(单点决策)

```ts
import { Kernel, Ledger, JevJudge, defaultPolicy } from 'vigil-oversight';

const kernel = new Kernel({
  ledger: new Ledger('.vigil/ledger.ndjson'),
  judge: new JevJudge({ apiKey: process.env.TYPESAFE_API_KEY! }),
  policy: defaultPolicy({ moneyCapPerAction: 50 }),
});

const verdict = await kernel.decide({
  sessionId: 'sess_1',
  actor: 'bot/outbound',
  intent: '把报价单发给客户',
  action: { kind: 'send_email', target: 'ceo@acme.com', reversible: false, external: true },
  evidence: [
    { id: 'ev_pricing', producedBy: 'bot/researcher', claim: '对方有 3 档定价', verified: true },
  ],
  questions: [
    { name: 'proceed', type: 'boolean', instructions: '这一步是否应当继续', floor: 0.8 },
    { name: 'risk', type: 'boolean', instructions: '是否构成商业承诺风险', escalateWhenTrue: true, floor: 0.75 },
  ],
});

if (verdict.outcome !== 'allow') throw new Error(`被拦截:${verdict.reason}`);
```

#### 要完整回路(统筹助理带工人跑任务)

```ts
import { createPipelineKernel, Orchestrator, JevJudge, Ledger } from 'vigil-oversight';

const kernel = createPipelineKernel(
  new Ledger('.vigil/ledger.ndjson'),
  new JevJudge({ apiKey: process.env.TYPESAFE_API_KEY! }),
);
const company = new Orchestrator(kernel, { maxAttempts: 3 });

const report = await company.runTask('sess_1', 'task_1', '写一份行业调研', [
  { id: 'bot/researcher', role: '研究员', run: async (ctx) => ({ /* 产出 */ }) },
  { id: 'bot/writer', role: '写手', run: async (ctx) => ({ /* 产出 */ }) },
]);
// report.status: completed | stopped | escalated
// report.steps: 每个岔路的走向,人能直接读懂
```

工人只管干活和交证据;路由、质量门、止损、验收、升级全部由 Orchestrator 驱动,每一步过内核、落账本。`ctx.feedback` 会告诉你上一轮为什么被打回。

## 四项指标

不看这四个数,这套系统就是在自娱自乐。

| 指标 | 定义 | 健康区间 |
|---|---|---|
| **决策可追溯率** | 被放行的动作里,能追到证据链的比例 | > 90% |
| **虚假完成率** | 被放行、事后被证伪的比例 | < 5% |
| **自主完成率** | 无需人工介入的比例 | 越低说明越保守,不是越低越好 |
| **单位任务成本** | 每个成功任务的累计花费 | 自己定基线,重点看突变 |

## 与相邻项目的关系

| 项目 | 定位 | 与 Vigil 的关系 |
|---|---|---|
| `jkudish/jev-mcp` | 12 个判断工具封装 | 可直接作为 Judge 后端 |
| Langfuse / Phoenix | trace 与评估 | Vigil 记录决定,它记录过程,互补 |
| OPA / Rego | 策略引擎 | 我们故意不用 Rego:0.1 阶段 YAML 化的谓词函数更快出活,等规则过百条再换 |
| Guardrails AI | 输出校验 | 管的是模型输出,Vigil 管的是动作 |

## 存储层

两种后端,**哈希链语义完全一致** —— 哈希只覆盖记录内容,与存储方式无关,
所以换后端不会让已有的验证包失效。

| 后端 | 什么时候用 | 特点 |
|---|---|---|
| **NDJSON**(默认) | 单进程、个人使用 | 零依赖、人可读、可以直接 `grep` |
| **SQLite** | 多个进程 / 多个 agent 同时写 | Node 22 内置 `node:sqlite`,同样零依赖 |

选哪个由路径决定:`.db` / `.sqlite` 结尾就用 SQLite。

```bash
export VIGIL_LEDGER=.vigil/ledger.db     # 切到 SQLite
vigil ledger-migrate .vigil/ledger.ndjson .vigil/ledger.db   # 迁移现有账本
```

SQLite 后端用 WAL 模式 + `BEGIN IMMEDIATE`:写锁排队而不是报错,
"读上一条 → 算哈希 → 写入"是一个原子操作,所以多个进程同时写也不会算错 `prevHash`。

实测:三个进程各写 40 条,共 120 条,哈希链完整,序号连续。

## 已知局限

诚实列出来,免得你踩:

1. **SQLite 后端还是实验特性**。Node 22 的 `node:sqlite` 尚未转正,接口可能变。
   我们屏蔽了它的警告提示(`VIGIL_SHOW_SQLITE_WARNING=1` 可以放出来),但这不改变它随时可能变的性质。
2. **NDJSON 后端不支持并发写**。要用多个进程同时写,请切到 SQLite。
3. **规则库很薄**。默认规则只覆盖了最常见的事故形态。真正的护城河是规则库随事故积累变厚,不是代码。
4. **校准需要标签**。没有事后回填,`calibrate` 和 `thresholds` 就是个空壳。这是运营问题,不是技术问题。
5. **判定不了"意图是否真诚"**。agent 说它要发邮件,实际可能干别的。Vigil 看的是动作形状,不是自述。要防这个得在工具边界强制执行,不在这一层。
6. **阈值的"按题型建议"只覆盖放行方向**。风险型问题(escalateWhenTrue)的自动标定还没做,因为放行/拦截语义相反,需要更大样本才可靠。

## 合规对照

采购或审计场景看 [`docs/compliance-mapping.md`](docs/compliance-mapping.md):OWASP Agentic Top 10 逐条对照(4 项完整覆盖、5 项部分覆盖),EU AI Act 相关义务映射。

## 路线图

- [x] 决策内核(硬规则 + 概率灰区 + fail-closed)
- [x] 哈希链账本与校验
- [x] 事后梳理 / 校准 / 预警
- [x] MCP stdio 服务端(零依赖)
- [x] Claude Code PreToolUse 钩子 + 通用命令包装器
- [x] 五岔路统筹循环(路由 / 质量门 / 止损 / 验收 / 升级)
- [x] Ed25519 账本签名 + 离线验证包(verify-standalone.mjs)
- [x] 本地决策模型接入(strands-decider / laya / kev,/v1/systemone 方言)
- [x] 按题型阈值自动建议与生效(校准闭环最后一公里)
- [x] 审批事件绑定记录哈希 + 升级回调(人工可续命)
- [x] OTel GenAI span 导出(进 Langfuse/Phoenix 生态)
- [x] `vigil scan` 上手工具:扫描能力清单与风险,一键生成生效的治理配置
- [x] 统筹循环接入 MCP(可续跑状态机,外部 agent 扮演工人)
- [x] 冲突仲裁 `Arbiter`:证据与权级定夺,未被驳倒的否决立场绝不放行
- [x] 规则包(声明式 JSON,4 个内置行业包,可安装可校验)
- [x] 存储层换 SQLite(node:sqlite 内置,多进程并发写,哈希链语义不变)
- [x] 决策循环 DecisionLoop:实时事件总线 + 证据完整性闸门 + 确认闸门(HITL) + 可暂停续跑
- [x] 决策领域注册表:衣食住行 / 工作 / 购物 / 旅行 / 融资 / 投资 / 医疗 / 教育(新增领域往 `DECISION_DOMAINS` 加一项即生效)
- [x] 证据驱动裁决 Reasoning:金融实时信号 → 具体仓位;非金融证据 → 多方案决策矩阵(完整度 × 风险红牌排序)
- [x] 实时证据采集层:汇率 / 黄金 / 白银 / 10Y 国债 / 加密 / 股指多源真抓(附 source+时间戳),抓不到诚实 blocked
- [x] 推送通道 Notifier:企微 / 邮件 / 通用 webhook(拉→推,每次事件主动推送)
- [x] 真实执行适配器 Executor:确认闸门放行后才触发(dry-run / webhook 两种),deny / 未确认绝不执行
- [x] 统一入口 `vigil ask`:一句话自然语言 → 自动识别领域 → 实时采集 → 证据驱动裁决 → 闭环
- [x] 搜索源接真后端:配置 `VIGIL_SEARCH_URL` 后政策 / 专家 / 风险等维度自动补抓(未配置诚实不假核验)
- [x] GitHub 顶级 AI 项目 Top 50 调研([报告](docs/ai-landscape-2026-10.md))+ MCP 协议合规与工具授权标注
- [x] 竞品对标 ECC 实跑([报告](docs/competitor-benchmark.md)):多事件钩子、输出侧凭据检测、告警级规则、紧急制动
- [x] 规则包索引与分发:指纹 + 签名 + 本地信任锚 + 只能收紧不放松
- [x] `vigil init` 一键接入 + `vigil doctor` 体检(对标 ECC 的 doctor/repair)

## License

MIT