diff --git a/external_plugins/glm-routing-suite/.zcode-plugin/plugin.json b/external_plugins/glm-routing-suite/.zcode-plugin/plugin.json new file mode 100644 index 00000000..379a85bf --- /dev/null +++ b/external_plugins/glm-routing-suite/.zcode-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "glm-routing-suite", + "version": "0.5.0", + "description": "Four-stage workflow + delivery gate + graded task protocol with a soft stage gate and variant presets, ported from dsh-routing-suite (MIT/Apache-2.0): default zero-injection, completion-signal advancement, delivery_check evidence manifest, two-tier spec gating with red-team gates, PreToolUse write/edit gating, spec/react style presets", + "description_i18n": { + "en": "Four-stage workflow + delivery gate + graded task protocol with a soft stage gate and variant presets, ported from dsh-routing-suite (MIT/Apache-2.0): default zero-injection, completion-signal advancement, delivery_check evidence manifest, two-tier spec gating with red-team gates, PreToolUse write/edit gating, spec/react style presets", + "zh-CN": "移植自 dsh-routing-suite 的四阶段工作流+交付门禁+分级任务协议+软阶段门禁+变体预设:默认零注入、完成信号晋级、delivery_check 证据清单、两级规格门控含红队门、规格阶段 PreToolUse 扣住写入、spec/react 风格档" + }, + "author": { + "name": "forrestneo" + }, + "skills": "./skills", + "hooks": "./hooks/hooks.json" +} \ No newline at end of file diff --git a/external_plugins/glm-routing-suite/NOTICE b/external_plugins/glm-routing-suite/NOTICE new file mode 100644 index 00000000..1f6f3a6a --- /dev/null +++ b/external_plugins/glm-routing-suite/NOTICE @@ -0,0 +1,40 @@ +glm-routing-suite — a ZCode plugin port of dsh-routing-suite +Copyright (c) 2026 forrestneo (port). MIT License for this port's original +code. Upstream component licenses differ and are respected per component: + + - preset/router-standard (router-core.mjs, router-bootstrap-v34.mjs): + yjh051108/dsh-routing-suite — https://github.com/yjh051108/dsh-routing-suite + Copyright (c) 2026 yjh051108. MIT License. + + - graded/ (dsh-graded-mode): Copyright (c) 2026 the dsh-graded-mode authors. + Apache-2.0 License (see upstream graded/LICENSE). Any future port of the + graded two-tier task protocol carries Apache-2.0 attribution and + reproduces only its behavioral semantics, not its source. + +Ported components: + - scripts/router_core.py ← preset/router-standard/router-core.mjs + (task-classification regexes, behavior bands, personas, complexity + heuristic, mode parsing — preserved verbatim where they are the measured + artifacts of the upstream P1-P23 experiments) + - scripts/delivery_check.py ← the deliveryCheck gate in + preset/router-standard/router-bootstrap-v34.mjs (v1.23-v1.28 semantics: + file/utf8 checks + 8-kind evidence manifest, reviewed visuals, + non-blocking numeric hint), re-exposed as a CLI + - skills/mode-router/SKILL.md ← the v1.20 four-stage progressive workflow + (understanding→planning→development→verification, completion-signal + advancement, STAGE_GUIDES behavioral guidance) as a protocol + +Note on design lineage: v1.20 (read in full from router-bootstrap-v34.mjs) +fixes the persona to a single RL sentence and enforces behavior via staged +tool disclosure; the older band-swapped-persona design lives on only as +informational mode output. This port follows the v1.20 mainline. + +Not ported (DSH-runtime specific, no ZCode equivalent): + - injector/ (runtime agent-loop injection, dev_* tooling, restrict) + - phase_begin/phase_advance/tools_catalog secondary disclosure plumbing + - graded/ (separate two-tier task-protocol plugin) + +Measured claims (three behavior bands, weak-persona optima per model, +completion-signal advancement, anchor effects) originate from the upstream +repository's P1-P23 experiments on DeepSeek models and are cited, not +re-verified here. diff --git a/external_plugins/glm-routing-suite/README.md b/external_plugins/glm-routing-suite/README.md new file mode 100644 index 00000000..98219d7c --- /dev/null +++ b/external_plugins/glm-routing-suite/README.md @@ -0,0 +1,64 @@ +# glm-routing-suite: four-stage workflow + delivery gate (ZCode port) + +Ported from the **measured mainline design** of router-standard v1.20 in [yjh051108/dsh-routing-suite](https://github.com/yjh051108/dsh-routing-suite) (MIT, 7k stars). Direction corrected after reading all 1293 lines of the upstream bootstrap: the value of v1.20 is not "swap personas per task" (the old design was dropped; the persona is a fixed single sentence) — it lives in two things: + +1. **A four-stage progressive workflow**: understanding/alignment → planning → development → verification. Advancement is driven by completion signals (clarification / recorded plan / passing self-check / delivery check); **tool usage alone never skips a stage**. Upstream enforces this with hard tool restriction; ZCode plugins cannot, so it works here as protocol discipline plus reminders. +2. **A delivery_check evidence gate**: a mandatory local check before declaring done — file exists / non-empty / UTF-8, plus an evidence manifest (8 kinds; visuals must be `reviewed:true`; page deliverables need at least one reviewed visual; numeric invariants turn "I think it works" into "I verified it"). **This part is a complete port — pure local file validation.** + +## Components + +| Component | Role | +|---|---| +| `skills/mode-router` | The four-stage protocol: per-stage behavioral guidance (decide whether to ask by impact, attention reclamation, local-optima avoidance, hypothesis audit) + delivery gate usage | +| `scripts/delivery_check.py` | Delivery gate CLI: `--file [--url ] --evidence evidence.json`; FAIL exits 1 | +| `scripts/router_core.py` / `route.py` | Regex task classification (build/fix/weak) — auxiliary info for reporting and stage emphasis | +| `hooks/on_prompt.py` | UserPromptSubmit hook: task echo + one-line stage-discipline reminder, fail-open | + +## Quick start + +```bash +# classify a task +python scripts/route.py --text "fix the login error" # → fix (stage-0 diagnosis is the focus) + +# run the gate before declaring done +python scripts/delivery_check.py --file dist/app.py --evidence evidence.json +``` + +Example evidence.json: + +```json +{"items": [ + {"label": "main script", "kind": "file", "target": "dist/app.py"}, + {"label": "smoke run", "kind": "run", "result": "python dist/app.py --selftest → 3/3 passed"}, + {"label": "symbol count", "kind": "numeric", "result": "42"} +]} +``` + +## Measured-claims labeling + +- **Upstream numbers** (three behavior bands, completion-signal advancement, anchor effects, 0%→100% completion): from the P1-P23 experiments of dsh-routing-suite on **DSH + DeepSeek models** (mostly n=2); **not reproduced in this port** — cited as design rationale only +- **This plugin's own tests** (2026-10-14, GLM-5.3): 34 exhaustive state-machine unit tests + 13 gate tests all pass; plus an n=20 ambiguous-task first-action A/B (10 tasks × 2 conditions × 2 reps, test/alignment-ab-results.json) — **no aggregate effect observed** (both conditions ask-first at 50%; the bare GLM-5.3 baseline is already high, a ceiling effect). The protocol's alignment benefit on GLM is **unverified** and is not claimed as a selling point + +## Differences from the original (honest notes) + +- Upstream enforces **hard** per-stage tool restriction via a DSH runtime injector and keeps a persistent stage section in the system prompt; ZCode plugins cannot do either, so this port relies on protocol discipline + hook reminders and model compliance +- `phase_begin` / `phase_advance` / `tools_catalog` (secondary disclosure) / goal tools / stages.json persistence are DSH tool-system specific and not ported +- `delivery_check` semantics are fully ported (v1.23-v1.28: the gate validates evidence instead of running its own browser; reviewed visuals; non-blocking numeric hint), only the interaction changes from tool call to CLI call +- Measured behavioral claims are cited from the upstream experiments on DeepSeek models, not re-verified on ZCode/GLM +- Stage state is not persisted; it lives in conversation context + +## Side effects and dependencies + +- **Network access: none.** Everything is local. +- **Command execution:** `python scripts/route.py`, `python scripts/delivery_check.py` (stdlib only). +- **File writes: none.** Scripts only read files and write stdout (delivery_check expresses PASS/FAIL via exit code 0/1). +- **Hooks:** one UserPromptSubmit hook, fail-open, disable via Settings → Plugins if unwanted. +- **No MCP server, no credentials, no data collection.** + +## Install + +Local test marketplace: Plugin Marketplace → Add → paste `C:\Users\w\Documents\z_fa\plugins` → Personal → dev-z-fa-e3f2b3e7 → glm-routing-suite → Install. + +## License + +MIT. Porting relationship and upstream attribution in [NOTICE](./NOTICE). diff --git a/external_plugins/glm-routing-suite/README_CN.md b/external_plugins/glm-routing-suite/README_CN.md new file mode 100644 index 00000000..62a6895a --- /dev/null +++ b/external_plugins/glm-routing-suite/README_CN.md @@ -0,0 +1,64 @@ +# glm-routing-suite:四阶段工作流 + 交付门禁(ZCode 移植版) + +复刻自 [yjh051108/dsh-routing-suite](https://github.com/yjh051108/dsh-routing-suite)(MIT,7k★)router-standard v1.20 的**实测主线设计**。读完上游 1293 行 bootstrap 源码后修正过方向:v1.20 的价值不在"按任务换 persona"(老设计已弃用,persona 固定为一句 RL 句),而在两件事—— + +1. **四阶段渐进工作流**:了解/对齐 → 拟合方案 → 开发 → 验证。完成信号驱动晋级(澄清/计划/自检/交付检查),**工具使用本身绝不跳级**。上游用工具 restrict 强制执行;ZCode 做不到硬限制,以协议+提醒形式生效。 +2. **delivery_check 交付证据门禁**:宣告完成前必须过的本地校验——文件存在/非空/UTF-8 三查 + 证据清单(8 种 kind,视觉类必须 `reviewed:true`,页面交付物必须有已复核的视觉证据,数值不变量把"我认为对"变成"我验证过")。**这部分是完整移植,纯本地文件校验。** + +## 组件 + +| 组件 | 作用 | +|---|---| +| `skills/mode-router` | 四阶段协议:各阶段行为要点(歧义按影响决定问不问、注意力回收、防局部最优、假设审计)+ 交付门禁用法 | +| `scripts/delivery_check.py` | 交付门禁 CLI:`--file <交付物> [--url <页面>] --evidence evidence.json`,FAIL 退出码 1 | +| `scripts/router_core.py` / `route.py` | 任务正则分类(build/fix/weak)——辅助信息,用于报告任务性质和阶段侧重 | +| `hooks/on_prompt.py` | UserPromptSubmit 钩子:任务回显 + 一行阶段纪律提醒,fail-open | + +## 快速上手 + +```bash +# 分类一个任务 +python scripts/route.py --text "帮我修一下登录报错" # → fix(阶段0排查是重心) + +# 交付前过门禁(模型在宣告完成前必须跑) +python scripts/delivery_check.py --file dist/app.py --evidence evidence.json +``` + +evidence.json 示例: + +```json +{"items": [ + {"label": "主脚本", "kind": "file", "target": "dist/app.py"}, + {"label": "冒烟运行", "kind": "run", "result": "python dist/app.py --selftest → 3/3 passed"}, + {"label": "语法计数", "kind": "numeric", "result": "42"} +]} +``` + +## 实测声明(分级标注) + +- **上游数字**(三行为带、完成信号晋级、锚点效果、0%→100% 完成率等):来自 dsh-routing-suite 在 **DSH + DeepSeek 模型**上的 P1-P23 实验(多为 n=2),**未在本移植复现**,仅作设计依据引用 +- **本插件自测**(2026-10-14,GLM-5.3):状态机 34 项穷举单测 + 门禁 13 项单测全过;另做了 n=20 的歧义任务首动作开/关对照(10 任务 × 2 条件 × 2 重复,test/alignment-ab-results.json)——**未观察到聚合效应**(两条件先问率均为 50%;GLM-5.3 裸基线已较高,存在天花板效应)。协议对 GLM 的对齐收益目前是**未证实状态**,不作为卖点宣称 + +## 与原版的差异(诚实声明) + +- 上游用 DSH 运行时注入器做**硬**工具限制(每阶段只见本阶段工具)和 system prompt 常驻阶段声明;ZCode 插件无此能力,本移植以**协议纪律 + 钩子提醒**形式生效,依赖模型遵循 +- 上游 `phase_begin`/`phase_advance`/`tools_catalog`(二级披露)/goal 工具/stages.json 持久化均为 DSH 工具系统专属,未移植 +- `delivery_check` 语义完整移植(v1.23-v1.28:门禁验证据不自跑浏览器、reviewed 视觉、numeric 非阻断提示),仅交互从"工具调用"变为"CLI 调用" +- 实测数字(完成信号晋级、注意力回收等行为效应)引自上游在 DeepSeek 模型上的实验,未在 ZCode/GLM 复测 +- 阶段状态不持久化,按会话上下文维持 + +## 副作用与依赖声明 + +- **网络访问:无。** 全部本地。 +- **命令执行:** `python scripts/route.py`、`python scripts/delivery_check.py`(仅标准库)。 +- **文件写入:无。** 所有脚本只读文件、只写 stdout(delivery_check 退出码 0/1 表达 PASS/FAIL)。 +- **钩子:** UserPromptSubmit 一个,fail-open,可在 设置 → 插件 禁用。 +- **无 MCP server、无凭据、无数据收集。** + +## 安装 + +本地测试市场:插件市场 → 添加 → 粘贴 `C:\Users\w\Documents\z_fa\plugins` → 个人 → dev-z-fa-e3f2b3e7 → glm-routing-suite → 安装。 + +## 许可 + +MIT。移植关系与上游致谢见 [NOTICE](./NOTICE)。 diff --git a/external_plugins/glm-routing-suite/hooks/gate_hook.py b/external_plugins/glm-routing-suite/hooks/gate_hook.py new file mode 100644 index 00000000..68f6f939 --- /dev/null +++ b/external_plugins/glm-routing-suite/hooks/gate_hook.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""PreToolUse 软门禁(P2):分级协议的规格阶段拦截 write/edit + 状态文件自保护。 + +协议(与 ZCode/Mimosa 实证格式一致): + 允许 → 退出 0,无输出 + 拦截 → 退出 0 + stdout JSON: + {"hookSpecificOutput": {"hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "..."}} + +门禁语义(九评审修订版): + 1. 自保护(分级/提醒激活时):Write/Edit/MultiEdit 指向 ~/.glm-routing-suite/ 状态文件 + 一律拒——状态仅 CLI 进程可写,防提示注入篡改门禁(安全评审 #b) + 2. 阶段门(仅 graded 会话激活且 stage ∈ {brainstorm, l1-edit, l2-edit, review}): + 拦 write/edit,理由附解锁命令 + 3. 降级:连续拦 3 次(10 分钟窗口内)自动降级为警告放行,防误伤卡死(架构评审) + 4. fail-open:任何异常/超时/解析失败 → 静默放行(与 Mimosa 的 fail-closed 相反, + 本门禁是纪律工具不是安全边界,宁可放过不可卡死) + +手动冒烟: + printf '%s' '{"tool_name":"Write","tool_input":{"file_path":"C:/x/y.txt"}}' | python hooks/gate_hook.py +""" +import json +import os +import sys +import time +from datetime import datetime + +_HOOK_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(_HOOK_DIR, os.pardir, "scripts")) + +STATE_DIR = os.path.realpath(os.path.join(os.path.expanduser("~"), ".glm-routing-suite")) +GATE_BLOCK_LIMIT = 3 +GATE_WINDOW_S = 600 + +PRE_DEV_STAGES = ("brainstorm", "l1-edit", "l2-edit", "review") +GATED_TOOLS = {"Edit", "Write", "MultiEdit"} + + +def _deny(reason): + print(json.dumps({"hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": reason, + }}, ensure_ascii=False)) + sys.exit(0) + + +def _safe_path_in_state_dir(path): + try: + p = os.path.realpath(path) + return os.path.commonpath([STATE_DIR, p]) == STATE_DIR + except (OSError, ValueError): + return False + + +def _load_gate_counter(): + try: + with open(os.path.join(STATE_DIR, "gate.json"), encoding="utf-8") as f: + d = json.load(f) + if isinstance(d, dict) and time.time() - float(d.get("at", 0)) < GATE_WINDOW_S: + return int(d.get("blocks", 0)) + except (OSError, ValueError, TypeError): + pass + return 0 + + +def _save_gate_counter(n): + try: + os.makedirs(STATE_DIR, exist_ok=True) + tmp = os.path.join(STATE_DIR, "gate-%d.tmp" % int(time.time() * 1000)) + with open(tmp, "w", encoding="utf-8") as f: + json.dump({"blocks": n, "at": time.time()}, f) + os.replace(tmp, os.path.join(STATE_DIR, "gate.json")) + except OSError: + pass + + +def main(): + try: + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + raw = sys.stdin.read() + data = json.loads(raw) if raw.strip() else {} + tool = str(data.get("tool_name") or data.get("toolName") or "") + if tool not in GATED_TOOLS: + return # matcher 已过滤,双保险 + + file_path = str((data.get("tool_input") or data.get("toolInput") or {}) + .get("file_path") or "") + + import suite_mode as sm + import graded as gr + + mode = sm.load_state().get("mode", "off") + gstate = gr.load_state() + graded_active = gstate.get("stage") not in (None, "off") + + # 1) 自保护:状态文件仅 CLI 可写(mode 激活或 graded 会话存在时) + if (mode != "off" or graded_active) and file_path and _safe_path_in_state_dir(file_path): + _deny("[glm-routing-suite] 状态文件仅 CLI 进程可写(防篡改门禁):请用 " + "suite_mode.py / graded.py 子命令操作状态,不要直接编辑 %s" % file_path) + + # 2) 阶段门:分级会话的规格阶段扣住 write/edit + if graded_active and gstate["stage"] in PRE_DEV_STAGES: + blocks = _load_gate_counter() + if blocks >= GATE_BLOCK_LIMIT: + _save_gate_counter(0) # 降级后重置 + sys.stderr.write("[glm-routing-suite] 门禁已连续拦 %d 次,自动降级为警告放行" + "(防误伤)\n" % blocks) + return + _save_gate_counter(blocks + 1) + _deny("[glm-routing-suite·graded] 阶段=%s:规格未审核通过,write/edit 暂扣。" + "解锁:完成规格后跑 `python graded.py review --decision approve`;" + "或 `python graded.py off` 退出分级模式" % gstate["stage"]) + + # 通过(develop/final 或未激活)→ 重置计数 + _save_gate_counter(0) + except SystemExit: + raise + except Exception: + pass # fail-open + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/external_plugins/glm-routing-suite/hooks/hooks.json b/external_plugins/glm-routing-suite/hooks/hooks.json new file mode 100644 index 00000000..c3f59daf --- /dev/null +++ b/external_plugins/glm-routing-suite/hooks/hooks.json @@ -0,0 +1,36 @@ +{ + "description": "glm-routing-suite hooks: per-prompt close-range guidance (opt-in) + graded stage gate.", + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "process", + "command": "python", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/on_prompt.py" + ], + "timeoutMs": 4000, + "statusMessage": "glm-routing-suite: classifying task…" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "process", + "command": "python", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/gate_hook.py" + ], + "timeoutMs": 5000, + "statusMessage": "glm-routing-suite: 检查阶段门禁…" + } + ] + } + ] + } +} diff --git a/external_plugins/glm-routing-suite/hooks/on_prompt.py b/external_plugins/glm-routing-suite/hooks/on_prompt.py new file mode 100644 index 00000000..ada0ec55 --- /dev/null +++ b/external_plugins/glm-routing-suite/hooks/on_prompt.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""UserPromptSubmit 钩子:按 suite_mode 状态注入(上游"未触发=官方原样"铁律)。 + + off(默认) → 零注入(什么都不打印,纯净官方体验) + discipline → 每条用户消息注入一行:任务回显 + 阶段纪律提醒 + graded → 分级任务协议徽章行(P1':阶段+当前任务+红队灯) + +fail-open:任何异常静默退出 0。状态缺失/损坏按 off 处理。 +手动冒烟: + python ../scripts/suite_mode.py on + printf '%s' '{"prompt":"帮我修一下登录报错"}' | python hooks/on_prompt.py + python ../scripts/suite_mode.py off +""" +import json +import os +import sys + +_HOOK_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(_HOOK_DIR, os.pardir, "scripts")) + +try: + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + import suite_mode as sm + import router_core as rc + + mode = sm.load_state().get("mode", "off") + if mode == "off": + raise SystemExit(0) + + raw = sys.stdin.read() + prompt = "" + try: + data = json.loads(raw) if raw.strip() else {} + p = data.get("prompt") + if isinstance(p, list): + prompt = " ".join( + x if isinstance(x, str) else (x or {}).get("text", "") for x in p) + elif isinstance(p, str): + prompt = p + elif isinstance(raw, str): + prompt = raw # 非 JSON 输入按纯文本处理 + except ValueError: + prompt = raw + + if not prompt.strip(): + raise SystemExit(0) + + echo = prompt.strip()[:80] + ("…" if len(prompt.strip()) > 80 else "") + preset = sm.load_state().get("preset", "standard") + if mode == "graded": + # P1':graded 徽章行(阶段+当前唯一任务+红队灯),细节由 graded 状态补充 + print("[glm-routing-suite·graded] ★任务: %s | 规格未定稿前不进入开发;红队未裁不得打卡" % echo) + elif preset == "spec": + print(("[glm-routing-suite·spec 深思优先] 先读全上下文、穷尽歧义(两种以上实质不同解读就问)、" + "长思维链推演后再动手;改前重读文件 | task: %s") % echo) + elif preset == "react": + print(("[glm-routing-suite·react 动手优先] 直接写/改代码,产出→验证→修复紧环;" + "不搭没要的测试脚手架和仪式;交付前过 delivery_check | task: %s") % echo) + else: + task_class = rc.task_class_of(prompt) + task_hint = {"build": "建造类:阶段2开发是重心", "fix": "修复类:阶段0排查是重心"}.get( + task_class, "先判断 build/fix 再进阶段") + print(("[glm-routing-suite] 阶段纪律:0了解/对齐→1拟合方案→2开发→3验证,完成信号才晋级,工具使用不跳级;" + "宣告完成前必须过 delivery_check(%s)| task: %s") % (task_hint, echo)) +except SystemExit: + raise +except Exception: + pass +sys.exit(0) diff --git a/external_plugins/glm-routing-suite/scripts/delivery_check.py b/external_plugins/glm-routing-suite/scripts/delivery_check.py new file mode 100644 index 00000000..48bd9495 --- /dev/null +++ b/external_plugins/glm-routing-suite/scripts/delivery_check.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""delivery_check:交付门禁(上游 dsh-routing-suite v1.23-v1.28 语义的移植)。 + +在向用户宣告"完成"之前必须跑本检查。校验: + 1. file-exists / file-nonempty / encoding-utf8(交付物文件必查三项) + 2. delivery-evidence(证据清单,8 种 kind) + +evidence 结构(与上游 schema 同构): + {"items": [{"label": ..., "kind": "file|page|image|run|test|text|external|numeric", + "target": "路径(file/page/image/test 必填)", + "result": "文本(run/text 必填;external 与 target 二选一)", + "reviewed": true(page/image 视觉类必须人工复核过)}]} +规则:页面交付物(--url)要求至少一项 reviewed:true 的视觉证据(page/image/external); +数值不变量(kind=numeric)缺省时给非阻断提示。全部 PASS 才允许宣告完成。 + +用法: + python delivery_check.py --file <交付物路径> [--url <页面地址>] --evidence + cat evidence.json | python delivery_check.py --file <路径> + +纯本地校验:只读文件、只写 stdout,无网络。 +""" +import argparse +import json +import os +import sys + +ALLOWED_KINDS = {"file", "page", "image", "run", "test", "text", "external", "numeric"} + + +def check_file(path): + checks = [] + try: + st = os.stat(path) + checks.append({"name": "file-exists", "pass": True, + "detail": "%s (%d bytes)" % (path, st.st_size)}) + checks.append({"name": "file-nonempty", "pass": st.st_size > 0, + "detail": "%d bytes" % st.st_size}) + except OSError as e: + checks.append({"name": "file-exists", "pass": False, "detail": str(e)}) + return checks + try: + with open(path, "rb") as f: + head = f.read(65536) + head.decode("utf-8") # 严格解码,非法字节抛异常 + checks.append({"name": "encoding-utf8", "pass": True, + "detail": "UTF-8 decode OK (head 64KB)"}) + except UnicodeDecodeError as e: + checks.append({"name": "encoding-utf8", "pass": False, "detail": str(e)}) + return checks + + +def is_valid_number(s): + s = s.strip() + if not s: + return False + try: + float(s) + return True + except ValueError: + return False + + +def check_evidence(ev, has_url): + checks = [] + items = (ev or {}).get("items") + if not isinstance(items, list) or not items: + checks.append({"name": "delivery-evidence", "pass": False, + "detail": "missing evidence items — 至少一项:{label, kind, target?, result?, reviewed?}"}) + return checks + failures = [] + for it in items: + label = str(it.get("label") or "").strip() + kind = str(it.get("kind") or "").strip() + if not label: + failures.append("empty label") + continue + if kind not in ALLOWED_KINDS: + failures.append("bad kind: %s" % kind) + continue + result = str(it.get("result") or "").strip() + if kind in ("run", "text"): + if not result: + failures.append("%s evidence without result" % kind) + continue + if kind == "numeric": + if not is_valid_number(result): + failures.append("numeric evidence needs a numeric result (e.g. minr=2.07 中的 2.07)") + continue + if kind == "external": + target = str(it.get("target") or "").strip() + if not target and not result: + failures.append("external evidence needs target (file) or result (output summary)") + if target: + if not os.path.isfile(target) or os.path.getsize(target) <= 0: + failures.append("external target not valid file: %s" % target) + continue + target = str(it.get("target") or "").strip() + if not target: + failures.append("%s evidence without target" % kind) + continue + if not os.path.isfile(target) or os.path.getsize(target) <= 0: + failures.append("target not valid file: %s" % target) + if kind in ("page", "image") and it.get("reviewed") is not True: + failures.append("visual not reviewed: %s" % label) + if has_url: + has_reviewed_visual = any( + str(it.get("kind")) in ("page", "image", "external") and it.get("reviewed") is True + for it in items) + if not has_reviewed_visual: + failures.append("page deliverable needs at least one reviewed visual evidence (page/image/external)") + checks.append({"name": "delivery-evidence", "pass": not failures, + "detail": "evidence accepted (%d item(s))" % len(items) if not failures + else "; ".join(failures)}) + # v1.28:数值不变量提示(引导,不强杀) + if all(str(it.get("kind")) != "numeric" for it in items): + checks.append({"name": "numeric-assertion", "pass": True, + "detail": "hint: 若交付物有可测不变量(守恒量/计数/编译通过),加一项 numeric 证据" + "(kind=numeric, result=<数字>)把「我认为对」变成「我验证过」。非阻断。"}) + return checks + + +def main(): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + ap = argparse.ArgumentParser() + ap.add_argument("--file", required=True, help="交付物文件路径") + ap.add_argument("--url", default=None, help="页面交付物地址(触发视觉证据要求)") + ap.add_argument("--evidence", default=None, help="evidence JSON 文件路径;缺省从 stdin 读") + args = ap.parse_args() + + ev = {} + ev_error = None + if args.evidence: + with open(args.evidence, encoding="utf-8") as f: + raw = f.read() + else: + raw = sys.stdin.read() + if raw.strip(): + try: + ev = json.loads(raw) + except ValueError as e: + ev_error = "evidence JSON 解析失败:%s(Windows 路径请用正斜杠或双反斜杠转义)" % e + else: + ev_error = "stdin 为空且未给 --evidence" + + checks = check_file(args.file) + if ev_error: + checks.append({"name": "delivery-evidence", "pass": False, "detail": ev_error}) + else: + checks += check_evidence(ev, bool(args.url)) + ok = all(c["pass"] for c in checks) + out = {"ok": ok, "checks": checks, + "verdict": ("PASS — 门禁满足,可以向用户报告完成" if ok + else "FAIL — 不得宣告完成;修复未通过项后重跑")} + print(json.dumps(out, ensure_ascii=False, indent=2)) + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() diff --git a/external_plugins/glm-routing-suite/scripts/graded.py b/external_plugins/glm-routing-suite/scripts/graded.py new file mode 100644 index 00000000..ee670df9 --- /dev/null +++ b/external_plugins/glm-routing-suite/scripts/graded.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python3 +"""graded:分级任务协议状态机(上游 dsh-graded-mode Apache-2.0 的语义移植,CLI 形态)。 + +七步链:脑暴定稿(commit_star) → L1 大类规格(edit_plan --level 1) → 锁定(lock_stage 1) +→ L2 小类规格(edit_plan --level 2) → 锁定(lock_stage 2) → 审核(review) → 开发打卡 +(mark_task / redteam_verdict / revise_do) → 终验(final → delivery_check)。 + +阶段机:off → brainstorm → l1-edit → l2-edit → review → develop → final + +门控语义(忠实上游): + - 拒绝=状态不变(校验失败不落盘) + - 规格全必填(L1: title/spec/accept[];L2: title/spec/accept[]/do/verify,concept 可选) + - 已锁大类必须保留,不可新增/改名 + - mark_task 仅 develop/final;verify=redteam 且未裁决通过不得打卡(再审批铁律) + - L1 completed 需组内小类全部 completed;in_progress 单线聚焦(同组其它复位) + - commit_star 的 purpose ≥12 字、不得照抄任务原文;develop/final 阶段星象只读 + - revise_do 只改 do 并登记轨迹;verify 只读 + +状态文件 ~/.glm-routing-suite/graded.json(原子写;损坏=按无会话处理)。 +默认回执=摘要(状态+下一步),--verbose 给完整规格单。 +""" +import argparse +import json +import os +import sys +import tempfile +from datetime import datetime + +STATE_DIR = os.path.join(os.path.expanduser("~"), ".glm-routing-suite") +STAGES = ["off", "brainstorm", "l1-edit", "l2-edit", "review", "develop", "final"] +MODES = ("correct", "experience", "research") +VERIFY_SPECIAL = "redteam" + + +def safe_path(root, *parts): + p = os.path.realpath(os.path.join(root, *parts)) + r = os.path.realpath(root) + if os.path.commonpath([r, p]) != r: + raise ValueError("path escapes allowed dir: %s" % p) + return p + + +def _now(): + return datetime.now().isoformat(timespec="seconds") + + +def init_state(): + return {"version": 1, "stage": "off", "star": None, "plan": {"groups": []}, + "review": None, "trail": [], "updated_at": _now()} + + +def load_state(): + try: + with open(safe_path(STATE_DIR, "graded.json"), encoding="utf-8") as f: + d = json.load(f) + if isinstance(d, dict) and d.get("stage") in STAGES: + return d + except (OSError, ValueError): + pass + return init_state() + + +def save_state(state): + state["updated_at"] = _now() + os.makedirs(STATE_DIR, exist_ok=True) + tmp_fd, tmp_name = tempfile.mkstemp(dir=STATE_DIR, prefix="graded-", suffix=".tmp") + final = safe_path(STATE_DIR, "graded.json") + if final: + with os.fdopen(tmp_fd, "w", encoding="utf-8") as f: + json.dump(state, f, ensure_ascii=False, indent=2) + os.replace(tmp_name, final) + return state + + +# ---------- 校验(拒绝=状态不变) ---------- + +def _strs(a): + if isinstance(a, list): + return [str(x).strip() for x in a if str(x).strip()] + return [] + + +def assert_l1(items): + if not isinstance(items, list) or not items: + return "大类列表为空:至少一个 L1 大类" + titles = [str(it.get("title") or "").strip() for it in items] + if any(not t for t in titles): + return "存在空的大类名" + if len(set(titles)) != len(titles): + return "大类名重复:%s" % next(t for t in titles if titles.count(t) > 1) + for it in items: + if not str(it.get("spec") or "").strip(): + return "大类「%s」缺 spec(必须写清做什么)" % it.get("title") + if not _strs(it.get("accept")): + return "大类「%s」缺 accept(验收标准至少一条)" % it.get("title") + return None + + +def assert_l2(groups, locked_titles): + if not isinstance(groups, list) or not groups: + return "小类树为空:至少一组" + g_titles = [str(g.get("title") or "").strip() for g in groups] + if any(not t for t in g_titles): + return "存在空的大类名" + if len(set(g_titles)) != len(g_titles): + return "大类名重复:%s" % next(t for t in g_titles if g_titles.count(t) > 1) + if locked_titles: + missing = [t for t in locked_titles if t not in g_titles] + if missing: + return "小类编辑必须保留已锁定的大类:缺少 %s" % "、".join(missing) + extra = [t for t in g_titles if t not in locked_titles] + if extra: + return "小类编辑不能新增/改名大类:%s(大类已锁定)" % "、".join(extra) + for g in groups: + items = g.get("items") or [] + if not items: + return "大类「%s」下没有小类" % g.get("title") + i_titles = [str(it.get("title") or "").strip() for it in items] + if any(not t for t in i_titles): + return "大类「%s」下存在空的小类名" % g.get("title") + if len(set(i_titles)) != len(i_titles): + return "大类「%s」下小类名重复:%s" % (g.get("title"), next(t for t in i_titles if i_titles.count(t) > 1)) + for it in items: + t = it.get("title") + for field, desc in (("spec", "规格"), ("do", "执行形态"), ("verify", "验证方式")): + if not str(it.get(field) or "").strip(): + return "小类「%s」缺 %s(全必填门控:写清楚再锁定)" % (t, field) + if not _strs(it.get("accept")): + return "小类「%s」缺 accept(验收标准至少一条)" % t + m = str(it.get("mode") or "").strip() + if m and m not in MODES: + return "小类「%s」mode 非法:%s(correct/experience/research)" % (t, m) + return None + + +# ---------- 命令语义 ---------- + +def cmd_commit_star(state, args): + p = str(getattr(args, "purpose", "") or "").strip() + if state["stage"] in ("develop", "final") and state.get("star"): + return None, "开发/终验阶段星象只读(北极星已入规约)" + if len(p) < 12: + return None, "purpose 过短(<12 字):请给出「为谁/处境/价值/结果」句式的目的宣言,而非口号" + task = str((state.get("star") or {}).get("task") or "").strip() + if task and (p == task or task in p): + return None, "purpose 不得机械照抄任务原文:须重新表述" + star = {"purpose": p, "requirements": _strs(getattr(args, "requirements", None)), + "non_goals": _strs(getattr(args, "non_goals", None)), + "assumptions": _strs(getattr(args, "assumptions", None))} + mode = getattr(args, "mode", None) + if mode: + if mode not in MODES: + return None, "mode 非法:%s(correct/experience/research)" % mode + star["mode"] = mode + if state["stage"] == "off": + state["stage"] = "brainstorm" + state["star"] = star + return state, "★北极星定稿:%s\n下一步:edit_plan --level 1 拟大类规格(title/spec/accept[])" % p + + +def _read_plan(args): + plan = getattr(args, "plan", None) + if plan and plan != "-": + with open(plan, encoding="utf-8") as f: + return json.load(f) + return json.loads(sys.stdin.read()) + + +def cmd_edit_plan(state, args): + data = _read_plan(args) + if getattr(args, "level", 0) == 1: + if state["stage"] not in ("brainstorm", "l1-edit"): + return None, "当前阶段 %s 不可编辑 L1(仅 brainstorm/l1-edit)" % state["stage"] + err = assert_l1(data.get("items")) + if err: + return None, err + if state["stage"] == "brainstorm": + state["stage"] = "l1-edit" + state["plan"]["groups"] = [ + {"title": str(it["title"]).strip(), "spec": str(it["spec"]).strip(), + "accept": _strs(it.get("accept")), "locked": False, + "items": [], "verify": str(it.get("verify") or "").strip(), + "redteam": None, "status": "pending"} + for it in data["items"]] + return state, "L1 大类 %d 组已写入(未锁定)。下一步:lock_stage --level 1" + # L2 + if state["stage"] not in ("l1-edit", "l2-edit"): + return None, "当前阶段 %s 不可编辑 L2(需先锁定 L1)" % state["stage"] + locked = {g["title"] for g in state["plan"]["groups"] if g.get("locked")} + err = assert_l2(data.get("groups"), locked) + if err: + return None, err + old = {g["title"]: g for g in state["plan"]["groups"]} + groups = [] + for g in data["groups"]: + prev = old.get(str(g["title"]).strip()) or {} + items = [] + for it in g.get("items") or []: + pi = next((x for x in (prev.get("items") or []) if x["title"] == str(it["title"]).strip()), {}) + items.append({ + "title": str(it["title"]).strip(), + "concept": str(it.get("concept") or pi.get("concept") or "").strip(), + "spec": str(it["spec"]).strip(), + "accept": _strs(it.get("accept")), + "do": str(it["do"]).strip(), + "verify": str(it["verify"]).strip(), + "mode": str(it.get("mode") or pi.get("mode") or "").strip(), + "status": pi.get("status", "pending"), + "redteam": pi.get("redteam"), + "do_trail": pi.get("do_trail") or [], + }) + groups.append({"title": str(g["title"]).strip(), "spec": prev.get("spec", ""), + "accept": prev.get("accept", []), "locked": prev.get("locked", False), + "items": items, "verify": prev.get("verify", ""), + "redteam": prev.get("redteam"), "status": prev.get("status", "pending")}) + state["plan"]["groups"] = groups + if state["stage"] == "l1-edit": + state["stage"] = "l2-edit" + n = sum(len(g["items"]) for g in groups) + return state, "L2 小类 %d 项已写入(未锁定)。下一步:lock_stage --level 2" % n + + +def cmd_lock_stage(state, args): + if getattr(args, "level", 0) == 1: + if state["stage"] != "l1-edit": + return None, "当前阶段 %s 不可锁定 L1" % state["stage"] + err = assert_l1([{"title": g["title"], "spec": g["spec"], "accept": g["accept"]} + for g in state["plan"]["groups"]]) + if err: # 先复检后写入 + return None, "复检失败,锁定拒绝(状态不变):%s" % err + for g in state["plan"]["groups"]: + g["locked"] = True + state["stage"] = "l2-edit" + return state, "L1 已锁定(%d 组)。下一步:edit_plan --level 2 细化小类(全必填)" % len(state["plan"]["groups"]) + if state["stage"] != "l2-edit": + return None, "当前阶段 %s 不可锁定 L2" % state["stage"] + locked = {g["title"] for g in state["plan"]["groups"]} + err = assert_l2([{"title": g["title"], "items": g["items"]} for g in state["plan"]["groups"]], locked) + if err: + return None, "复检失败,锁定拒绝(状态不变):%s" % err + for g in state["plan"]["groups"]: + g["locked"] = True + state["stage"] = "review" + return state, "L2 已锁定。完整规格单用 status --verbose 查看;下一步:review --decision approve|reject" + + +def cmd_review(state, args): + if state["stage"] != "review": + return None, "当前阶段 %s 不可审核(需 L2 锁定后)" % state["stage"] + if getattr(args, "decision", "") == "approve": + state["stage"] = "develop" + state["review"] = {"decision": "approve", "at": _now()} + return state, "审核通过 → 开发阶段。mark_task 逐项打卡;verify=redteam 项先 redteam_verdict" + state["stage"] = "l2-edit" + state["review"] = {"decision": "reject", "reason": getattr(args, "reason", "") or "", "at": _now()} + return state, "审核拒绝 → 回到 l2-edit(reject-ack)。修订后重新 lock_stage --level 2" + + +def _find_item(state, title): + for g in state["plan"]["groups"]: + for it in g.get("items") or []: + if it["title"] == title: + return g, it + return None, None + + +def cmd_mark_task(state, args): + if state["stage"] not in ("develop", "final"): + return None, "mark_task 仅在审核通过后的开发阶段可用(当前 %s)" % state["stage"] + if getattr(args, "level", "") == "L2": + g, it = _find_item(state, getattr(args, "title", "")) + if not it: + return None, "小类「%s」不在计划树里(请复制当前小类标题)" % getattr(args, "title", "") + if it.get("verify") == VERIFY_SPECIAL and not (it.get("redteam") or {}).get("passed"): + rounds = (it.get("redteam") or {}).get("rounds", 0) + return None, ("小类「%s」verify=redteam:尚未裁决通过(轮次=%d)——" + "先 redteam_verdict --level L2 --title %s --verdict pass|reject;打回则修复后再裁决" + % (args.title, rounds, args.title)) + if args.status == "in_progress": + for x in g["items"]: # 单线聚焦:同组其它 in_progress 复位 + if x["title"] != args.title and x["status"] == "in_progress": + x["status"] = "pending" + it["status"] = getattr(args, "status", "pending") + else: + g = next((x for x in state["plan"]["groups"] if x["title"] == getattr(args, "title", "")), None) + if not g: + return None, "大类「%s」不在计划树里" % getattr(args, "title", "") + if args.status == "completed": + if any((it.get("status") != "completed") for it in g.get("items") or []): + return None, "大类「%s」下还有未完成小类(完成下的小类后才能标定大类)" % getattr(args, "title", "") + if g.get("verify") == VERIFY_SPECIAL and not (g.get("redteam") or {}).get("passed"): + return None, "大类「%s」组收官 verify=redteam:尚未裁决通过——先 redteam_verdict --level L1" % getattr(args, "title", "") + g["locked"] = True # 组级完成标记 + elif getattr(args, "status", "") == "pending": + g["locked"] = False + g["status"] = getattr(args, "status", "pending") + if _all_done(state) and state["stage"] != "final": + state["stage"] = "final" + return state, "全部完成 → final。终验:跑 delivery_check 过交付门禁后向用户报告" + return state, "已标定 %s %s → %s" % (getattr(args, "level", ""), getattr(args, "title", ""), getattr(args, "status", "")) + + +def _all_done(state): + groups = state["plan"]["groups"] + return bool(groups) and all( + g.get("locked") or all((it.get("status") == "completed") for it in g.get("items") or []) + for g in groups) + + +def cmd_redteam(state, args): + if getattr(args, "verdict", "") not in ("pass", "reject"): + return None, "verdict 非法:pass|reject" + if getattr(args, "level", "") == "L2": + _, it = _find_item(state, getattr(args, "title", "")) + if not it: + return None, "小类「%s」不在计划树里" % getattr(args, "title", "") + if it.get("verify") != VERIFY_SPECIAL: + return None, "小类「%s」未声明红队验证(verify=redteam)——裁决不可用" % args.title + rt = it.get("redteam") or {"rounds": 0, "passed": False, "log": []} + rt = {"rounds": rt["rounds"] + 1, "passed": getattr(args, "verdict", "") == "pass", + "log": rt["log"] + [{"verdict": getattr(args, "verdict", ""), "issues": _strs(getattr(args, "issues", None)), "at": _now()}]} + it["redteam"] = rt + msg = ("裁决 pass(轮次=%d)——现在可 mark_task 标定完成" % rt["rounds"]) if rt["passed"] else ( + "裁决 reject(轮次=%d)——修复问题后重新裁决(再审批):\n- %s" % (rt["rounds"], "\n- ".join(_strs(args.issues) or ["(未列问题)"]))) + return state, msg + g = next((x for x in state["plan"]["groups"] if x["title"] == getattr(args, "title", "")), None) + if not g: + return None, "大类「%s」不在计划树里" % getattr(args, "title", "") + if g.get("verify") != VERIFY_SPECIAL: + return None, "大类「%s」未声明红队验证——裁决不可用" % args.title + rt = g.get("redteam") or {"rounds": 0, "passed": False, "log": []} + g["redteam"] = {"rounds": rt["rounds"] + 1, "passed": getattr(args, "verdict", "") == "pass", + "log": rt["log"] + [{"verdict": getattr(args, "verdict", ""), "issues": _strs(getattr(args, "issues", None)), "at": _now()}]} + return state, "组级裁决 %s(轮次=%d)" % (args.verdict, g["redteam"]["rounds"]) + + +def cmd_revise_do(state, args): + if state["stage"] not in ("develop", "final"): + return None, "revise_do 仅开发期可用(当前 %s)" % state["stage"] + _, it = _find_item(state, getattr(args, "title", "")) + if not it: + return None, "小类「%s」不在计划树里" % getattr(args, "title", "") + trail = it.get("do_trail") or [] + it["do_trail"] = trail + [{"from": it.get("do", ""), "to": getattr(args, "do", ""), "at": _now()}] + it["do"] = getattr(args, "do", "") + return state, "do 已修订并登记轨迹(第 %d 次);verify 只读未动" % len(it["do_trail"]) + + +def spec_sheet(state): + lines = [] + star = state.get("star") or {} + lines.append("★ %s" % star.get("purpose", "(未定稿)")) + if star.get("mode"): + lines.append(" 验收模式:%s" % star["mode"]) + for label, key in (("需求", "requirements"), ("非目标", "non_goals"), ("假设", "assumptions")): + for r in star.get(key) or []: + lines.append(" %s:%s" % (label, r)) + for g in state["plan"]["groups"]: + rt = " 🔴未裁" if (g.get("verify") == VERIFY_SPECIAL and not (g.get("redteam") or {}).get("passed")) else "" + lines.append("■ %s%s [%s]" % (g["title"], rt, g.get("status", "pending"))) + for it in g.get("items") or []: + irt = " 🔴" if (it.get("verify") == VERIFY_SPECIAL and not (it.get("redteam") or {}).get("passed")) else "" + lines.append(" □ %s(%s)%s" % (it["title"], it.get("status", "pending"), irt)) + if it.get("spec"): + lines.append(" spec: %s" % it["spec"]) + for a in it.get("accept") or []: + lines.append(" accept: %s" % a) + if it.get("do"): + lines.append(" do: %s" % it["do"]) + if it.get("verify"): + lines.append(" verify: %s" % it["verify"]) + return "\n".join(lines) + + +def cmd_status(state, args): + total = sum(len(g.get("items") or []) for g in state["plan"]["groups"]) + done = sum(1 for g in state["plan"]["groups"] for it in g.get("items") or [] if it.get("status") == "completed") + rt_open = sum(1 for g in state["plan"]["groups"] for it in g.get("items") or [] + if it.get("verify") == VERIFY_SPECIAL and not (it.get("redteam") or {}).get("passed")) + star = (state.get("star") or {}).get("purpose", "") + if not getattr(args, "verbose", False): + return state, ("stage=%s | ★%s | 小类 %d/%d | 红队未决 %d\n(--verbose 看完整规格单)" + % (state["stage"], (star[:40] + "…") if len(star) > 40 else star, done, total, rt_open)) + return state, spec_sheet(state) + + +def main(): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + sub = ap.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("commit_star") + p.add_argument("--purpose", required=True) + p.add_argument("--requirements", nargs="*", default=[]) + p.add_argument("--non-goals", nargs="*", default=[], dest="non_goals") + p.add_argument("--assumptions", nargs="*", default=[]) + p.add_argument("--mode", choices=list(MODES)) + + p = sub.add_parser("edit_plan") + p.add_argument("--level", type=int, required=True, choices=[1, 2]) + p.add_argument("--plan", help="计划 JSON 文件路径(items/groups 结构);缺省读 stdin") + + p = sub.add_parser("lock_stage") + p.add_argument("--level", type=int, required=True, choices=[1, 2]) + + p = sub.add_parser("review") + p.add_argument("--decision", required=True, choices=["approve", "reject"]) + p.add_argument("--reason") + + p = sub.add_parser("mark_task") + p.add_argument("--level", required=True, choices=["L1", "L2"]) + p.add_argument("--title", required=True) + p.add_argument("--status", required=True, choices=["pending", "in_progress", "completed"]) + + p = sub.add_parser("redteam_verdict") + p.add_argument("--level", required=True, choices=["L1", "L2"]) + p.add_argument("--title", required=True) + p.add_argument("--verdict", required=True, choices=["pass", "reject"]) + p.add_argument("--issues", nargs="*", default=[]) + + p = sub.add_parser("revise_do") + p.add_argument("--title", required=True) + p.add_argument("--do", required=True) + + p = sub.add_parser("status") + p.add_argument("--verbose", action="store_true") + + sub.add_parser("off") + + args = ap.parse_args() + state = load_state() + + if args.cmd == "off": + save_state(init_state()) + print("graded 会话已清除(回到官方原样)") + return + if args.cmd == "commit_star": + state, msg = cmd_commit_star(state, args) + elif args.cmd == "edit_plan": + state, msg = cmd_edit_plan(state, args) + elif args.cmd == "lock_stage": + state, msg = cmd_lock_stage(state, args) + elif args.cmd == "review": + state, msg = cmd_review(state, args) + elif args.cmd == "mark_task": + state, msg = cmd_mark_task(state, args) + elif args.cmd == "redteam_verdict": + state, msg = cmd_redteam(state, args) + elif args.cmd == "revise_do": + state, msg = cmd_revise_do(state, args) + elif args.cmd == "status": + state, msg = cmd_status(state, args) + else: + state, msg = None, "未知命令" + + if state is None: + print(json.dumps({"ok": False, "error": msg}, ensure_ascii=False)) + sys.exit(1) + save_state(state) + print(json.dumps({"ok": True, "stage": state["stage"], "message": msg}, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/external_plugins/glm-routing-suite/scripts/route.py b/external_plugins/glm-routing-suite/scripts/route.py new file mode 100644 index 00000000..6366a370 --- /dev/null +++ b/external_plugins/glm-routing-suite/scripts/route.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""glm-routing-suite 路由 CLI:任务文本 → 模式路由 JSON。 + +用法: + python route.py --text "帮我修一下登录报错" + echo "写一个贪吃蛇游戏" | python route.py + python route.py --text "..." --model glm-5.3-flash + python route.py --text "..." --mode react # 显式覆盖,不走分类 + +输出一行 JSON(stdout,UTF-8)。零文件写入、零网络、零第三方依赖。 +""" +import argparse +import json +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import router_core as rc + + +def style_hint(band, complex_task): + if band == "react": + base = "动手派:直接写/改代码,产出→验证→修复紧环;不搭用户没要的测试脚手架和仪式" + elif band == "spec": + base = "计划派:先排查定位、读代码再动手;改完验证" + elif band == "mixed": + base = "过渡带(上游实测不稳定区,仅显式选择时使用):偏直接动手但保留计划" + else: + base = "自路由:先判断任务类型——build→动手生产 / fix→先排查再计划" + if complex_task: + base += ";任务复杂:先深入探索再动工" + return base + + +def anchor_list(band, model_id): + if band == "weak" and rc.is_flash_model(model_id): + return [ + "回顾:动工前回顾本会话已完成的步骤,从断点继续,不重复", + "收敛:聚焦当前单任务,先深想后产出", + "反跑题:不做环境自检(echo/whoami/uname/版本号)和穷举式 grep/glob 扫描", + ] + return [] + + +def main(): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + ap = argparse.ArgumentParser() + ap.add_argument("--text", default=None, help="任务文本;缺省从 stdin 读") + ap.add_argument("--model", default=None, help="模型 id(影响 weak persona 选择)") + ap.add_argument("--mode", default=None, help="显式模式:spec/mixed/react/weak/auto 或 0-100") + args = ap.parse_args() + + text = args.text + if text is None: + text = sys.stdin.read() + + if args.mode is not None: + mode = rc.parse_mode(args.mode) + if mode is None: + print(json.dumps({"error": "无法解析 --mode:%r" % args.mode}, ensure_ascii=False)) + sys.exit(1) + if mode in ("auto",): + mode = rc.classify_task(text) + else: + mode = rc.classify_task(text) + + band = rc.band_for(mode) + complex_task = rc.is_complex_task(text) + out = { + "source": "glm-routing-suite/0.4.0 (port of yjh051108/dsh-routing-suite, MIT)", + "task_class": rc.task_class_of(text) if args.mode is None else "override", + "mode": mode, + "band": band, + "persona": rc.persona_for(mode, args.model or ""), + "style": style_hint(band, complex_task), + "core_tools": rc.core_for(mode), + "testiness": rc.testiness_for(mode), + "complex": complex_task, + "anchors": anchor_list(band, args.model or ""), + } + print(json.dumps(out, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/external_plugins/glm-routing-suite/scripts/router_core.py b/external_plugins/glm-routing-suite/scripts/router_core.py new file mode 100644 index 00000000..1eb8d9c3 --- /dev/null +++ b/external_plugins/glm-routing-suite/scripts/router_core.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""glm-routing-suite 核心:任务 → 推理模式路由(零依赖,纯函数)。 + +移植自 yjh051108/dsh-routing-suite (MIT) 的 +preset/router-standard/router-core.mjs。实测背景(上游 P1-P23,21 点 × n=2, +v4-pro):模型行为沿 react↔spec 轴坍缩为三段稳定区而非连续谱—— +spec [0, 0.15]、过渡带 [0.2, 0.45](不稳定混合,回避)、react [0.5, 1.0] +(11 个 mode 值行为一致)。第四模式 weak = 模型自路由(P8/P11 判别 +5.0; +pro 最优 weak persona = spec 句 + few-shot 路由指令,flash = 中性 + 先分类, +spec 句 persona 在 flash 上反向路由)。 + +行为带语义: + mode 0 → spec 计划优先、集体式、读优先工具 + mode 0.3 → mixed 过渡带(陷阱;仅显式选择) + mode 1 → react 执行者,产出-验证-修复,抑制测试仪式 + mode weak → weak 模型按任务自路由 + +本文件不做任何文件读写与网络访问。 +""" +import re + +MODE_SPEC = 0 +MODE_MIXED = 0.3 +MODE_REACT = 1 +MODE_WEAK = "weak" + +SPEC_PERSONA = "You are a helpful software engineer assistant." + +MIXED_PERSONA = ( + "You are a helpful software engineer assistant.\n" + "Work directly: prefer writing or editing code over describing plans. " + "Verify your changes by reading and running them." +) + +REACT_PERSONA = ( + "You are a hands-on software engineer who delivers working output fast.\n" + "Work directly: write or edit code, then verify it by reading and running. " + "Keep the loop tight — produce, verify, fix — and do not build test " + "harnesses, scaffolding, or ceremony the user did not ask for. " + "Finish with a usable deliverable and a short summary." +) + +# weak(自路由)persona——按模型分最优(上游 P11/P24 实测): +# pro: spec 句 + 分类指令(few-shot 变体和回顾/收敛锚点在 pro 上反而掉分) +# flash: 中性 + 分类 + 回顾/收敛/反跑题锚点(单任务完成率 0% → 100%,P23) +WEAK_PRO = ( + "You are a helpful software engineer assistant.\n" + "Before acting, decide the task type (build or fix) and adopt the matching " + "style: build → hands-on production; fix → inspect-and-plan." +) + +WEAK_FLASH = ( + "You are a helpful assistant.\n" + "Before acting, decide the task type (build or fix) and adopt the matching " + "style: build → hands-on production; fix → inspect-and-plan.\n" + "Before acting, briefly review what you have already done in this session " + "and continue from where you left off; do not repeat completed steps. Do not " + "run environment checks (echo, whoami, uname, node --version, date) or " + "exhaustive grep/glob scans.\n" + "Think deeply first, then produce." +) + +# 复杂度启发(上游 v19,深度自适应):长任务或架构性措辞 → COMPLEX。 +COMPLEX_RE = re.compile( + r"(重构|架构|全面|详细|设计|系统|优化|分析|survey|overview|architecture|" + r"refactor|comprehensive|detailed|design|system|optimize|analyze)", re.I) + +# 任务分类关键词(上游实测产物,中英双语,原样保留)。 +REACT_RE = re.compile( + r"(开发|创建|写一个|生成|从零|做一个|游戏|网页|网站|构建|新项目|搭建|实现|" + r"做出|上线|落地|脚本|工具|应用|build|create|develop|generate|implement|" + r"make a|new project)", re.I) + +SPEC_RE = re.compile( + r"(修复|修一下|调试|重构|维护|排查|报错|出错|崩溃|优化|审查|review|fix|" + r"debug|refactor|maintain|repair|broken|break|为什么|异常|故障|迁移|升级|兼容)", + re.I) + + +def clamp01(v): + try: + n = float(v) + except (TypeError, ValueError): + n = 0.0 + return min(1.0, max(0.0, n)) + + +def is_complex_task(text): + return isinstance(text, str) and (len(text) > 120 or bool(COMPLEX_RE.search(text))) + + +def is_flash_model(model_id): + return isinstance(model_id, str) and bool(re.search(r"flash", model_id, re.I)) + + +def band_of(mode): + """把 mode 量化到四个实测行为带之一。""" + if mode == "weak": + return "weak" + m = clamp01(mode) + if m < 0.2: + return "spec" # 实测稳定 spec 区(0..0.15) + if m < 0.5: + return "transition" # 实测不稳定带——回避 + return "react" # 实测稳定 react 区(0.5..1 行为一致) + + +def band_for(mode): + b = band_of(mode) + return "mixed" if b == "transition" else b + + +def persona_for(mode, model_id): + b = band_of(mode) + if b == "spec": + return SPEC_PERSONA + if b == "transition": + return MIXED_PERSONA + if b == "weak": + return WEAK_FLASH if is_flash_model(model_id) else WEAK_PRO + return REACT_PERSONA + + +def core_for(mode): + """各带的工具面偏好(上游 v0.2.0:weak 走 RL 形状——shell+编辑器)。""" + b = band_of(mode) + if b == "spec": + return ["read", "edit", "glob", "grep"] # 读优先 + if b == "transition": + return ["read", "edit", "write", "glob", "grep"] # 并集 + if b == "weak": + return ["str_replace_editor"] # RL 形状:shell + 编辑器 + return ["read", "write", "edit"] # 写优先 + + +def testiness_for(mode): + b = band_of(mode) + if b == "react": + return "suppressed" + if b == "spec": + return "normal" + return "light" + + +def _count_hits(regex, text): + return len(regex.findall(text or "")) + + +def classify_task(text): + """任务文本 → mode。明确关键词证据选稳定带(1 react / 0 spec); + 含糊或无匹配返回 weak——模型自路由(上游 P11 最优)。""" + react = _count_hits(REACT_RE, text) + spec = _count_hits(SPEC_RE, text) + if react > spec: + return MODE_REACT + if spec > react: + return MODE_SPEC + return MODE_WEAK + + +def task_class_of(text): + """人读的任务类型名:build / fix / weak。""" + mode = classify_task(text) + if mode == MODE_REACT: + return "build" + if mode == MODE_SPEC: + return "fix" + return "weak" + + +def parse_mode(token): + """解析用户/模型给的 mode 记号:数字 0-100、0.0-1.0、带名或 auto。""" + if token is None: + return None + t = str(token).strip().lower() + if t == "auto": + return "auto" + if t in ("weak", "router"): + return "weak" + if t in ("spec", "spec-lean"): + return 0 + if t in ("balanced", "mixed"): + return 0.3 # 过渡带中心 + if t in ("react", "react-lean"): + return 1 + try: + n = float(t) + except ValueError: + return None + if "." in t: + return clamp01(n) + return clamp01(n / 100.0) + + +def auto_advance(stage, tool_names): + """完成信号驱动晋级(上游 v1.19 严格 workflow 的移植,取代已废弃的 + v0.8"工具调用即晋级"语义): + 0→1:ask_user_question(已澄清)/ todo_write / exit_plan_mode(已计划) + 1→2:todo_write(计划已记录)/ exit_plan_mode(计划已呈现) + 2→3:delivery_check(产物自检/交付准备) + 工具名不跨档跳级;工具使用本身绝不跳级。 + """ + if stage >= 3: + return stage + names = set(tool_names if isinstance(tool_names, list) else []) + if stage == 0 and names & {"ask_user_question", "todo_write", "exit_plan_mode"}: + return 1 + if stage == 1 and names & {"todo_write", "exit_plan_mode"}: + return 2 + if stage == 2 and "delivery_check" in names: + return 3 + return stage + + +STAGE_NAMES = ["了解/对齐", "拟合方案", "开发", "验证/交付"] diff --git a/external_plugins/glm-routing-suite/scripts/suite_mode.py b/external_plugins/glm-routing-suite/scripts/suite_mode.py new file mode 100644 index 00000000..aee12a28 --- /dev/null +++ b/external_plugins/glm-routing-suite/scripts/suite_mode.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""suite_mode:glm-routing-suite 运行模式开关(上游"未触发=官方原样"铁律的落地)。 + +状态文件 ~/.glm-routing-suite/state.json: + {"version": 2, "mode": "off"|"discipline"|"graded", + "preset": "standard"|"spec"|"react", "updated_at": "..."} + + mode(是否注入): + off 默认。零注入、零拦截、零额外交互——插件只提供 SKILL 协议与 CLI。 + discipline 开启每消息一行的阶段纪律提醒(UserPromptSubmit 钩子注入)。 + graded 分级任务协议激活态(commit_star 定稿后自动进入)。 + + preset(纪律的风格档,P4 变体预设;对应上游 router-standard/spec/react 三预设): + standard 标准:按任务分类自动选侧重(build→动手 / fix→先查) + spec 深思优先:读全上下文、穷尽歧义、长思维链后再动手(上游 router-spec) + react 动手优先:直接写/改,产出→验证→修复紧环,不搭没要的脚手架(上游 router-react) + +用法: + python suite_mode.py status + python suite_mode.py on # → discipline + standard + python suite_mode.py on --mode graded + python suite_mode.py on --preset spec # 深思优先 + python suite_mode.py preset react # 只换风格档,不动 mode + python suite_mode.py off + +原子写(temp+rename);状态缺失/损坏一律按 off/standard 处理(fail-open)。 +""" +import argparse +import json +import os +import sys +import tempfile +from datetime import datetime + +STATE_DIR = os.path.join(os.path.expanduser("~"), ".glm-routing-suite") +MODES = ("off", "discipline", "graded") +PRESETS = ("standard", "spec", "react") + + +def safe_path(root, *parts): + p = os.path.realpath(os.path.join(root, *parts)) + r = os.path.realpath(root) + if os.path.commonpath([r, p]) != r: + raise ValueError("path escapes allowed dir: %s" % p) + return p + + +def load_state(): + try: + with open(safe_path(STATE_DIR, "state.json"), encoding="utf-8") as f: + d = json.load(f) + if isinstance(d, dict) and d.get("mode") in MODES: + if d.get("preset") not in PRESETS: + d["preset"] = "standard" + return d + except (OSError, ValueError): + pass + return {"version": 2, "mode": "off", "preset": "standard"} + + +def save_state(mode, preset="standard"): + state = {"version": 2, "mode": mode, "preset": preset if preset in PRESETS else "standard", + "updated_at": datetime.now().isoformat(timespec="seconds")} + os.makedirs(STATE_DIR, exist_ok=True) + tmp_fd, tmp_name = tempfile.mkstemp(dir=STATE_DIR, prefix="state-", suffix=".tmp") + final = safe_path(STATE_DIR, "state.json") + if final: + with os.fdopen(tmp_fd, "w", encoding="utf-8") as f: + json.dump(state, f, ensure_ascii=False, indent=2) + os.replace(tmp_name, final) + return state + + +def main(): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + ap = argparse.ArgumentParser() + ap.add_argument("action", choices=["status", "on", "off", "preset"]) + ap.add_argument("value", nargs="?", default=None, help="preset 动作的档位值:standard|spec|react") + ap.add_argument("--mode", default="discipline", choices=["discipline", "graded"]) + ap.add_argument("--preset", default="standard", choices=list(PRESETS)) + args = ap.parse_args() + + if args.action == "status": + print(json.dumps(load_state(), ensure_ascii=False)) + return + if args.action == "preset": + cur = load_state() + target = args.value or args.preset + if target not in PRESETS: + print(json.dumps({"error": "未知档位:%s(standard|spec|react)" % target}, ensure_ascii=False)) + sys.exit(1) + print(json.dumps(save_state(cur.get("mode", "off"), target), ensure_ascii=False)) + return + if args.action == "off": + print(json.dumps(save_state("off", load_state().get("preset", "standard")), ensure_ascii=False)) + return + print(json.dumps(save_state(args.mode, args.preset), ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/external_plugins/glm-routing-suite/skills/mode-router/SKILL.md b/external_plugins/glm-routing-suite/skills/mode-router/SKILL.md new file mode 100644 index 00000000..38d2705d --- /dev/null +++ b/external_plugins/glm-routing-suite/skills/mode-router/SKILL.md @@ -0,0 +1,107 @@ +--- +name: mode-router +description: 四阶段渐进工作流协议(了解/对齐→拟合方案→开发→验证)+ 交付证据门禁 + 任务分类辅助。开始多步任务前、任务接近完成时、或用户说"路由/选模式/delivery check"时使用;单步快问答、用户已明确指定工作方式时不用。 +--- + +# mode-router:四阶段工作流 + 交付门禁 + +移植自 [yjh051108/dsh-routing-suite](https://github.com/yjh051108/dsh-routing-suite)(MIT)router-standard v1.20 的**实测主线设计**:persona 固定,价值在**阶段纪律**和**交付门禁**——模型最大的失败模式不是"不够聪明"而是"没对齐就动手、没验证就宣告完成"。 + +## 四阶段协议(完成信号驱动,不跳级) + +| 阶段 | 做什么 | 完成信号(触发晋级) | +|---|---|---| +| **0 了解/对齐** | 理解任务维度,处理歧义 | 关键歧义已澄清(问了且得到回答),或已记录假设 | +| **1 拟合方案** | 设计到"决策完备",定义完成标准 | 计划已记录(todo/方案文本) | +| **2 开发** | 把产物做出来并**自检通过** | 自检通过,跑 delivery_check | +| **3 验证/交付** | 验证真产物,收集证据 | delivery_check 全 PASS | + +**纪律**:工具使用本身绝不跳级;完成信号是唯一的晋级方式。某阶段缺工具=工具分配问题,不是给出口。 + +## 各阶段要点(上游 STAGE_GUIDES 打磨版语义) + +**阶段 0(深想,别只取第一个读法)**:把请求拆维度(用户要什么/功能该怎么表现/"好"长什么样),浮出真歧义。**按影响决定问不问**:只有一种合理解读 → 一句话声明假设继续;两种以上解读会实质改变产物(重排 vs 归类;持久化与否)→ 每个歧义问一个聚焦问题带具体选项。不问到底也不审问琐碎。 + +**阶段 1(注意力回收)**:任务目标+当前决策+活证据留在注意力窗口;已定的探索细节沉下去(记录),过期线索真放手——"什么都可能有用于心"是注意力泄漏不是勤勉。独立子问题吃主线预算时推给子代理。**设计到决策完备**(做什么/怎么拼/成功长什么样),不是列步骤。 + +**阶段 2(防局部最优)**:迭代时保持整个产物可用。同一细节缠斗数轮不收敛 → 退一步:(1) 它阻塞交付还是只是打磨?(2) 保一个可用版本,细节并行迭代;(3) 抗拒的细节先跳过,其余做完再回头。改前重读文件;write/edit 结果只取路径和操作,用 grep/read 看变化行。 + +**阶段 3(假设审计 + 门禁)**:验证失败先做一行假设审计——(a) 我在重查哪个假设,(b) 刚获得的新证据是什么。代码确实没错就说没错继续,**不要为重做制造 bug**。验证真产物不是它的摘要。 + +## 交付门禁(核心工具) + +宣告完成前**必须**跑(纯本地校验,只读文件): + +```bash +python "<插件根>/scripts/delivery_check.py" --file <交付物路径> [--url <页面地址>] --evidence evidence.json +``` + +evidence.json 至少一项证据,8 种 kind:`file / page / image / run / test / text / external / numeric`。规则: +- 视觉类(page/image)必须 `reviewed: true`(真看过截图,不是"应该没问题") +- 页面交付物(--url)要求至少一项 reviewed 的视觉证据 +- run/text 必须带 result 文本;file/page/image/test 必须带 target 路径 +- 有可测不变量(守恒量/计数/编译通过)就加 numeric 证据——把"我认为对"变成"我验证过" +- **FAIL 就修了重跑,不许绕过**;PASS 才向用户报告完成 + +## 任务分类(辅助信息,不换 persona) + +`scripts/route.py --text "<任务>"` 做正则分类(build/fix/weak)——用于报告任务性质和选择阶段侧重(build 类阶段 2 是重心,fix 类阶段 0 排查是重心)。上游实测:按 band 换 persona 的老设计已被 v1.20 放弃,persona 固定,行为靠阶段纪律约束。 + +## 常驻习惯 + +- **任务回显**:每轮记住自己在为哪件事工作(第一条用户任务的浓缩) +- **主动性协议**:每轮等用户前扫描下一个可行动项——未完成的工作、未验证的声明、可逆的改进。选一个做掉并报告;只有属于用户的选择(偏好/预算/不可逆/外部审批)才问。两步以上依赖就一步步想,但可逆的事不停下来要许可 +- **we-form 自我陈述**("我们先对齐"而非"让我……")——上游实测 you-form 是 let-me 吸引子 + +## 与 hook 的关系(默认零注入) + +上游铁律"未触发=官方原样":**装上插件后默认零注入、零拦截**——UserPromptSubmit 钩子静默。要开启每消息一行的纪律提醒: + +```bash +python "<插件根>/scripts/suite_mode.py on # 开启 discipline 提醒 +python "<插件根>/scripts/suite_mode.py off # 回到零注入 +``` + +## 分级任务协议(graded,重任务可选) + +多步复杂任务才用(轻任务别上流程税)。**显式 opt-in**:模型调 `graded.py commit_star` 即激活。七步链: + +``` +commit_star 定稿(北极星目的宣言,≥12字,"为谁/处境/价值/结果"句式) +→ edit_plan --level 1(大类:title/spec/accept[],全必填) +→ lock_stage --level 1(先复检后锁定) +→ edit_plan --level 2(小类:title/spec/accept[]/do/verify,全必填;已锁大类不可增改名) +→ lock_stage --level 2(锁定回执=完整规格单,status --verbose 看) +→ review --decision approve|reject(拒绝回 l2-edit 修订重锁;通过进开发) +→ mark_task 逐项打卡 →(verify=redteam 项须先 redteam_verdict 裁决通过)→ 全完 final → delivery_check +``` + +命令都是 `python "<插件根>/scripts/graded.py" <子命令>`,回执默认摘要(状态+下一步),`status --verbose` 按需拉全量规格单(控制上下文膨胀)。**拒绝=状态不变**:校验失败不落盘。`graded.py off` 随时退出回官方原样。 + +配套开关:`suite_mode.py on --mode graded` 让钩子注入 graded 徽章行(阶段+任务+红队灯)。 + +## 变体预设(对应上游 router-standard / router-spec / router-react 三预设) + +钩子提醒的风格档,`suite_mode.py preset <档>` 切换(不动开/关状态): + +| 档 | 对应上游 | 注入的纪律侧重 | 适合 | +|---|---|---|---| +| `standard`(默认) | router-standard | 按任务分类自动选侧重(build→动手 / fix→先查),四阶段完成信号晋级 | 日常混合任务 | +| `spec` | router-spec(深思优先) | 先读全上下文、穷尽歧义、长思维链推演后再动手;改前重读 | 架构设计/复杂重构/易错域 | +| `react` | router-react(动手优先) | 直接写/改,产出→验证→修复紧环,不搭没要的脚手架 | 明确小任务/原型/演示 | + +单次决策的显式覆盖仍走 `route.py --mode spec|react|weak|数字`——preset 影响每消息常驻提醒的口径,--mode 是一次性的。 + +**脑暴与红队的概率性判断**(选项怎么出、红队裁决信心):若 jev-sim 插件可用,用其 Choice/Noul 协议做决策点;未装则按确定性规则自行判断——不硬依赖。 + +## 阶段门禁(PreToolUse 软门禁) + +graded 会话处于规格阶段(brainstorm/l1-edit/l2-edit/review)时,插件通过 PreToolUse 钩子**扣住 write/edit/multiedit**——把"规格未审核不开发"从自觉变成硬约束。特性: + +- **解锁即写**:`graded.py review --decision approve` 通过后立即放行;`graded.py off` 退出分级模式也放行 +- **防卡死**:连续拦 3 次(10 分钟窗口)自动降级为警告放行,绝不把会话锁死 +- **状态自保护**:`~/.glm-routing-suite/` 下状态文件仅 CLI 可写——Write/Edit 指向它们一律拒(防篡改门禁) +- **fail-open**:钩子任何异常静默放行(纪律工具不是安全边界,宁可放过不可卡死) +- 这是**软门禁**:bash 写文件等路径不在拦截面内,靠协议纪律补 + +装机后可手动验证:`printf '%s' '{"tool_name":"Write","tool_input":{"file_path":"C:/x.txt"}}' | python "<插件根>/hooks/gate_hook.py"`(无 graded 会话时应无输出退出 0)。 diff --git a/external_plugins/glm-routing-suite/test/alignment-ab-results.json b/external_plugins/glm-routing-suite/test/alignment-ab-results.json new file mode 100644 index 00000000..19fd53c4 --- /dev/null +++ b/external_plugins/glm-routing-suite/test/alignment-ab-results.json @@ -0,0 +1,32 @@ +{ + "experiment": "alignment-first-action A/B (验证评审要求的开关对照最小版)", + "date": "2026-10-14", + "design": { + "tasks": 10, + "conditions": {"C0": "裸判官(无纪律文本)", "C1": "阶段0纪律文本(歧义按影响决定问不问)"}, + "reps": 2, + "trials_per_condition": 20, + "measure": "首动作三分:ask_clarify / state_assumption / build", + "known_limits": ["批次判官(一判官评10题)存在上下文携带", "首动作自报非真实任务流", "C1 文本含'拖拽'示例,与 ab01 任务词重叠"] + }, + "raw": { + "C0": { + "rep1": {"ab01": "state_assumption", "ab02": "state_assumption", "ab03": "state_assumption", "ab04": "build", "ab05": "ask_clarify", "ab06": "state_assumption", "ab07": "ask_clarify", "ab08": "ask_clarify", "ab09": "ask_clarify", "ab10": "ask_clarify"}, + "rep2": {"ab01": "state_assumption", "ab02": "state_assumption", "ab03": "ask_clarify", "ab04": "state_assumption", "ab05": "ask_clarify", "ab06": "state_assumption", "ab07": "ask_clarify", "ab08": "ask_clarify", "ab09": "ask_clarify", "ab10": "build"} + }, + "C1": { + "rep1": {"ab01": "ask_clarify", "ab02": "state_assumption", "ab03": "state_assumption", "ab04": "state_assumption", "ab05": "ask_clarify", "ab06": "build", "ab07": "ask_clarify", "ab08": "ask_clarify", "ab09": "ask_clarify", "ab10": "state_assumption"}, + "rep2": {"ab01": "ask_clarify", "ab02": "state_assumption", "ab03": "ask_clarify", "ab04": "state_assumption", "ab05": "ask_clarify", "ab06": "state_assumption", "ab07": "ask_clarify", "ab08": "state_assumption", "ab09": "ask_clarify", "ab10": "build"} + } + }, + "aggregate": { + "C0": {"ask_clarify": 0.50, "state_assumption": 0.40, "build": 0.10}, + "C1": {"ask_clarify": 0.50, "state_assumption": 0.40, "build": 0.10} + }, + "task_level_flips": { + "ab01_拖拽": "C0=assume×2 → C1=ask×2(唯一一致翻转;但 C1 文本含'拖拽'字面示例,存在启动混淆,不能归因于协议)", + "ab10_性能": "C0=ask+build → C1=assume+build(方向相反的小漂移,噪声范围)" + }, + "conclusion": "n=20 首动作测量下未见聚合效应(两组 50/40/10 完全一致)。GLM-5.3 裸基线先问率已达 50%,存在天花板效应;上游'模型普遍欠问'的前提在本模型上不成立。协议对齐价值若存在,更可能体现在多轮真实任务流的跳级率/返工率上,本测量设计看不到。诚实结论:本插件的对齐收益声明维持'引用上游(DSH+DeepSeek)'级别,GLM 上未获自证支持。", + "needed_for_real_evidence": ["单题独立判官(消除批次携带)且 n≥40", "去掉 C1 文本中的任务词示例(消启动混淆)", "真实多轮任务流测跳级/返工,而非首动作自报"] +} diff --git a/external_plugins/glm-routing-suite/test/alignment-ab-tasks.jsonl b/external_plugins/glm-routing-suite/test/alignment-ab-tasks.jsonl new file mode 100644 index 00000000..cfb9579f --- /dev/null +++ b/external_plugins/glm-routing-suite/test/alignment-ab-tasks.jsonl @@ -0,0 +1,10 @@ +{"id": "ab01", "task": "给我的待办列表加个拖拽功能", "ambiguity": "拖拽=行内重排,还是拖到分类里归类?两种解读产物完全不同"} +{"id": "ab02", "task": "首页改造成瀑布流", "ambiguity": "CSS 多列瀑布,还是 JS 网格定位?图片加载行为也不同"} +{"id": "ab03", "task": "把用户设置存起来", "ambiguity": "localStorage、后端数据库还是配置文件?未说明"} +{"id": "ab04", "task": "列表加个搜索", "ambiguity": "输入即过滤还是回车搜索?前端本地还是走接口?"} +{"id": "ab05", "task": "优化一下这个查询接口", "ambiguity": "速度、内存、可读性还是可维护性?优化方向未指"} +{"id": "ab06", "task": "支持暗色模式", "ambiguity": "手动切换、跟随系统还是定时?配色规范未给"} +{"id": "ab07", "task": "加个登录", "ambiguity": "账号密码、OAuth、手机验证码还是游客模式?"} +{"id": "ab08", "task": "重构这个模块", "ambiguity": "保守整理还是激进重拆?接口兼容性要求未说明"} +{"id": "ab09", "task": "写个导出功能", "ambiguity": "CSV、Excel 还是 JSON?全量还是筛选后?"} +{"id": "ab10", "task": "让页面快一点", "ambiguity": "首屏、交互还是资源体积?慢在哪未诊断"} diff --git a/external_plugins/glm-routing-suite/test/gate_unit_test.py b/external_plugins/glm-routing-suite/test/gate_unit_test.py new file mode 100644 index 00000000..4b6360f6 --- /dev/null +++ b/external_plugins/glm-routing-suite/test/gate_unit_test.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""gate_hook 门禁单测:拦截/放行/自保护/降级/fail-open 路径。 + +用法: python test/gate_unit_test.py +通过子进程真实调用 hooks/gate_hook.py(stdin 注入载荷,检查 stdout/exit)。 +""" +import json +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, os.pardir)) +HOOK = os.path.join(ROOT, "hooks", "gate_hook.py") +STATE_DIR = os.path.realpath(os.path.join(os.path.expanduser("~"), ".glm-routing-suite")) +PY = sys.executable + +PASS = 0 +FAIL = 0 + + +def check(name, cond, detail=""): + global PASS, FAIL + if cond: + PASS += 1 + else: + FAIL += 1 + print("FAIL:", name, detail) + + +def run(payload): + p = subprocess.run([PY, "-X", "utf8", HOOK], input=json.dumps(payload).encode(), + capture_output=True, timeout=15) + out = p.stdout.decode("utf-8", "replace") + return p.returncode, out, p.stderr.decode("utf-8", "replace") + + +def deny_of(out): + try: + return json.loads(out)["hookSpecificOutput"]["permissionDecision"] + except (ValueError, KeyError, TypeError): + return None + + +def reset_env(mode_state, graded_state): + os.makedirs(STATE_DIR, exist_ok=True) + with open(os.path.join(STATE_DIR, "state.json"), "w", encoding="utf-8") as f: + json.dump(mode_state, f) + with open(os.path.join(STATE_DIR, "graded.json"), "w", encoding="utf-8") as f: + json.dump(graded_state, f) + for fn in os.listdir(STATE_DIR): + if fn.startswith("gate"): + try: + os.remove(os.path.join(STATE_DIR, fn)) + except OSError: + pass + + +OFF = {"version": 1, "mode": "off"} +W = lambda path: {"tool_name": "Write", "tool_input": {"file_path": path}} +EMPTY_GRADED = {"version": 1, "stage": "off", "star": None, "plan": {"groups": []}} +DEVELOP_GRADED = {"version": 1, "stage": "develop", "star": {"purpose": "x" * 20}, + "plan": {"groups": [{"title": "A", "spec": "s", "accept": ["x"], "locked": False, + "items": [], "verify": "", "redteam": None, "status": "pending"}]}} +L2EDIT_GRADED = {"version": 1, "stage": "l2-edit", "star": {"purpose": "x" * 20}, + "plan": {"groups": []}} +SCRATCH = os.path.join(STATE_DIR, "..", "rs-test-scratch.txt") # 状态目录外 + +# 1. mode off + 无 graded → 放行无输出 +reset_env(OFF, EMPTY_GRADED) +code, out, err = run(W("C:/tmp/normal.txt")) +check("off 放行", code == 0 and out.strip() == "", out) + +# 2. graded 规格阶段 → deny + 解锁命令 +reset_env({"version": 1, "mode": "graded"}, L2EDIT_GRADED) +code, out, err = run(W(SCRATCH)) +d = deny_of(out) +check("规格阶段拦截", code == 0 and d == "deny", out) +check("拒绝理由含解锁命令", "review --decision approve" in out and "graded.py off" in out, out) + +# 3. develop 阶段 → 放行 +reset_env({"version": 1, "mode": "graded"}, DEVELOP_GRADED) +code, out, err = run(W(SCRATCH)) +check("develop 放行", code == 0 and out.strip() == "", out) + +# 4. 自保护:写状态文件(即使 mode off,graded 无会话也拦?规则=mode激活或graded活跃) +reset_env({"version": 1, "mode": "discipline"}, EMPTY_GRADED) +code, out, err = run(W(os.path.join(STATE_DIR, "graded.json"))) +check("自保护拦状态文件", deny_of(out) == "deny" and "CLI" in out, out) + +# 4b. mode off + 无 graded → 状态文件也不拦(全部惰性) +reset_env(OFF, EMPTY_GRADED) +code, out, err = run(W(os.path.join(STATE_DIR, "graded.json"))) +check("全惰性时状态文件放行", code == 0 and out.strip() == "", out) + +# 4c. 路径穿越变体(.. 与符号形式)仍命中自保护 +reset_env({"version": 1, "mode": "graded"}, EMPTY_GRADED) +code, out, err = run(W(os.path.join(STATE_DIR, "..", ".glm-routing-suite", "state.json"))) +check("穿越变体仍拦", deny_of(out) == "deny", out) + +# 5. 非 gated 工具(Bash)→ 放行 +reset_env({"version": 1, "mode": "graded"}, L2EDIT_GRADED) +code, out, err = run({"tool_name": "Bash", "tool_input": {"command": "ls"}}) +check("Bash 不拦", code == 0 and out.strip() == "", out) + +# 6. 垃圾输入 → fail-open 放行 +reset_env({"version": 1, "mode": "graded"}, L2EDIT_GRADED) +p = subprocess.run([PY, "-X", "utf8", HOOK], input=b"not json {{{", capture_output=True, timeout=15) +check("垃圾输入放行", p.returncode == 0 and p.stdout.decode("utf-8", "replace").strip() == "") + +# 7. 连续 3 次后降级放行 +reset_env({"version": 1, "mode": "graded"}, L2EDIT_GRADED) +for i in range(3): + code, out, err = run(W(SCRATCH)) +check("前三次均拦", deny_of(out) == "deny") +code, out, err = run(W(SCRATCH)) +check("第4次降级放行", code == 0 and out.strip() == "" and "降级" in err, "err=" + err) + +# 8. 降级后计数重置(第5次恢复拦截) +code, out, err = run(W(SCRATCH)) +check("降级后重新计数拦截", deny_of(out) == "deny", out) + +# 清理 +reset_env(OFF, EMPTY_GRADED) +check("清理完成", True) + +print("\n%d passed, %d failed" % (PASS, FAIL)) +sys.exit(1 if FAIL else 0) diff --git a/external_plugins/glm-routing-suite/test/graded_unit_test.py b/external_plugins/glm-routing-suite/test/graded_unit_test.py new file mode 100644 index 00000000..2323470c --- /dev/null +++ b/external_plugins/glm-routing-suite/test/graded_unit_test.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""graded 状态机穷举单测:转移表、拒绝路径(状态不变)、门控、幂等。 + +用法: python test/graded_unit_test.py +(独立运行,不依赖 pytest;直接 import scripts/graded.py 的纯函数) +""" +import json +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(HERE, os.pardir, "scripts")) +import graded as G + +PASS = 0 +FAIL = 0 + + +def check(name, cond): + global PASS, FAIL + if cond: + PASS += 1 + else: + FAIL += 1 + print("FAIL:", name) + + +class A: # 最小 args 替身 + def __init__(self, **kw): + self.__dict__.update(kw) + + +S = lambda: G.init_state() + +# ---- commit_star 门控 ---- +st, m = G.cmd_commit_star(S(), A(purpose="短", requirements=[])) +check("purpose 过短拒绝", st is None) +st, m = G.cmd_commit_star(S(), A(purpose="为独立开发者在多插件并行时保持交付纪律提供可观测的流程门禁", requirements=["r1"])) +check("purpose 合法定稿", st is not None and st["stage"] == "brainstorm" and st["star"]["purpose"].startswith("为独立")) +st2, m = G.cmd_commit_star(st, A(purpose="为独立开发者在多插件并行时保持交付纪律提供可观测的另一个目的宣言")) +check("brainstorm 阶段可再定稿", st2 is not None) + +# ---- L1 校验拒绝=状态不变 ---- +l1_bad = {"items": [{"title": "A", "spec": "", "accept": ["x"]}]} +err = G.assert_l1(l1_bad["items"]) +check("L1 spec 缺失被拒", err is not None and "spec" in err) +err = G.assert_l1([]) +check("L1 空列表被拒", err is not None) +err = G.assert_l1([{"title": "A", "spec": "s", "accept": []}, {"title": "A", "spec": "s", "accept": ["x"]}]) +check("L1 重名被拒", err is not None and "重复" in err) + +# ---- L1 写入+锁定 ---- +st = st2 +st["plan"]["groups"] = [{"title": "A", "spec": "s", "accept": ["x"], "locked": False, "items": [], "verify": "", "redteam": None, "status": "pending"}] +st["stage"] = "l1-edit" +st3, m = G.cmd_lock_stage(st, A(level=1)) +check("L1 锁定成功→l2-edit", st3 is not None and st3["stage"] == "l2-edit" and st3["plan"]["groups"][0]["locked"]) + +# ---- L2 校验 ---- +l2 = [{"title": "A", "items": [{"title": "a1", "spec": "s", "accept": ["ok"], "do": "做", "verify": "redteam"}]}] +err = G.assert_l2(l2, {"A"}) +check("L2 合法通过", err is None) +err = G.assert_l2([{"title": "A", "items": [{"title": "a1", "spec": "s", "accept": ["ok"], "do": "", "verify": "v"}]}], {"A"}) +check("L2 缺 do 被拒", err is not None and "do" in err) +err = G.assert_l2([{"title": "B", "items": [{"title": "a1", "spec": "s", "accept": ["ok"], "do": "d", "verify": "v"}]}], {"A"}) +check("L2 新增锁定外大类被拒", err is not None and "锁定" in err) +err = G.assert_l2([{"title": "A", "items": []}], {"A"}) +check("L2 空小类被拒", err is not None) + +# ---- L2 写入(直接构造状态)+ 锁定 + 审核 ---- +st3["plan"]["groups"][0]["items"] = [ + {"title": "a1", "concept": "", "spec": "s", "accept": ["ok"], "do": "做", "verify": "redteam", + "mode": "", "status": "pending", "redteam": None, "do_trail": []}, + {"title": "a2", "concept": "", "spec": "s", "accept": ["ok2"], "do": "做2", "verify": "自测", + "mode": "", "status": "pending", "redteam": None, "do_trail": []}] +st4, m = G.cmd_lock_stage(st3, A(level=2)) +check("L2 锁定→review", st4 is not None and st4["stage"] == "review") + +st5, m = G.cmd_review(st4, A(decision="reject", reason="规格太粗")) +check("审核拒绝回 l2-edit", st5 is not None and st5["stage"] == "l2-edit") +st5b, m = G.cmd_review(st5, A(decision="approve")) +check("l2-edit 直接审核被拒(须先重新锁定)", st5b is None) # 上游语义:reject-ack 后必须修订→重锁→再审 +st5c, m = G.cmd_lock_stage(st5, A(level=2)) +check("重新锁定 L2 回 review", st5c is not None and st5c["stage"] == "review") +st6, m = G.cmd_review(st5c, A(decision="approve")) +check("审核通过→develop", st6 is not None and st6["stage"] == "develop") + +# ---- mark 门禁 ---- +st7, m = G.cmd_mark_task(st6, A(level="L2", title="a1", status="completed")) +check("红队未裁不得打卡", st7 is None and "redteam" in m) +st7, m = G.cmd_mark_task(S(), A(level="L2", title="a1", status="completed")) +check("非开发阶段不可打卡", st7 is None and "开发" in m) + +st8, m = G.cmd_redteam(st6, A(level="L2", title="a1", verdict="reject", issues=["缺边界"])) +check("红队 reject 记轮次", st8 is not None and st8["plan"]["groups"][0]["items"][0]["redteam"]["rounds"] == 1 + and not st8["plan"]["groups"][0]["items"][0]["redteam"]["passed"]) +sheet8_open = G.spec_sheet(st8) # cmd_* 原地改 dict:先捕获,避免后续 pass 裁决的别名覆盖 +st9, m = G.cmd_mark_task(st8, A(level="L2", title="a1", status="completed")) +check("reject 后仍不得打卡", st9 is None) +st10, m = G.cmd_redteam(st8, A(level="L2", title="a1", verdict="pass")) +check("红队 pass", st10 is not None and st10["plan"]["groups"][0]["items"][0]["redteam"]["passed"]) +st11, m = G.cmd_mark_task(st10, A(level="L2", title="a1", status="completed")) +check("pass 后可打卡", st11 is not None and st11["plan"]["groups"][0]["items"][0]["status"] == "completed") + +st12, m = G.cmd_redteam(st11, A(level="L2", title="a2", verdict="pass")) +check("非 redteam 项裁决被拒", st12 is None) + +# ---- 单线聚焦 ---- +st13, m = G.cmd_mark_task(st11, A(level="L2", title="a2", status="in_progress")) +check("单线聚焦:a2 进行中", st13 is not None and st13["plan"]["groups"][0]["items"][1]["status"] == "in_progress") +st14, m = G.cmd_mark_task(st13, A(level="L2", title="a1", status="in_progress")) +check("单线聚焦:a1 复位 a2", st14 is not None + and st14["plan"]["groups"][0]["items"][1]["status"] == "pending" + and st14["plan"]["groups"][0]["items"][0]["status"] == "in_progress") + +# ---- L1 标定门 ---- +st15, m = G.cmd_mark_task(st14, A(level="L1", title="A", status="completed")) +check("小类未全完不得标大类", st15 is None and "未完成小类" in m) + +# ---- revise_do 轨迹 ---- +st16, m = G.cmd_revise_do(st14, A(title="a1", do="改用并行")) +check("revise_do 登记", st16 is not None and len(st16["plan"]["groups"][0]["items"][0]["do_trail"]) == 1 + and st16["plan"]["groups"][0]["items"][0]["do"] == "改用并行") +st17, m = G.cmd_revise_do(S(), A(title="a1", do="x")) +check("非开发期 revise_do 拒", st17 is None) + +# ---- 全完→final ---- +st18, m = G.cmd_mark_task(st16, A(level="L2", title="a1", status="completed")) +st19, m = G.cmd_mark_task(st18, A(level="L2", title="a2", status="completed")) +check("全部完成→final", st19 is not None and st19["stage"] == "final") +st20, m = G.cmd_mark_task(st19, A(level="L1", title="A", status="completed")) +check("final 后 L1 标定仍可用", st20 is not None and st20["plan"]["groups"][0]["locked"]) + +# ---- 星象只读 ---- +st21, m = G.cmd_commit_star(st20, A(purpose="另一个全新的十二字以上的目的宣言声明")) +check("final 阶段星象只读", st21 is None and "只读" in m) + +# ---- 状态不变验证(拒绝路径不动状态)---- +before = json.dumps(st6, sort_keys=True, ensure_ascii=False) +G.cmd_mark_task(st6, A(level="L2", title="a1", status="completed")) # 拒 +after = json.dumps(st6, sort_keys=True, ensure_ascii=False) +check("拒绝=状态不变", before == after) + +# ---- spec_sheet 渲染 ---- +sheet = G.spec_sheet(st19) +check("规格单含状态与北极星", "completed" in sheet and "★" in sheet) +check("红队未决时规格单亮灯", "🔴" in sheet8_open) # reject 后未再裁的快照 + +print("\n%d passed, %d failed" % (PASS, FAIL)) +sys.exit(1 if FAIL else 0) diff --git a/marketplace.json b/marketplace.json index b6152c39..9470447d 100644 --- a/marketplace.json +++ b/marketplace.json @@ -790,7 +790,9 @@ }, { "name": "blender", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "Blender", "displayName_i18n": { "en": "Blender", @@ -811,7 +813,9 @@ }, { "name": "excalidraw", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "Excalidraw", "displayName_i18n": { "en": "Excalidraw", @@ -832,7 +836,9 @@ }, { "name": "openpencil", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "OpenPencil", "displayName_i18n": { "en": "OpenPencil", @@ -855,7 +861,9 @@ "name": "photocraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/photocraft/icon.png", "source": "./plugins/photocraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "PhotoCraft", "displayName_i18n": { "en": "PhotoCraft", @@ -882,7 +890,9 @@ "name": "vectorcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/vectorcraft/icon.png", "source": "./plugins/vectorcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "VectorCraft", "displayName_i18n": { "en": "VectorCraft", @@ -909,7 +919,9 @@ "name": "pdfcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/pdfcraft/icon.png", "source": "./plugins/pdfcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "PdfCraft", "displayName_i18n": { "en": "PdfCraft", @@ -936,7 +948,9 @@ "name": "designcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/designcraft/icon.png", "source": "./plugins/designcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "DesignCraft", "displayName_i18n": { "en": "DesignCraft", @@ -963,7 +977,9 @@ "name": "wordcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/wordcraft/icon.png", "source": "./plugins/wordcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "WordCraft", "displayName_i18n": { "en": "WordCraft", @@ -990,7 +1006,9 @@ "name": "gridcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/gridcraft/icon.png", "source": "./plugins/gridcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "GridCraft", "displayName_i18n": { "en": "GridCraft", @@ -1017,7 +1035,9 @@ "name": "deckcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/deckcraft/icon.png", "source": "./plugins/deckcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "DeckCraft", "displayName_i18n": { "en": "DeckCraft", @@ -1044,7 +1064,9 @@ "name": "cadcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/cadcraft/icon.png", "source": "./plugins/cadcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "CADCraft", "displayName_i18n": { "en": "CADCraft", @@ -1071,7 +1093,9 @@ "name": "soundcraft", "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/soundcraft/icon.png", "source": "./plugins/soundcraft", - "componentTypes": ["ui"], + "componentTypes": [ + "ui" + ], "displayName": "SoundCraft", "displayName_i18n": { "en": "SoundCraft", @@ -1138,6 +1162,34 @@ "生成一个可 3D 打印的骑士小雕像,导出 STL 文件" ] } + }, + { + "name": "glm-routing-suite", + "source": "./external_plugins/glm-routing-suite", + "displayName": "GLM Routing Suite", + "displayName_i18n": { + "en": "GLM Routing Suite", + "zh-CN": "GLM 思维路由套件" + }, + "description": "Four-stage workflow + delivery gate + graded task protocol with a soft stage gate and variant presets, ported from dsh-routing-suite (MIT/Apache-2.0): default zero-injection, completion-signal advancement, delivery_check evidence manifest, two-tier spec gating with red-team gates, PreToolUse write/edit gating, spec/react style presets", + "description_i18n": { + "en": "Four-stage workflow + delivery gate + graded task protocol with a soft stage gate and variant presets, ported from dsh-routing-suite (MIT/Apache-2.0): default zero-injection, completion-signal advancement, delivery_check evidence manifest, two-tier spec gating with red-team gates, PreToolUse write/edit gating, spec/react style presets", + "zh-CN": "移植自 dsh-routing-suite 的四阶段工作流+交付门禁+分级任务协议+软阶段门禁+变体预设:默认零注入、完成信号晋级、delivery_check 证据清单、两级规格门控含红队门、规格阶段 PreToolUse 扣住写入、spec/react 风格档" + }, + "version": "0.5.0", + "author": { + "name": "forrestneo" + }, + "category": "developer-tools", + "keywords": [ + "routing", + "reasoning-mode", + "glm", + "workflow", + "delivery-gate", + "graded", + "red-team" + ] } ] }