Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "workflow",
"version": "0.6.0",
"version": "0.6.1",
"description": "让 AI Agent 直连 Workflow(workflow.games):规划需求与需求室、建需求、记 bug、查任务、拿单执行与证据回写、流转状态、线上 QA 验收、查文档、经确认后向平台方匿名反馈问题与建议",
"author": {
"name": "Workflow",
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## [0.6.1]

修正 `workflow-update` 的渠道误判:插件有两条分发渠道,版本真值不是同一个——宿主托管安装(Claude Code marketplace)认公开仓的 `plugin.json`,手动安装(Codex / 官网脚本)认官网 `version.json`。旧正文让所有形态先读 `version.json` 再分流,官网清单一旦滞后于 marketplace 发布,宿主托管用户就会拿到「已是最新」的反向结论、永远升不上去(2026-08-29 实测:官网停在 0.3.0,marketplace 已发 0.6.0)。

### 修复

- **`workflow-update` 改为「先判安装形态,再查版本」。** 分流升为第 1 节,且三条判定按「源码态 → 宿主托管 → 手动安装」定序、先命中先算——源码态必须最先判(否则插件仓库自己的工作副本会被「向上两级有 plugin.json」误判成宿主托管),手动安装的路径特征优先于宿主托管的清单特征;宿主托管(第 2 节)明确**不读 `version.json`**,改走宿主自己的机制(`claude plugin marketplace update` + `claude plugin update`,并提醒更新后需重启会话),版本真值指向 marketplace 公开仓的 `plugin.json`;手动安装(第 3 节)保留原有的 `cb` 绕缓存、语义化逐段比较与「线上 < 本地 绝不更新」降级红线。安全边界补一条:宿主托管形态不下载任何文件。
- **`/workflow:update` 命令描述同步新次序**,避免命令入口与技能正文两个口径。
- **README 中英更新表补上 Claude Code 的两条 CLI 命令与重启提醒**——原表只给了 `/plugin` 界面与 autoUpdate,非交互终端里两者都用不了。

### 测试

- **新增 `tests/workflow-update-contract.test.mjs`(16 项)**:分流段落必须先于抓取 `version.json?cb=` 的**次序断言**(本次回归的根因守卫)、开篇必须声明两条渠道版本真值不同、两个分支的路径判定特征、宿主托管分支的「不读 version.json + 说明滞后原因 + 宿主命令 + 重启提醒 + 不自改插件目录」、手动安装分支的 `cb` 与三分支比较与降级红线、安全边界四条、命令入口与技能同次序。

## [0.6.0]

新增面向平台方的匿名反馈通道,并与「记 bug 进自己项目」彻底分流。全部口径按线上合同(`createSupportTicket` / `getSupportConfig`)与官方指南逐条核实:只收集用户主动提供的信息、发送前逐字确认、公开匿名端点不碰凭证、202 回执如实说明不是正式单。反馈范围不限于报错——体验不佳、加载或操作卡慢、缺失功能、产品建议同样可报。
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ surfaces = ["web"]

| Install method | How to update |
| :-- | :-- |
| Claude Code (marketplace) | Supports autoUpdate, or update manually from the `/plugin` UI |
| Claude Code (marketplace) | `claude plugin marketplace update workflow-plugin` + `claude plugin update workflow@workflow-plugin --scope user` (restart the session afterwards); autoUpdate and the `/plugin` UI also work |
| Agent Plugins client | Re-run `npx plugins add Go1c/workflow-plugin` |
| Codex / manual | Tell the agent "update the workflow plugin" — checks the published version, verifies sha256 per file, backs up the old version, installs |

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ surfaces = ["web"]

| 安装方式 | 怎么更新 |
| :-- | :-- |
| Claude Code(marketplace) | 支持 autoUpdate 自动升级,也可在 `/plugin` 界面手动更新 |
| Claude Code(marketplace) | `claude plugin marketplace update workflow-plugin` + `claude plugin update workflow@workflow-plugin --scope user`(更新后需重启会话);也支持 autoUpdate 自动升级或在 `/plugin` 界面手动更新 |
| Agent Plugins 客户端 | 重跑一次 `npx plugins add Go1c/workflow-plugin` |
| Codex / 手动安装 | 对 Agent 说「更新 workflow 插件」—— 查线上版本 → 逐文件校验 sha256 → 备份旧版 → 就位 |

Expand Down
2 changes: 1 addition & 1 deletion commands/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
description: 检查并更新 Workflow Agent 插件到最新版本
---

强制调起 **workflow-update** 技能,严格按其正文流程执行:读本地 VERSION → 带 cb 参数查线上 version.json → 按宿主分流决定提示更新还是自更新
强制调起 **workflow-update** 技能,严格按其正文流程执行:**先判安装形态**(宿主托管 / 手动安装)→ 宿主托管交给宿主自己的更新机制、不读官网 version.json;手动安装才读本地 VERSION 并带 cb 参数比对线上 version.json,必要时走 sha256 校验的自更新
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "workflow-plugin",
"version": "0.6.0",
"version": "0.6.1",
"private": true,
"type": "module",
"description": "Agent plugin for Workflow (workflow.games) — 技能包与契约测试",
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "workflow",
"version": "0.6.0",
"version": "0.6.1",
"description": "让 AI Agent 直连 Workflow(workflow.games):规划需求与需求室、建需求、记 bug、查任务、拿单执行与证据回写、流转状态、线上 QA 验收、查文档、经确认后向平台方匿名反馈问题与建议",
"author": {
"name": "Workflow",
Expand Down
49 changes: 35 additions & 14 deletions skills/workflow-update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,44 @@ description: 更新或检查 Workflow(workflow.games)Agent 插件版本时

# workflow-update — 检查与更新插件

## 1. 读本地版本
**先判安装形态,再查版本。** 插件有两条分发渠道,**版本真值不是同一个**:宿主托管安装(Claude Code marketplace)认公开仓的 `plugin.json`,手动安装(Codex / 官网脚本)认官网 `version.json`。两条渠道的发布节奏可以脱节,拿另一条渠道的版本号判自己,会得出「已是最新」甚至反向降级的错误结论。

## 1. 判断安装形态

看**本技能所在路径**,按下面的顺序判,**先命中先算**:

**0. 源码态** —— 向上两级既是插件根、又带 `.git` 或 `tests/`(你在开发这个插件本身,不是在用它)→ **提示无需更新,结束**。源码态归 git 管,不归本技能;这一条必须先判,否则插件仓库自己的工作副本会被下面的清单特征误判成宿主托管。

**A. 宿主托管安装** —— 技能路径在 `~/.claude/plugins/cache` 或其他客户端的插件缓存下 → 走**第 2 节**。
没有缓存路径特征、但向上两级是插件根(存在 `plugin.json` 或 `.claude-plugin/plugin.json`),且**不在 B 列出的手动安装目录里**,同样按 A 处理。

**B. 手动安装** —— 技能目录直接落在 `~/.codex/skills`、`.agents/skills` 或 `~/.claude/skills` 下 → 走**第 3 节**。**B 的路径特征优先于 A 的清单特征**:项目里恰好放着一份 `plugin.json`,不改变这是手动安装的事实。

## 2. 宿主托管:交给宿主更新

**这条路不读 `version.json`。** 那份清单只服务手动安装渠道,可能**滞后**于 marketplace 发布;宿主托管的版本真值是 marketplace 公开仓里的 `plugin.json`,由宿主自己拉取比对。用官网清单判宿主托管安装,最常见的结果是误报「已是最新」,用户永远升不上去。

**本技能不自改插件目录**——宿主管理的目录由宿主维护,绕过它手改会造成状态不一致。改为提示用户走宿主自己的机制:

- **Claude Code**:先刷新 marketplace,再更新插件。

```
claude plugin marketplace update workflow-plugin
claude plugin update workflow@workflow-plugin --scope user
```

也可以用 `/plugin` 界面,或等 marketplace autoUpdate 自己生效。**更新后必须重启会话**才加载新版本——不提醒的话用户会以为没升成功。

- **其他 Agent Plugins 客户端**:用各自的安装器重装(例如 `npx plugins add` 那条路径)。

收尾读 `claude plugin list`(或宿主对应的列表命令)确认版本已变,向用户报告新旧版本号。

## 3. 手动安装:比对官网版本

读**本技能目录下的 `VERSION` 文件**(安装包内由构建器生成,纯文本一行版本号)。

- 没有 `VERSION` 文件 = 你运行的是源码态(开发仓里直接用),**提示无需更新**,结束。

## 2. 查线上版本

抓取:

```
Expand All @@ -25,17 +55,7 @@ https://workflow.games/plugin/version.json?cb=<当前 epoch 秒>

- 线上 **==** 本地 → 报告「已是最新(<版本>)」,结束。
- 线上 **<** 本地 → 本地更新(多半是源码态或线上发布滞后)。报告两个版本号并**结束,绝不"更新"**——照旧逻辑跑会把新版覆盖成旧版,是降级不是升级。
- 线上 **>** 本地 → 才进入第 3 节。

## 3. 宿主分流

看本技能所在路径判断安装方式:

**A. 宿主托管安装**(路径在 `~/.claude/plugins/cache` 或其他客户端的插件缓存下,或从本技能目录向上两级即为插件根、其中存在 `plugin.json` 或 `.claude-plugin/plugin.json`):

提示用户按宿主自己的方式更新——Claude Code 用 `/plugin` 界面或等 marketplace autoUpdate;其他 Agent Plugins 客户端用各自的安装器(如 `npx plugins add` 重装)。**本技能不自改插件目录**——宿主管理的目录由宿主维护,绕过它手改会造成状态不一致。

**B. 手动安装**(技能目录直接落在 `~/.codex/skills`、`.agents/skills` 或 `~/.claude/skills` 下,同级没有插件清单)→ 自更新,按第 4 节。
- 线上 **>** 本地 → 才进入第 4 节。

## 4. 自更新流程(仅手动安装)

Expand All @@ -49,4 +69,5 @@ https://workflow.games/plugin/version.json?cb=<当前 epoch 秒>

- **只从 `workflow.games` 域下载**。清单里出现任何其他域的地址 → 中止并告警。
- 技能包只应包含 **`.md` 与 `VERSION` 纯文本**。清单或下载内容里发现可执行文件(`.sh`、二进制等)→ **立即中止并告警**,不安装。
- 宿主托管形态(第 2 节)**不下载任何文件**——它只调用宿主自己的命令,下载与落盘都由宿主负责。
- 更新**绝不触碰** `~/.config/workflow/config.toml`——凭证与插件更新无关。
2 changes: 1 addition & 1 deletion skills/workflow-update/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.0
0.6.1
160 changes: 160 additions & 0 deletions tests/workflow-update-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// workflow-update 的提示词契约。
//
// 存在的理由:这个技能的失败模式不是「答得不好」,而是**拿错渠道的版本号判自己**。
// 插件有两条分发渠道,版本真值不是同一个:宿主托管(Claude Code marketplace)看公开仓的
// plugin.json,手动安装(Codex / 官网脚本)看官网 version.json。两条渠道的发布节奏可以脱节,
// 2026-08-29 实测官网 version.json 停在 0.3.0 而 marketplace 已发 0.6.0——此时若让宿主托管
// 安装去读 version.json,会得出「线上 0.3.0 < 本地 0.5.0,无需更新」的反向结论,用户永远升不上去。
//
// 因此本文件锁死一条结构性纪律:**先判安装形态,再查版本**;且宿主托管分支明确不读 version.json。
// 次序断言(分流段落必须出现在 version.json 之前)是这里的核心,不是风格检查。

import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");

function read(relativePath) {
const absolute = join(repoRoot, relativePath);
assert.ok(existsSync(absolute), `缺少 ${relativePath}`);
return readFileSync(absolute, "utf8");
}

const skill = read("skills/workflow-update/SKILL.md");
const command = read("commands/update.md");

describe("分流先于查版本", () => {
test("安装形态分流段落出现在抓取 version.json 之前", () => {
// 量的是**抓取动作**(version.json?cb=)而不是任何一次提及:开篇点名两条渠道的版本真值
// 恰恰是这次修复的重点,不该被自己的测试判违规。真正的红线是「先分流,后抓」。
const routing = skill.search(/##\s*1\.\s*判断安装形态/);
const probe = skill.indexOf("version.json?cb=");
assert.ok(routing !== -1, "SKILL.md 缺少「## 1. 判断安装形态」——分流必须是第一节");
assert.ok(probe !== -1, "SKILL.md 不再抓 version.json?cb=?手动安装渠道的探针不能删");
assert.ok(
routing < probe,
`分流段落(${routing})必须出现在抓取 version.json(${probe})之前,否则宿主托管安装会先被官网渠道的版本号误导`,
);
});

test("开篇即声明两条渠道版本真值不同", () => {
const opening = skill.slice(0, skill.search(/##\s*1\./));
assert.match(opening, /先判安装形态/, "开篇必须先立「先判形态」的规矩");
assert.match(opening, /版本真值/, "开篇必须点明两条渠道的版本真值不同");
});

test("源码态最先判,且排在 A / B 之前", () => {
// 插件仓库自己的工作副本:技能目录向上两级就是仓库根,那里有 plugin.json 与
// .claude-plugin/plugin.json。不先判源码态的话,开发者会被指去跑宿主的 update 命令。
const routing = skill.slice(skill.search(/##\s*1\./), skill.search(/##\s*2\./));
assert.match(routing, /先命中先算/, "三条判定必须声明先后,不能并列");
const source = routing.search(/\*\*0\.\s*源码态\*\*/);
const hosted = routing.search(/\*\*A\.\s*宿主托管安装\*\*/);
const manual = routing.search(/\*\*B\.\s*手动安装\*\*/);
assert.ok(source !== -1, "缺少源码态判定");
assert.ok(source < hosted && hosted < manual, "顺序必须是 源码态 → 宿主托管 → 手动安装");
assert.match(routing, /\.git|tests\//, "源码态要给出可判定的特征");
});

test("手动安装的路径特征优先于宿主托管的清单特征", () => {
// 项目里恰好放一份 plugin.json,不该把 ~/.codex/skills 下的手动安装改判成宿主托管。
const routing = skill.slice(skill.search(/##\s*1\./), skill.search(/##\s*2\./));
assert.match(routing, /B 的路径特征优先于 A 的清单特征/);
assert.match(routing, /不在 B 列出的手动安装目录里/, "A 的清单特征必须显式让位给 B");
});

test("两个分支都给出可判定的路径特征", () => {
assert.match(skill, /~\/\.claude\/plugins\/cache/, "宿主托管的判定特征丢了");
assert.match(skill, /\.claude-plugin\/plugin\.json/, "插件清单判定特征丢了");
assert.match(skill, /~\/\.codex\/skills/, "手动安装的判定特征丢了");
});
});

describe("宿主托管分支", () => {
const hosted = skill.slice(skill.search(/##\s*2\./), skill.search(/##\s*3\./));

test("明确不读 version.json,并说明为什么", () => {
assert.match(hosted, /不读\s*`?version\.json`?/, "必须显式写「不读 version.json」");
assert.match(hosted, /滞后/, "必须说明官网清单可能滞后于 marketplace 发布,否则下次还会有人接回去");
});

test("版本真值指向 marketplace 清单而非官网探针", () => {
assert.match(hosted, /marketplace/i);
assert.match(hosted, /plugin\.json/);
});

test("给出宿主自己的更新机制与重启提醒", () => {
assert.match(hosted, /claude plugin marketplace update/, "缺少刷新 marketplace 的命令");
assert.match(hosted, /claude plugin update/, "缺少更新插件的命令");
assert.match(hosted, /重启/, "宿主更新后需重启才生效,不写用户会以为没升级成功");
});

test("不自改宿主管理的插件目录", () => {
assert.match(hosted, /不自改插件目录/);
});
});

describe("手动安装分支", () => {
const manual = skill.slice(skill.search(/##\s*3\./), skill.search(/##\s*4\./));

test("读本地 VERSION,源码态直接结束", () => {
assert.match(manual, /`VERSION`/);
assert.match(manual, /源码态/);
});

test("查线上版本必须带 cb 参数绕缓存", () => {
assert.match(manual, /version\.json\?cb=/);
assert.match(manual, /必须带\s*`?cb`?\s*参数/);
});

test("语义化逐段比较,三个分支齐全", () => {
assert.match(manual, /语义化版本逐段比大小/);
assert.match(manual, /线上\s*\*\*==\*\*\s*本地/);
assert.match(manual, /线上\s*\*\*<\*\*\s*本地/);
assert.match(manual, /线上\s*\*\*>\*\*\s*本地/);
});

test("线上更旧时绝不更新——降级红线仍在", () => {
assert.match(manual, /绝不("|「|")?更新/, "「线上 < 本地 绝不更新」这条降级红线不得删除");
assert.match(manual, /降级不是升级/);
});
});

describe("安全边界不得放宽", () => {
const safety = skill.slice(skill.search(/##\s*安全边界/));

test("只从 workflow.games 域下载", () => {
assert.match(safety, /只从\s*`?workflow\.games`?\s*域下载/);
});

test("技能包只允许 Markdown 与 VERSION,遇可执行文件中止", () => {
assert.match(safety, /`\.md`\s*与\s*`VERSION`/);
assert.match(safety, /立即中止并告警/);
});

test("不触碰凭证文件", () => {
assert.match(safety, /config\.toml/);
assert.match(safety, /绝不触碰/);
});

test("宿主托管形态不下载任何文件", () => {
assert.match(safety, /不下载任何文件/, "宿主托管只调宿主命令,这条边界要写明");
});
});

describe("命令入口与技能同口径", () => {
test("update 命令描述的次序是「先分流、后查版本」", () => {
const routing = command.search(/分流|安装形态/);
const probe = command.indexOf("version.json");
assert.ok(routing !== -1, "commands/update.md 必须提到分流");
if (probe !== -1) {
assert.ok(
routing < probe,
"commands/update.md 里分流必须写在 version.json 之前——命令与技能次序不一致会让 Agent 按旧序执行",
);
}
});
});
Loading