tianshu-mcp 0.5.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +60 -0
- package/CHANGELOG.md +60 -0
- package/README.en.md +55 -6
- package/README.md +55 -6
- package/dist/config/schema.js +55 -1
- package/dist/index.js +6 -2
- package/dist/mcp/formatter.js +1 -0
- package/dist/mcp/handlers.js +478 -84
- package/dist/mcp/tools.js +2 -2
- package/dist/server.js +11 -7
- package/dist/tasks/idempotency.js +274 -0
- package/dist/tasks/task-manager.js +25 -1
- package/dist/tasks/task-store.js +10 -0
- package/dist/util/skill-install.js +362 -70
- package/dist/version.generated.js +1 -1
- package/package.json +2 -1
- package/skills/tianshu-mcp/SKILL.md +6 -1
- package/skills/tianshu-mcp/usage-examples.md +22 -2
package/CHANGELOG.en.md
CHANGED
|
@@ -8,6 +8,66 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [0.6.0] - 2026-09-23
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Tri-state and approval entry point for skill self-install** ([issue #16](https://github.com/lanlan0811/tianshu-mcp/issues/16)):
|
|
16
|
+
- `skills.autoInstall` is upgraded from a boolean to `true | "prompt" | false` (existing `true`/`false` stay valid, no migration needed). `"prompt"` means **install as usual on first run, but do not auto-overwrite when a change is needed** (warn and record `pendingUpdate` in the manifest) — a stdio server has no synchronous interaction channel, so "prompt" effectively means "do not act automatically, leave it for confirmation".
|
|
17
|
+
- New startup flag `--approve-skill-update` (equivalent env var `TIANSHU_MCP_APPROVE_SKILL_UPDATE=1`): for this run, permit a "needs change" skill directory to be overwritten with the in-package version (after backup). **It has no effect on directories confirmed to carry local edits**; `--no-skill-install` / `autoInstall:false` outrank it.
|
|
18
|
+
- New `skills.backupKeep` (default 3, range 0..50, `0` = never prune): after a successful overwrite, keep the newest N `.bak-<timestamp>` directories by timestamp and log the deletions.
|
|
19
|
+
- Install manifest `<dest>/.tianshu-mcp-install.json` (`schema`/`name`/`packageVersion`/`contentHash`/`installedAt`/`sourceDir`, plus `pendingUpdate` when held), which distinguishes "an untouched stale package copy" from "your local edits" — previously indistinguishable in principle.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Skill content is no longer discovered from the current working directory** (issue #16 problem 1): `resolveSkillSourceDir()` now locates the source relative to the package via `import.meta.url` only, and **both `process.cwd()` candidates were removed** along with a loose candidate that could never match; when no source is found, the existing "skip with a warning" path is kept. Previously, debugging the server inside a third-party repository that carried its own `skills/tianshu-mcp/` would install that repository's content into `~/.rivet/skills/` for the next session.
|
|
24
|
+
- **An overwrite no longer silently replaces your local edits** (issue #16 problem 2): when the target content differs from the manifest record (i.e. you edited it) or the source is unknown (no valid manifest), the existing content is **kept with a warning** (plus two remediation paths: rename/delete and restart, or merge manually); an overwrite happens only when the content is provably untouched or explicitly approved.
|
|
25
|
+
- **Install can no longer leave a half-copied tree**: it now follows an atomic "copy into `<dest>.incoming-<ts>-<hex>` (manifest included) → back the old directory up as `<dest>.bak-<ts>` → swap in" path, rolling back and cleaning the tmp tree on failure; stale `.incoming-*` directories older than one hour are cleaned on startup. The previous "rename the old directory away, then copy straight into the target" had a crash window that could leave a half-copied directory, which the new semantics would misread as local edits and block upgrades on permanently.
|
|
26
|
+
- **Log levels for overwrites corrected**: stale-copy upgrade / local edits kept / unknown source / install failure are now all `WARN` (previously backup and install were `INFO`, easily drowned out); skip and manifest repair stay `INFO`. Hashes are printed as the first 8 characters only, with the full value in the manifest.
|
|
27
|
+
|
|
28
|
+
### Tests
|
|
29
|
+
|
|
30
|
+
- New unit file `test/unit/skill-install.test.ts` (30 cases): three source-location checks (a "decoy with the same name under cwd does not change the source" regression plus a "the source no longer contains `process.cwd()`" textual assertion), hash exclusion rules (the manifest itself and `.DS_Store`/`Thumbs.db`/`desktop.ini`/`._*`/`.git*`), manifest parsing (missing / malformed JSON / invalid schema·name·hash → corrupt), a table-driven sweep of the 6-state decision matrix across modes and approval, real-filesystem end-to-end (first install / idempotency / manifest self-heal / trusted-stale overwrite / local edits kept with no backup / `prompt` hold and `pendingUpdate` / approved overwrite / failure rollback / backup pruning / non-matching entries untouched / stale tmp cleanup / log levels / missing source), and a smoke test using the real `skills/tianshu-mcp`.
|
|
31
|
+
- `test/unit/config-hotreload.test.ts` gains default/override/invalid-value last-known-good cases for `skills.autoInstall` (tri-state) and `skills.backupKeep`.
|
|
32
|
+
- The strict stdio gate gains two independent scenarios (6→8): `skill-locally-modified` (seed a "locally modified" directory → assert no overwrite, no backup, no install log) and `skill-approve-update` (seed an "unknown source" directory plus the flag → assert an overwrite to the in-package version, a backup created, and a manifest written); new dev-only fixture `scripts/seed-skill-state.mjs` (registered as `npm run seed:skill-state`, excluded from the npm package).
|
|
33
|
+
- Full run **898 passed / 12 skipped** (Windows 10 x64, Node 24.18.0), 33 more cases than v0.5.10.
|
|
34
|
+
|
|
35
|
+
### Docs
|
|
36
|
+
|
|
37
|
+
- New `docs/release-v0.6.0.md` / `.en.md` and `docs/issue-16-skill-install-hardening-record.md` (the raw Windows 10 real-machine verification R1–R7 and uncovered items).
|
|
38
|
+
- The bilingual README gains a "Skill self-install" section (source location, manifest and the three verdict classes, the `autoInstall` tri-state table, approval/disable flags) plus the M30 milestone; the bilingual ARCHITECTURE gains §3.4 "Trust and decision model of skill self-install" (decision matrix, atomicity, log levels, backup governance) and lists the skill boundary among the hard red lines; the bilingual SECURITY gains "Supply-chain boundary of skill self-install"; `docs/agent-profiles.md` / `.en.md` update the `skills` config notes; HANDOFF carries the new version snapshot and file table.
|
|
39
|
+
|
|
40
|
+
## [0.5.10] - 2026-09-23
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Idempotency keys (`idempotencyKey`) for `run_task` / `verify_task`** ([issue #15](https://github.com/lanlan0811/tianshu-mcp/issues/15)): a host retry after a `tools/call` timeout no longer becomes a duplicate dispatch or a duplicate verification run.
|
|
45
|
+
- `run_task`: resubmitting the same key with the same arguments within the TTL (24 h default) **always returns the original `taskId` and its current meta** (terminal tasks included — read-only, never re-dispatched); the same key with different arguments fails closed, reporting the original `taskId`.
|
|
46
|
+
- `verify_task`: a same-key verification that is **still running** returns a success result with `idempotencyReplay: "in_progress"` (deliberately not `isError`, so a host does not retry harder); one that has **already finished** returns the existing report paths plus that round's `reportRound` / verdict and **re-runs nothing**.
|
|
47
|
+
- The two tools use independent namespaces; keys are 1..128 characters after trimming with no control characters (protocol-level validation, rejected as `参数不合法`).
|
|
48
|
+
- New data file `<data-dir>/idempotency.json` (atomic writes, lazy loading, TTL + capacity pruning) so retries are still recognised after a restart; `TaskMeta` gains `idempotencyKey` / `idempotencyScope` / `idempotencyDigest` for auditing and for rebuilding the mapping when the file is corrupt.
|
|
49
|
+
- New `config.json` keys: `idempotency.ttlMs` (default `86400000`) and `idempotency.maxEntries` (default `2000`, oldest first by `createdAt` when exceeded).
|
|
50
|
+
- The meta block gains `idempotencyKey`, `idempotencyReplay` (`hit` / `in_progress`) and `projectActiveTask`; `tools/list` reports `annotations.idempotentHint: true` for the two tools (**only when the caller supplies `idempotencyKey`**, as stated in the tool descriptions and the skill document).
|
|
51
|
+
- **Duplicate-dispatch fallback**: even without a key, `run_task` names the unfinished task in the same workspace (active statuses plus `needs_user`) and suggests using an idempotency key or checking `query_task` first.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- On an idempotency hit an audit `note` is appended to the original task's event stream (`幂等重放:keyDigest=…`); the key **plaintext never enters logs or the event stream** (only the first 8 hex characters of its sha256).
|
|
56
|
+
- The standalone `verify_task` record id changes from `vfy_<Date.now()>` to the existing `genVerifyId()` (`vfy_<timestamp>_<6 random chars>`, previously without any caller), so same-millisecond collisions are no longer possible.
|
|
57
|
+
- **Crash window**: the idempotency path writes the mapping **before** creating the task inside one critical section (`runExclusive` serialises same-key concurrency); if the process dies in between, a retry sees a mapping with no task snapshot, treats it as not yet effective and re-dispatches — two agent queues are never left behind.
|
|
58
|
+
- **Fail-open on write failure**: a failed mapping write still returns the dispatched task and states in the response and meta that "the idempotency record could not be written, this task cannot be replayed by the same key" — a running agent is never reported as a failed dispatch.
|
|
59
|
+
|
|
60
|
+
### Tests
|
|
61
|
+
|
|
62
|
+
- New unit cases `test/unit/idempotency.test.ts` (16): key validation, `canonicalDigest` stable serialisation, TTL expiry, capacity eviction, cross-instance recovery, corrupt-file rebuild, `runExclusive` serialisation and failure isolation, in-flight markers, fail-open on write failure.
|
|
63
|
+
- New integration cases `test/integration/idempotency.test.ts` (8): same-key replay creating a single `tsk_*` directory, same-key/different-arguments fail-closed, faithful replay of terminal tasks, zero regression without a key, the `projectActiveTask` hint, finished standalone replay adding no report, **in-progress replay returning a success result**, `taskId`-mode replay, and **cross-restart** dispatch and verification replay.
|
|
64
|
+
- Protocol cases gained `idempotentHint` assertions (true for the two tools only); `config-hotreload` gained `idempotency.ttlMs` / `maxEntries` defaults and override coverage.
|
|
65
|
+
- Type check, lint (`--max-warnings 0`), the full test suite, the build, the strict stdio check (6/6) and `pack:check` all pass.
|
|
66
|
+
|
|
67
|
+
### Docs
|
|
68
|
+
|
|
69
|
+
- New `docs/release-v0.5.10.md` / `.en.md`; the bilingual README gained an idempotent-retry entry, tool-table and milestone updates; ARCHITECTURE gained the data file, config keys and annotation notes; `skills/tianshu-mcp/` gained the parameter quick-reference, meta fields, error codes and discipline notes; HANDOFF carries the version snapshot.
|
|
70
|
+
|
|
11
71
|
## [0.5.9] - 2026-09-23
|
|
12
72
|
|
|
13
73
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,66 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- **技能自检安装的三态与放行入口**([issue #16](https://github.com/lanlan0811/tianshu-mcp/issues/16)):
|
|
15
|
+
- `skills.autoInstall` 由布尔升级为 `true | "prompt" | false`(既有 `true`/`false` 继续合法,无需迁移)。`"prompt"` 语义为**首次安装照常、需变更时不自动覆盖**(只告警并在清单记 `pendingUpdate`)——stdio server 无同步交互通道,故「prompt」实为「不自动 + 留待确认」。
|
|
16
|
+
- 新增启动参数 `--approve-skill-update`(等价环境变量 `TIANSHU_MCP_APPROVE_SKILL_UPDATE=1`):本次启动允许把「需变更」的技能目录覆盖为包内版本(先备份)。**对已确证含用户本地修改的目录不生效**;`--no-skill-install` / `autoInstall:false` 的否决权高于本参数。
|
|
17
|
+
- 新增 `skills.backupKeep`(默认 3,范围 0..50,`0` = 不清理):覆盖成功后按时间戳保留最新 N 个 `.bak-<时间戳>` 目录,删除项记日志。
|
|
18
|
+
- 安装清单 `<目标>/.tianshu-mcp-install.json`(`schema`/`name`/`packageVersion`/`contentHash`/`installedAt`/`sourceDir`,保留时含 `pendingUpdate`),用于区分「未改动的旧版包副本」与「用户本地修改」——此前两者在原理上不可区分。
|
|
19
|
+
|
|
20
|
+
### 修复
|
|
21
|
+
|
|
22
|
+
- **技能源不再从当前工作目录发现内容**(issue #16 问题 1):`resolveSkillSourceDir()` 只由 `import.meta.url` 相对包自身定位,**删除两处 `process.cwd()` 候选**及一条永不命中的宽松候选;找不到源时沿用既有「跳过安装 + 告警」路径。此前在某个自带 `skills/tianshu-mcp/` 的第三方仓库目录里调试起 server,会把该仓库内容安装进 `~/.rivet/skills/` 并在新会话生效。
|
|
23
|
+
- **技能覆盖不再静默替换用户本地修改**(issue #16 问题 2):检测到目标内容与清单记录不符(= 用户改过)或来源不明(无有效清单)时**保留现有内容并告警**(附「改名/删除后重启」与「手工合并」两条处置指引),仅在内容可证未被改动或显式放行时才覆盖。
|
|
24
|
+
- **安装不再可能留下半成品**:改为「拷贝到 `<目标>.incoming-<ts>-<hex>`(含写清单)→ 旧目录备份为 `<目标>.bak-<ts>` → 换入」的原子路径,失败回滚并清理临时目录;启动时清理 mtime 早于 1 小时的 `.incoming-*` 崩溃残留。此前「先 rename 旧目录、再直接往目标拷贝」的崩溃窗口会留下半拷贝目录,在新语义下会被误判为「用户修改」而永久阻塞升级。
|
|
25
|
+
- **覆盖相关日志分级修正**:旧版覆盖 / 保留用户修改 / 来源不明 / 安装失败一律 `WARN`(此前备份与安装均为 `INFO`,容易被日志淹没);跳过与清单补写保持 `INFO`。hash 只打印前 8 位,完整值落在清单文件。
|
|
26
|
+
|
|
27
|
+
### 测试
|
|
28
|
+
|
|
29
|
+
- 新增单元用例 `test/unit/skill-install.test.ts`(30 项):源定位三态(含「cwd 下同名诱饵不影响源」回归与「源码不再出现 `process.cwd()`」文本断言)、hash 排除规则(清单自身与 `.DS_Store`/`Thumbs.db`/`desktop.ini`/`._*`/`.git*`)、清单解析(缺失/非法 JSON/schema·name·hash 不合法 → corrupt)、判定矩阵 6 态 × 模式 × 放行的表驱动覆盖、真实文件系统端到端(首次/幂等/清单自愈/可信旧版覆盖/用户修改保留且无备份/`prompt` 保留与 `pendingUpdate`/放行覆盖/失败回滚/备份收敛/非匹配项不删/残留临时目录清理/日志分级/源缺失)、以真实 `skills/tianshu-mcp` 为源的冒烟。
|
|
30
|
+
- `test/unit/config-hotreload.test.ts` 补 `skills.autoInstall` 三态与 `skills.backupKeep` 的默认值/覆盖/非法值 last-known-good 用例。
|
|
31
|
+
- 严格 stdio 门禁新增两个独立场景(6→8):`skill-locally-modified`(种子预置「含本地修改」目录 → 断言不覆盖、无备份、无安装日志)、`skill-approve-update`(种子预置「来源不明」目录 + 传参 → 断言覆盖为包内版本、生成备份、写入清单);新增 dev-only 夹具 `scripts/seed-skill-state.mjs`(登记为 `npm run seed:skill-state`,不进 npm 包)。
|
|
32
|
+
- 全量 **898 passed / 12 skipped**(Windows 10 x64,Node 24.18.0),较 v0.5.10 净增 33 项用例。
|
|
33
|
+
|
|
34
|
+
### 文档
|
|
35
|
+
|
|
36
|
+
- 新增 `docs/release-v0.6.0.md` / `.en.md`、`docs/issue-16-skill-install-hardening-record.md`(Windows 10 真机复验 R1–R7 原始证据与未覆盖项)。
|
|
37
|
+
- README 双语新增「技能自检安装」小节(源定位、清单与三类判定、`autoInstall` 三态表、放行/关闭参数)并补 M30 里程碑;ARCHITECTURE 双语新增 §3.4「技能自检安装的信任与判定模型」(判定矩阵、原子性、日志分级、备份治理)并把技能边界列入安全红线;SECURITY 双语新增「技能自检安装的供应链边界」;`docs/agent-profiles.md` / `.en.md` 更新 `skills` 配置说明;HANDOFF 同步版本快照与文件表。
|
|
38
|
+
|
|
39
|
+
## [0.5.10] - 2026-09-23
|
|
40
|
+
|
|
41
|
+
### 新增
|
|
42
|
+
|
|
43
|
+
- **`run_task` / `verify_task` 幂等键 `idempotencyKey`**([issue #15](https://github.com/lanlan0811/tianshu-mcp/issues/15)):宿主在 `tools/call` 超时后重试不再变成「重复派单 / 重复验收」。
|
|
44
|
+
- `run_task`:TTL(默认 24h)内同键同参重复提交**恒返回原 `taskId` 与当前 meta**(含终态任务,只读不重派);同键异参 fail-closed 报错并回报原 `taskId`。
|
|
45
|
+
- `verify_task`:同键验收**执行中**返回成功结果 + `idempotencyReplay: "in_progress"`(刻意不是 `isError`,避免宿主再重试放大);**已完成**直接返回既有报告路径与该轮 `reportRound` / 结论,**不重跑**检查。
|
|
46
|
+
- 两个工具各自独立命名空间;键为 trim 后 1..128 字符、不含控制字符(协议级校验,非法即 `参数不合法`)。
|
|
47
|
+
- 新数据文件 `<数据目录>/idempotency.json`(原子写、惰性加载、TTL + 容量裁剪),进程重启后仍能识别重试;`TaskMeta` 新增 `idempotencyKey` / `idempotencyScope` / `idempotencyDigest` 供审计与映射文件损坏时重建。
|
|
48
|
+
- 新配置 `config.json` → `idempotency.ttlMs`(默认 `86400000`)、`idempotency.maxEntries`(默认 `2000`,超限按 `createdAt` 逐出最旧)。
|
|
49
|
+
- meta 块新增 `idempotencyKey`、`idempotencyReplay`(`hit` / `in_progress`)与 `projectActiveTask`;`tools/list` 的 `annotations.idempotentHint` 对两个工具置 `true`(**前提是调用方传入 `idempotencyKey`**,工具描述与技能文档已写明)。
|
|
50
|
+
- **重复派单降级保险**:未传幂等键时,`run_task` 仍会点名同工作区未结束的任务(活动态 + `needs_user`),提示改用幂等键或先 `query_task` 复核。
|
|
51
|
+
|
|
52
|
+
### 修复
|
|
53
|
+
|
|
54
|
+
- 幂等命中时向原任务事件流追加审计 `note`(`幂等重放:keyDigest=…`);键**明文不入日志与事件流**(只用 sha256 前 8 位摘要)。
|
|
55
|
+
- `verify_task` 独立路径记录 id 由 `vfy_<Date.now()>` 改为既有的 `genVerifyId()`(`vfy_<时间戳>_<随机6位>`,此前该函数全仓无调用方),同毫秒并发不再可能撞 id。
|
|
56
|
+
- **崩溃窗口**:幂等路径在同一临界区内**先落映射、再建任务**(`runExclusive` 串行化同键并发);崩溃于两者之间时,重试看到「有映射无任务快照」即视为未生效并重新派发,不会留下两条 agent 队列。
|
|
57
|
+
- **写失败 fail-open**:映射落盘失败仍返回已派发的任务,并在响应与 meta 明示「幂等记录写入失败,本任务无法被同键重放」,绝不把已经跑起来的 agent 报成派发失败。
|
|
58
|
+
|
|
59
|
+
### 测试
|
|
60
|
+
|
|
61
|
+
- 新增单元用例 `test/unit/idempotency.test.ts`(16 项):键规范、`canonicalDigest` 稳定序列化、TTL 过期、容量逐出、跨实例恢复、损坏重建、`runExclusive` 串行化与失败不阻断、在途标记、写失败 fail-open。
|
|
62
|
+
- 新增集成用例 `test/integration/idempotency.test.ts`(8 项):同键重放只产生一个 `tsk_*` 目录、同键异参 fail-closed、终态照实重放、未传 key 零回归、`projectActiveTask` 提示、独立路径已完成重放不新增报告、**执行中重放为成功结果**、taskId 模式重放、**跨 server 重启**的派单与验收重放。
|
|
63
|
+
- 协议用例补 `idempotentHint` 断言(仅两个工具为 true);`config-hotreload` 补 `idempotency.ttlMs` / `maxEntries` 默认值与覆盖。
|
|
64
|
+
- 类型检查、lint(`--max-warnings 0`)、全量测试、构建、严格 stdio 检查(6/6)、`pack:check` 全部通过。
|
|
65
|
+
|
|
66
|
+
### 文档
|
|
67
|
+
|
|
68
|
+
- 新增 `docs/release-v0.5.10.md` / `.en.md`;README 双语补幂等重试条目、工具表与里程碑;ARCHITECTURE 双语补数据文件、配置项与注解说明;`skills/tianshu-mcp/` 补参数速查、meta 字段、错误码与纪律;HANDOFF 同步版本快照。
|
|
69
|
+
|
|
10
70
|
## [0.5.9] - 2026-09-23
|
|
11
71
|
|
|
12
72
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# tianshu-mcp
|
|
10
10
|
|
|
11
|
-
Visual acceptance (since v0.5.0, with optional AI content validation since v0.5.4): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.
|
|
11
|
+
Visual acceptance (since v0.5.0, with optional AI content validation since v0.5.4): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.10.en.md>) · [All versions](CHANGELOG.en.md).
|
|
12
12
|
|
|
13
13
|
**Tianshu × AI-Agent orchestration MCP server**
|
|
14
14
|
|
|
@@ -44,7 +44,9 @@ Tianshu plays the role of the overall commander; this MCP server is the **schedu
|
|
|
44
44
|
- **Rework loop**: automatic rework (`autoFixRounds`) + manual `rework_task`; on verification failure a repair-plan file is generated and fed back to the agent; when rounds run out → `needs_attention` awaiting Tianshu's verdict.
|
|
45
45
|
- **Execution surfaces**: `driver: "gui"` selects an explicit, isolated Codex/TraeWork/ZCode/Kimi Code/Qoder CN CDP adapter; `driver: "spawn"` runs an external CLI child process.
|
|
46
46
|
- **Project-less dispatch (ZCode, issue #12)**: `run_task`'s `projectPath` may be omitted — ZCode runs the task in its `default` workspace without registering/importing a project, collecting a Git baseline, or running project acceptance (the result is marked structurally as `verificationNotApplicable: "no_project"` and `verify_task`/`get_task_report` return a not-applicable explanation). The companion `allowCreateProject: false` stops dispatch before any import side effect when the target directory is unregistered. See the [ZCode CDP adapter](docs/zcode-cdp.en.md).
|
|
47
|
-
- **
|
|
47
|
+
- **Idempotent retries (issue #15)**: `run_task` / `verify_task` accept an optional `idempotencyKey` — a retry with the same key within the TTL (24 h default) never duplicates a dispatch (the original `taskId` and its current status are returned) or re-runs verification (a running pass answers "in progress", a finished one returns the existing report); the same key with different arguments fails closed. The mapping is persisted in `<data-dir>/idempotency.json` and survives a server restart. See the [v0.5.10 release notes](<docs/release-v0.5.10.en.md>).
|
|
48
|
+
- **Skill self-install (issue #16)**: on startup the **in-package** `skills/tianshu-mcp/` is synced idempotently to `~/.rivet/skills/tianshu-mcp/`. The source is located relative to the package via `import.meta.url` (no cwd content discovery); an install manifest inside the target lets it auto-upgrade **only when the copy is provably untouched**, while **detected local edits or an unknown source are kept with a warning**; overwrites go through an atomic "tmp dir → backup → swap in" path and are governed by `skills.autoInstall` (`true`/`"prompt"`/`false`) and `skills.backupKeep`. See [README §Skill self-install](#skill-self-install).
|
|
49
|
+
- **Scheduling discipline**: per-project serial queue + global concurrency cap (default 2, configurable); even without an idempotency key, `run_task` names the unfinished task in the same workspace so a retry is not mistaken for a fresh dispatch.
|
|
48
50
|
- **Optional AI content validation (v0.5.4, off by default)**: validates whether the **content** of an image or page screenshot matches an expectation you declare explicitly. Judgement is fully **delegated to a local command you supply** (the MCP reads, stores, and forwards no keys and ships no model client), it **warns only** by default and can be upgraded to failing per rule, and it debounces with majority sampling plus a task-level cache; split votes or confidence below the threshold yield `uncertain`, which never gates and never triggers rework. Configuration and the command contract are in [visual acceptance](docs/visual-acceptance.en.md).
|
|
49
51
|
- **No key handling**: each agent uses its own login state; this server never stores or forwards any API key. Optional AI content validation adds no credential management either — the judge command manages its own key (see [SECURITY.en.md](SECURITY.en.md)).
|
|
50
52
|
- **Extensible**: a new agent = one profile (data) + (if needed) one adapter file — no changes to the orchestration core.
|
|
@@ -70,7 +72,7 @@ git clone https://github.com/lanlan0811/tianshu-mcp.git
|
|
|
70
72
|
cd tianshu-mcp
|
|
71
73
|
npm ci
|
|
72
74
|
npm run build # sync-version + tsc → dist/
|
|
73
|
-
npm test #
|
|
75
|
+
npm test # 867 passed / 12 skipped (879 tests, 85 files: unit/integration/protocol plus 3 real-browser files skipped by design)
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
### Install the npm package
|
|
@@ -184,13 +186,13 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
184
186
|
|
|
185
187
|
| Tool | Capability / approval | Purpose |
|
|
186
188
|
|---|---|---|
|
|
187
|
-
| `run_task` | write + approval | Dispatch work (optional auto-verify / auto-rework); returns `taskId` asynchronously |
|
|
189
|
+
| `run_task` | write + approval | Dispatch work (optional auto-verify / auto-rework); returns `taskId` asynchronously. Optional `idempotencyKey`: a retry with the same key returns the original `taskId` instead of creating a task |
|
|
188
190
|
| `continue_task` | write + approval | Resume the session behind `needs_user` (ZCode resumes the recorded session; Codex re-observes for `user_confirmation` / re-dispatches for `login_required`; Kimi Code resumes the recorded session and distinguishes question answering / re-observation / full re-dispatch) |
|
|
189
191
|
| `query_task` | read | Poll status / progress / log tail |
|
|
190
192
|
| `list_tasks` | read | Filtered history of tasks |
|
|
191
193
|
| `get_task_report` | read | Full text of a verification round's report (`report.md`) |
|
|
192
194
|
| `cancel_task` | write + approval | Cancel a running task: CLI agents kill the process tree; GUI agents click the in-app stop control over CDP and bounded-wait (`gui.cancelWaitMs`, default 15s) for the GUI to go idle, stating so explicitly in the final message when the stop is unconfirmed. For a terminal GUI task this call doubles as the manual acknowledgement entry point — after verifying the window holds no residual run, it clears the `guiStopUnconfirmed` marker |
|
|
193
|
-
| `verify_task` | read | Run one verification pass on a task/project path (no source changes) |
|
|
195
|
+
| `verify_task` | read | Run one verification pass on a task/project path (no source changes). Optional `idempotencyKey`: a retry with the same key never re-runs (a running pass answers "in progress", a finished one returns the existing report) |
|
|
194
196
|
| `rework_task` | write + approval | Manual rework (feed the failure report back to the same agent) |
|
|
195
197
|
| `get_profiles` | read | Inspect agent adapters and executable discovery results |
|
|
196
198
|
| `prepare_visual_baseline` | write + approval | Capture or import reference images into a reviewable candidate with a digest |
|
|
@@ -200,6 +202,8 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
200
202
|
|
|
201
203
|
> **Path safety gate** (since v0.4.0): `projectPath` is validated at submission — must be absolute, the directory must exist, and symlinks are canonicalized via realpath (the receipt notes the resolution). The home directory itself and system/root directories are rejected outright so a worker's write access can never cover a whole system subtree; dirty git repos come with an uncommitted-changes coexistence warning.
|
|
202
204
|
|
|
205
|
+
> **Idempotent retries** (since v0.5.10, issue #15): the `idempotencyKey` of `run_task` / `verify_task` makes a host timeout retry safe to repeat — the same key with the same arguments returns the original `taskId` (`run_task`, terminal tasks included) or the existing report (`verify_task`, including an "in progress" success result with `idempotencyReplay: "in_progress"`); the same key with different arguments fails with the original record id in the message. Key plaintext never enters logs or the event stream (only a digest). Boundaries: a `verify_task(taskId=…)` key is not written into the task snapshot, and "in progress" detection is process-local. See the [v0.5.10 release notes](<docs/release-v0.5.10.en.md>).
|
|
206
|
+
|
|
203
207
|
## Logging & stdio contract
|
|
204
208
|
|
|
205
209
|
This server is a standard MCP **stdio server** and follows the transport contract strictly:
|
|
@@ -212,6 +216,37 @@ The data directory defaults to `~/.tianshu-mcp` (override with `TIANSHU_MCP_HOME
|
|
|
212
216
|
|
|
213
217
|
Use `server.log` when troubleshooting connections; do not treat stderr output itself as a server fault.
|
|
214
218
|
|
|
219
|
+
## Skill self-install
|
|
220
|
+
|
|
221
|
+
On startup, the **in-package** `skills/tianshu-mcp/` is synced idempotently to `~/.rivet/skills/tianshu-mcp/` so the host (Tianshu) picks up the orchestration skill in a new session. Three points:
|
|
222
|
+
|
|
223
|
+
- **Skill content comes from the package only**: the source directory is located relative to the module via `import.meta.url` (both source and dist runs resolve to the in-package `skills/`); content is **never** discovered from the current working directory. When no source is found, install is skipped with a warning.
|
|
224
|
+
- **No silent overwrite on mismatch**: an install manifest `<dest>/.tianshu-mcp-install.json` (version + content hash) is kept inside the target. Three cases follow from it — content identical → skip; a stale copy this package provably installed and left untouched → handled per `skills.autoInstall`; **manifest hash ≠ target content (i.e. you edited files) or no valid manifest (unknown source) → your version is kept and a warning is logged**, never overwritten.
|
|
225
|
+
- **Overwrite is atomic**: content is staged into `<dest>.incoming-*`, the old directory is backed up as `.bak-<timestamp>`, then swapped in; failures roll back, leaving no half-copied tree. After an overwrite, historical backups are pruned to `skills.backupKeep` (default 3).
|
|
226
|
+
|
|
227
|
+
```jsonc
|
|
228
|
+
// <data dir>/config.json
|
|
229
|
+
{
|
|
230
|
+
"skills": {
|
|
231
|
+
"autoInstall": true, // true (default) | "prompt" | false
|
|
232
|
+
"backupKeep": 3 // backups kept after an overwrite; 0 = never prune
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| `autoInstall` | Behaviour |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `true` (default) | First install; skip when identical; a provably untouched stale copy is **backed up and upgraded automatically**; your local edits and unknown-source directories are **still kept as-is** |
|
|
240
|
+
| `"prompt"` | First install as usual; otherwise the same, but **no automatic overwrite when a change is needed** — only a warning plus a `pendingUpdate` note in the manifest |
|
|
241
|
+
| `false` | No automatic install at all |
|
|
242
|
+
|
|
243
|
+
Opt-in and opt-out (CLI flag or equivalent env var; `--no-skill-install` / `autoInstall:false` vetoes everything else):
|
|
244
|
+
|
|
245
|
+
- `--approve-skill-update` (or `TIANSHU_MCP_APPROVE_SKILL_UPDATE=1`): for this run, permit a "needs change" skill directory to be overwritten with the in-package version (after backup). **It does not apply to directories confirmed to carry your local edits** — those require renaming/deleting the directory and restarting, or merging your edits manually.
|
|
246
|
+
- `--no-skill-install` (or `TIANSHU_MCP_NO_SKILL_INSTALL=1`): no skill install or check for this run.
|
|
247
|
+
|
|
248
|
+
On stderr, `INFO` means skipped/installed; `WARN` means a stale copy was upgraded, your edits were kept, or an issue was detected (short markers such as `含本地修改` / `来源不明` / `未自动覆盖` are searchable). A failed install only warns — it never blocks the server.
|
|
249
|
+
|
|
215
250
|
## Documentation
|
|
216
251
|
|
|
217
252
|
| Doc | Content |
|
|
@@ -228,6 +263,7 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
|
|
|
228
263
|
| [docs/codex-windows-smoke.en.md](docs/codex-windows-smoke.en.md) | Codex Windows hardware record (incl. verify-fail → auto plan → repair-pass loop) |
|
|
229
264
|
| [docs/release-v0.3.4.en.md](<docs/release-v0.3.4.en.md>) | v0.3.4 release notes (ZCode project/model read-back, initialization recovery, session dispatch confirmation, issues #8/#9/#10) |
|
|
230
265
|
| [docs/qoder-cdp.en.md](docs/qoder-cdp.en.md) | Qoder CN GUI driver: installation discovery and instance reuse, full-path workspaces with native import, `modelSource` and global Model Management reasoning tiers, send/answer checkpoints, liveness judging and same-session repair, hardware evidence and uncovered items |
|
|
266
|
+
| [docs/release-v0.5.10.en.md](<docs/release-v0.5.10.en.md>) | v0.5.10 release notes (`run_task` / `verify_task` idempotency keys: TTL replay, in-progress answer, fail-closed on same key with different arguments, the persisted `idempotency.json`, and the `idempotentHint` annotation; issue #15) |
|
|
231
267
|
| [docs/release-v0.5.9.en.md](<docs/release-v0.5.9.en.md>) | v0.5.9 release notes (truthful GUI terminal state on server exit / restart archiving: wording branches on `guiStop`, the bounded `shutdown.guiStopWaitMs` wait, structured pending fields and the manual acknowledgement entry point; issue #14) |
|
|
232
268
|
| [docs/release-v0.5.8.en.md](<docs/release-v0.5.8.en.md>) | v0.5.8 release notes (the four primary documents rewritten against the code, plus the missing TraeWork probe and three probe scripts; no runtime change) |
|
|
233
269
|
| [docs/release-v0.5.7.en.md](<docs/release-v0.5.7.en.md>) | v0.5.7 release notes (orchestration skill docs rewritten against the code: parameter matrix, default precedence, tier correction and the qoder section; no runtime change) |
|
|
@@ -401,6 +437,19 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
|
|
|
401
437
|
- **Bounded wait**: the new `shutdown.guiStopWaitMs` (15 s default, one shared budget) lets the best-effort stop finish before the terminal state is written; both branches of `abortTerminal()` now persist `guiStop` plus the structured fields, and the window name is derived from `profile.displayName`
|
|
402
438
|
- **Manual acknowledgement entry point**: new `TaskMeta.interruptedCleanStop` / `guiResidualUnconfirmed` and the meta-block `guiStopUnconfirmed`; calling `cancel_task` on a terminal GUI task clears the pending marker (no new tool, terminal status unchanged)
|
|
403
439
|
- See the [v0.5.9 release notes](<docs/release-v0.5.9.en.md>)
|
|
440
|
+
- **M29 — Idempotency keys for dispatch and verification + v0.5.10** (2026-09-23) — 16 new unit cases + 8 new integration cases (issue #15)
|
|
441
|
+
- **No duplicated side effects**: a same-key `run_task` retry always returns the original `taskId` and current meta (terminal tasks included — read-only, never re-dispatched); `verify_task` answers a running pass with a success result plus `idempotencyReplay: "in_progress"` and returns the existing report and round once finished, **re-running no** `build`/`e2e`/deploy check
|
|
442
|
+
- **Fail-closed and honest disclosure**: the same key with different arguments errors while reporting the original record id; a failed mapping write still returns the dispatched task but states that it cannot be replayed by key; a crash between "mapping written" and "task created" is treated as not-yet-effective and re-dispatched
|
|
443
|
+
- **Persistence and observability**: `<data-dir>/idempotency.json` (atomic writes + TTL/capacity pruning, effective across restarts), configurable `idempotency.ttlMs` / `maxEntries`, new meta fields `idempotencyKey`/`idempotencyReplay`/`projectActiveTask`, and an event-stream note keyed by digest only
|
|
444
|
+
- **Protocol-level completion**: `tools/list` reports `idempotentHint: true` for the two tools (only meaningful when the caller supplies a key); without a key `run_task` still names the unfinished task in the same workspace
|
|
445
|
+
- See the [v0.5.10 release notes](<docs/release-v0.5.10.en.md>)
|
|
446
|
+
- **M30 — Skill self-install hardening (source location / overwrite semantics / tri-state & backup governance) + v0.6.0** (2026-09-23) — 30 new unit cases (issue #16)
|
|
447
|
+
- **Source location tightened**: the skill source is located relative to the package via `import.meta.url` only; **both `process.cwd()` candidates were removed** (content is never discovered from the current working directory, closing the "poisoned while debugging inside a third-party repo" supply-chain surface). When no source is found, the existing skip-with-warning path is kept
|
|
448
|
+
- **Distinguishing a "stale package copy" from "your local edits"**: the target now carries a manifest `<dest>/.tianshu-mcp-install.json` (version + content hash, with the manifest itself and platform noise such as `.DS_Store` excluded from the hash). Auto-upgrade happens **only when the copy is provably untouched**; detected local edits or an unknown source are **kept with a loud warning** (plus two remediation paths)
|
|
449
|
+
- **Tri-state and a new entry point**: `skills.autoInstall` becomes `true | "prompt" | false` (backwards compatible with the existing boolean); `--approve-skill-update` / `TIANSHU_MCP_APPROVE_SKILL_UPDATE=1` is a non-interactive approval channel (it does not apply to confirmed local edits); new `skills.backupKeep` (default 3)
|
|
450
|
+
- **Atomic install and log levels**: "tmp dir → backup → swap in", with rollback on failure so no half-copied tree remains; skip = INFO, overwrite/keep/unknown-source/failure = WARN; after an overwrite, historical `.bak-<timestamp>` directories are pruned to `backupKeep`
|
|
451
|
+
- **Gates**: the strict stdio check gains `skill-locally-modified` / `skill-approve-update` scenarios (6→8); Windows 10 real-machine verification R1–R7 recorded in the [issue-16 record](docs/issue-16-skill-install-hardening-record.md)
|
|
452
|
+
- See the [v0.6.0 release notes](<docs/release-v0.6.0.en.md>)
|
|
404
453
|
|
|
405
454
|
## Agent support status
|
|
406
455
|
|
|
@@ -477,7 +526,7 @@ Behavior and limits:
|
|
|
477
526
|
|
|
478
527
|
| Document | Content |
|
|
479
528
|
|---|---|
|
|
480
|
-
| [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.
|
|
529
|
+
| [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.10) |
|
|
481
530
|
| [CONTRIBUTING.en.md](CONTRIBUTING.en.md) | Dev setup, conventions, commit/release flow, adding an agent |
|
|
482
531
|
| [SECURITY.en.md](SECURITY.en.md) | Security model (zero credentials / command whitelist / process & desktop-automation boundaries) and private reporting |
|
|
483
532
|
| [CODE_OF_CONDUCT.en.md](CODE_OF_CONDUCT.en.md) | Contributor Code of Conduct |
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# tianshu-mcp
|
|
10
10
|
|
|
11
|
-
视觉验收(v0.5.0 起,含 v0.5.4 可选 AI 内容校验):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.
|
|
11
|
+
视觉验收(v0.5.0 起,含 v0.5.4 可选 AI 内容校验):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.10.md>) · [全部版本](CHANGELOG.md)。
|
|
12
12
|
|
|
13
13
|
**天枢 × AI-Agent 编排 MCP server**
|
|
14
14
|
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
- **失败返修闭环**:自动返修(`autoFixRounds`)+ 手动 `rework_task`;验收失败时自动生成修复计划文件并回填给 agent;轮次用尽 → `needs_attention` 等天枢裁决。
|
|
45
45
|
- **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode / Kimi Code 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
|
|
46
46
|
- **无项目派发(ZCode,issue #12)**:`run_task` 的 `projectPath` 可省略——ZCode 在 `default` 工作区承接任务,不登记/导入项目、不采集 Git 基线、不执行项目验收(结果以 `verificationNotApplicable: "no_project"` 结构化标注,`verify_task`/`get_task_report` 返回不适用说明)。配套 `allowCreateProject: false` 可在目标目录未登记时于任何导入副作用之前停止派发。详见 [ZCode CDP 适配器](docs/zcode-cdp.md)。
|
|
47
|
-
-
|
|
47
|
+
- **幂等重试(issue #15)**:`run_task` / `verify_task` 接受可选 `idempotencyKey`——同一 key 在 TTL(默认 24h)内的重试**不会**重复派单(恒返回原 `taskId` 与当前状态)或重复跑验收(执行中返回进行中提示,已完成直接返回既有报告);同键异参 fail-closed 报错。映射落盘于 `<数据目录>/idempotency.json`,跨 server 重启仍生效。详见 [v0.5.10 发布说明](<docs/release-v0.5.10.md>)。
|
|
48
|
+
- **技能自检安装(issue #16)**:启动时把**包内** `skills/tianshu-mcp/` 幂等同步到 `~/.rivet/skills/tianshu-mcp/`。源只由 `import.meta.url` 相对包自身定位(无 cwd 内容发现);目标内含安装清单,据此仅在**可证未被改动**时自动升级,**检出你的本地修改或来源不明一律保留 + 告警**;覆盖走「临时目录 → 备份 → 换入」的原子路径,并按 `skills.autoInstall`(`true`/`"prompt"`/`false`)与 `skills.backupKeep` 治理。详见 [README §技能自检安装](#技能自检安装)。
|
|
49
|
+
- **调度纪律**:每项目串行队列 + 全局并发上限(默认 2,可配);未传幂等键时,`run_task` 仍会点名同工作区未结束的任务,避免误判为重试。
|
|
48
50
|
- **可选 AI 内容校验(v0.5.4,默认关闭)**:校验图片或页面截图**内容**是否符合你显式声明的期望描述。判定完全**委托给你自备的本地命令**(MCP 不读取、不存储、不转发任何密钥,也不内置模型客户端),默认**仅告警**、逐规则可升级为致败;采样多数票 + 任务级缓存防抖,票不集中或低于置信度阈值判 `uncertain`(永不阻塞、不触发返修)。配置与命令契约见 [视觉验收](docs/visual-acceptance.md)。
|
|
49
51
|
- **不碰密钥**:各 agent 用自己的登录态;本 server 不保存/转发任何 API key。可选 AI 内容校验同样不引入凭证管理——判定命令自己管密钥(见 [SECURITY.md](SECURITY.md))。
|
|
50
52
|
- **可扩展**:新 agent = 一个 profile(数据)+(如需)一个 adapter 文件,零改编排核心。
|
|
@@ -70,7 +72,7 @@ git clone https://github.com/lanlan0811/tianshu-mcp.git
|
|
|
70
72
|
cd tianshu-mcp
|
|
71
73
|
npm ci
|
|
72
74
|
npm run build # sync-version + tsc → dist/
|
|
73
|
-
npm test #
|
|
75
|
+
npm test # 867 passed / 12 skipped(879 项,85 个测试文件:单元/集成/协议 + 3 个真实浏览器文件按设计 skip)
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
### 安装 npm 包
|
|
@@ -180,13 +182,13 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
180
182
|
|
|
181
183
|
| 工具 | 能力 / 审批 | 作用 |
|
|
182
184
|
|---|---|---|
|
|
183
|
-
| `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId` |
|
|
185
|
+
| `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey`:同键重试恒返回原 `taskId`,不新建任务 |
|
|
184
186
|
| `continue_task` | write + 审批 | 恢复 `needs_user` 的原会话(ZCode 恢复原会话;Codex 按 `user_confirmation` 重新观察 / `login_required` 重派;Kimi Code 恢复原会话并区分提问续答 / 重新观察 / 补发任务书) |
|
|
185
187
|
| `query_task` | read | 轮询状态 / 进度 / 日志尾 |
|
|
186
188
|
| `list_tasks` | read | 历史任务过滤列表 |
|
|
187
189
|
| `get_task_report` | read | 某轮验收报告全文(`report.md`) |
|
|
188
190
|
| `cancel_task` | write + 审批 | 取消运行中任务:CLI agent kill 进程树;GUI agent 经 CDP 点击停止并在 `gui.cancelWaitMs`(默认 15s)内有界等待 GUI 空闲,未确认停止时终态明示。对已终态的 GUI 任务,本调用兼任人工确认入口——核实窗口无残留运行后调用可清除 `guiStopUnconfirmed` 待确认标记 |
|
|
189
|
-
| `verify_task` | read |
|
|
191
|
+
| `verify_task` | read | 对任务/项目路径做一次验收(不改源码);可选 `idempotencyKey`:同键重试不重跑(执行中返回进行中提示,已完成返回既有报告) |
|
|
190
192
|
| `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent) |
|
|
191
193
|
| `get_profiles` | read | 查看 agent 适配与可执行探测结果 |
|
|
192
194
|
| `prepare_visual_baseline` | write + 审批 | 截图或导入参考图,生成待审阅候选和摘要 |
|
|
@@ -198,6 +200,8 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
198
200
|
|
|
199
201
|
> **无项目派发**(ZCode 专用,v0.5.2 起):省略 `projectPath` 时任务在 ZCode 的 `default` 工作区运行,跳过项目登记、Git 基线、项目快照、项目锁与项目验收(终态标注 `not_applicable: no_project`)。`allowCreateProject=false` 可禁止自动导入未登记的项目。详见 [docs/zcode-cdp.md](docs/zcode-cdp.md#无项目default-工作区)。
|
|
200
202
|
|
|
203
|
+
> **幂等重试**(v0.5.10 起,issue #15):`run_task` / `verify_task` 的 `idempotencyKey` 让宿主的超时重试安全可重复——同键同参返回原 `taskId`(`run_task`,含终态)或既有报告(`verify_task`,含「仍在执行中」的成功结果 + `idempotencyReplay: "in_progress"`);同键异参直接报错并回报原记录 id。键明文不入日志与事件流(只用摘要)。边界:`verify_task(taskId=…)` 的键不写入任务快照,且「执行中」判定是进程内的。详见 [v0.5.10 发布说明](<docs/release-v0.5.10.md>)。
|
|
204
|
+
|
|
201
205
|
## 日志与 stdio 契约
|
|
202
206
|
|
|
203
207
|
本 server 是标准 MCP **stdio server**,严格遵守传输契约:
|
|
@@ -210,6 +214,37 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
210
214
|
|
|
211
215
|
排查连接问题时以 `server.log` 为准;不要因为 stderr 有输出就判定 server 异常。
|
|
212
216
|
|
|
217
|
+
## 技能自检安装
|
|
218
|
+
|
|
219
|
+
启动时把**包内** `skills/tianshu-mcp/` 幂等同步到 `~/.rivet/skills/tianshu-mcp/`,让宿主(天枢)在新会话里读到编排技能。三个要点:
|
|
220
|
+
|
|
221
|
+
- **技能内容只来自包自身**:源目录由 `import.meta.url` 相对定位(dev 直跑与 dist 运行都指向包内 `skills/`),**不**从当前工作目录发现内容。找不到源时跳过安装并告警。
|
|
222
|
+
- **不一致时不静默覆盖**:安装目录内维护清单 `<目标>/.tianshu-mcp-install.json`(版本 + 内容 hash)。据此分三类——内容一致 → 跳过;清单可证是本包装过、未被改动的旧版副本 → 按 `skills.autoInstall` 处理;**清单记录与目标内容不符(= 检出你改过文件)或没有有效清单(来源不明)→ 默认保留你的版本并告警**,不会覆盖。
|
|
223
|
+
- **覆盖是原子的**:先装到 `<目标>.incoming-*`,再备份旧目录为 `.bak-<时间戳>`,最后换入;失败回滚,不留半成品。覆盖后按 `skills.backupKeep`(默认 3)收敛历史备份。
|
|
224
|
+
|
|
225
|
+
```jsonc
|
|
226
|
+
// <数据目录>/config.json
|
|
227
|
+
{
|
|
228
|
+
"skills": {
|
|
229
|
+
"autoInstall": true, // true(默认)| "prompt" | false
|
|
230
|
+
"backupKeep": 3 // 覆盖后保留的历史备份个数;0 = 不清理
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
| `autoInstall` | 行为 |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `true`(默认) | 首次安装;内容一致跳过;可证未被改动的旧版副本**自动备份并升级**;用户本地修改与来源不明目录**仍保留不覆盖** |
|
|
238
|
+
| `"prompt"` | 首次安装照常;其余同上,但**需变更时不自动覆盖**,只告警并在清单记 `pendingUpdate` |
|
|
239
|
+
| `false` | 完全不自动安装 |
|
|
240
|
+
|
|
241
|
+
放行与关闭(命令行参数或等价环境变量;`--no-skill-install` / `autoInstall:false` 的否决权最高):
|
|
242
|
+
|
|
243
|
+
- `--approve-skill-update`(或 `TIANSHU_MCP_APPROVE_SKILL_UPDATE=1`):本次启动允许「需变更」的技能目录由包内版本覆盖(先备份)。**对已确证含用户本地修改的目录不生效**——那类只能人工改名/删除后重启,或手工合并改动。
|
|
244
|
+
- `--no-skill-install`(或 `TIANSHU_MCP_NO_SKILL_INSTALL=1`):本次启动不做任何技能安装与检查。
|
|
245
|
+
|
|
246
|
+
日志里 `INFO` 表示跳过/已安装,`WARN` 表示覆盖了旧版、保留了你的修改或检出问题(`含本地修改` / `来源不明` / `未自动覆盖` 等短句便于检索)。安装失败只告警,不阻断 server。
|
|
247
|
+
|
|
213
248
|
## 文档
|
|
214
249
|
|
|
215
250
|
| 文档 | 内容 |
|
|
@@ -225,6 +260,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
225
260
|
| [docs/kimi-cdp.md](docs/kimi-cdp.md) | Kimi Code GUI 驱动:双渲染进程(主窗口 + `Kimi Browser Overlay`)、工作区完整路径绑定与原生对话框导入、模型三级选择与思考档位、执行模式、运行检测与排障 |
|
|
226
261
|
| [docs/codex-windows-smoke.md](docs/codex-windows-smoke.md) | Codex Windows 真机验收记录(含验收失败→自动生成计划→返修通过闭环) |
|
|
227
262
|
| [docs/qoder-cdp.md](docs/qoder-cdp.md) | Qoder CN GUI 驱动:安装发现与实例复用、完整路径工作区与原生导入、`modelSource` 与模型管理全局思考等级、发送/答题检查点、运行判定与原会话返修、真机证据与未覆盖项 |
|
|
263
|
+
| [docs/release-v0.5.10.md](<docs/release-v0.5.10.md>) | v0.5.10 发布说明(`run_task` / `verify_task` 幂等键:TTL 重放、执行中提示、同键异参 fail-closed、`idempotency.json` 落盘与 `idempotentHint` 注解;issue #15) |
|
|
228
264
|
| [docs/release-v0.5.9.md](<docs/release-v0.5.9.md>) | v0.5.9 发布说明(server 退出 / 重启归档的 GUI 终态如实化:按 `guiStop` 分流文案、`shutdown.guiStopWaitMs` 有界等待、结构化待确认字段与人工确认入口;issue #14) |
|
|
229
265
|
| [docs/release-v0.5.8.md](<docs/release-v0.5.8.md>) | v0.5.8 发布说明(四份主文档按代码逐项核对重写 + 补发 TraeWork 探针与三个 probe script;无运行时变更) |
|
|
230
266
|
| [docs/release-v0.5.7.md](<docs/release-v0.5.7.md>) | v0.5.7 发布说明(编排技能文档按代码实况重写:参数兼容矩阵、默认值优先级、档位修正与 qoder 章节;无运行时变更) |
|
|
@@ -402,6 +438,19 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
402
438
|
- **有界等待**:新增 `shutdown.guiStopWaitMs`(默认 15s,全局共享预算)让「尽力停止 + 有界等待」跑完再落终态;`abortTerminal()` 两分支补齐 `guiStop` 与结构化字段,窗口名改由 `profile.displayName` 派生
|
|
403
439
|
- **人工确认入口**:新增 `TaskMeta.interruptedCleanStop` / `guiResidualUnconfirmed` 与 meta 块 `guiStopUnconfirmed`;对已终态 GUI 任务调用 `cancel_task` 可清除待确认标记(不新增工具,不改终态)
|
|
404
440
|
- 详见 [v0.5.9 发布说明](<docs/release-v0.5.9.md>)
|
|
441
|
+
- **M29 — 派单/验收幂等键 + v0.5.10**(2026-09-23)— 新增 16 单元 + 8 集成用例(issue #15)
|
|
442
|
+
- **不再重复副作用**:`run_task` 同键重试恒返回原 `taskId` 与当前 meta(含终态,只读不重派);`verify_task` 执行中返回成功结果 + `idempotencyReplay: "in_progress"`、已完成直接返回既有报告与轮次,**不重跑** `build`/`e2e`/部署类检查
|
|
443
|
+
- **fail-closed 与如实披露**:同键异参报错并回报原记录 id;映射写入失败仍返回已派发的任务但明示「无法被同键重放」;崩溃于「已落映射、未建任务」之间时按未生效重新派发
|
|
444
|
+
- **落盘与可观测**:`<数据目录>/idempotency.json`(原子写 + TTL/容量裁剪,跨重启生效)、`idempotency.ttlMs` / `maxEntries` 可配、meta 新增 `idempotencyKey`/`idempotencyReplay`/`projectActiveTask`、命中写事件流(只用键摘要)
|
|
445
|
+
- **协议层补齐**:`tools/list` 的 `idempotentHint` 对两个工具置 true(前提是调用方传键);未传键时仍点名同工作区未结束任务
|
|
446
|
+
- 详见 [v0.5.10 发布说明](<docs/release-v0.5.10.md>)
|
|
447
|
+
- **M30 — 技能自装加固(源定位 / 覆盖语义 / 三态与备份治理)+ v0.6.0**(2026-09-23)— 新增 30 单元用例(issue #16)
|
|
448
|
+
- **源定位收敛**:技能源只由 `import.meta.url` 相对包自身定位,**删除两处 `process.cwd()` 候选**(不再从当前工作目录发现内容,消除「在第三方仓库里调试即被投毒」的供应链面);找不到源时沿用跳过安装 + 告警
|
|
449
|
+
- **区分「旧版包」与「用户本地修改」**:安装目录内新增清单 `<目标>/.tianshu-mcp-install.json`(版本 + 内容 hash,hash 排除清单自身与 `.DS_Store` 等平台噪声);据此仅在**可证未被改动**时自动升级,检出本地修改或来源不明一律**保留 + 强告警**(附两条处置指引)
|
|
450
|
+
- **三态与新入口**:`skills.autoInstall` 升级为 `true | "prompt" | false`(向后兼容既有 boolean);新增 `--approve-skill-update` / `TIANSHU_MCP_APPROVE_SKILL_UPDATE=1` 作为非交互放行通道(对已确证的用户修改不生效);新增 `skills.backupKeep`(默认 3)
|
|
451
|
+
- **安装原子化与日志分级**:改为「tmp 目录 → 备份 → 换入」,失败回滚不留半成品;跳过=INFO、覆盖/保留/来源不明/失败=WARN;覆盖后按 `backupKeep` 收敛历史 `.bak-<时间戳>`
|
|
452
|
+
- **门禁**:严格 stdio 检查新增 `skill-locally-modified` / `skill-approve-update` 两场景(6→8);Windows 10 真机复验 R1–R7 留档 [issue-16 记录](docs/issue-16-skill-install-hardening-record.md)
|
|
453
|
+
- 详见 [v0.6.0 发布说明](<docs/release-v0.6.0.md>)
|
|
405
454
|
|
|
406
455
|
## Agent 适配现状
|
|
407
456
|
|
|
@@ -479,7 +528,7 @@ run_task(projectPath=/path/to/项目, agentId=codex-cli, task="任务书", autoV
|
|
|
479
528
|
| 文档 | 内容 |
|
|
480
529
|
|---|---|
|
|
481
530
|
| [HANDOFF.md](HANDOFF.md) | 项目交接文档:当前状态快照、架构导览、硬性红线、已知限制、接手建议 |
|
|
482
|
-
| [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.
|
|
531
|
+
| [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.10) |
|
|
483
532
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | 开发环境、工程规范、提交与发布流程、如何新增 agent |
|
|
484
533
|
| [SECURITY.md](SECURITY.md) | 安全模型(凭证零管理/命令白名单/进程与桌面自动化边界)与私密报告渠道 |
|
|
485
534
|
| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | 贡献者行为准则 |
|
package/dist/config/schema.js
CHANGED
|
@@ -29,6 +29,24 @@ export const ReasoningLevelSchema = z.enum([
|
|
|
29
29
|
"on",
|
|
30
30
|
"off",
|
|
31
31
|
]);
|
|
32
|
+
/**
|
|
33
|
+
* 幂等键(issue #15):调用方为「同一次逻辑派单/验收」提供的稳定标识。
|
|
34
|
+
* trim 后 1..128 字符、不允许控制字符;键在 `run_task` 与 `verify_task` 中**各自独立命名空间**。
|
|
35
|
+
* 键明文只落本地任务快照,日志与事件流只用摘要(见 `keyDigest`)。
|
|
36
|
+
*/
|
|
37
|
+
export const IdempotencyKeySchema = z
|
|
38
|
+
.string()
|
|
39
|
+
.transform((s) => s.trim())
|
|
40
|
+
.pipe(z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1, "idempotencyKey 不能为空白")
|
|
43
|
+
.max(128, "idempotencyKey 过长(上限 128 字符)")
|
|
44
|
+
.regex(/^[^\u0000-\u001f\u007f]+$/, "idempotencyKey 不允许控制字符"));
|
|
45
|
+
/** 幂等键命名空间:一次派单与一次验收互不干扰(issue #15)。 */
|
|
46
|
+
export const IdempotencyScopeSchema = z.enum(["run_task", "verify_task"]);
|
|
47
|
+
/** 幂等映射默认 TTL(24h)与条目上限(超限逐出最旧)。 */
|
|
48
|
+
export const IDEMPOTENCY_TTL_DEFAULT_MS = 24 * 60 * 60_000;
|
|
49
|
+
export const IDEMPOTENCY_MAX_ENTRIES_DEFAULT = 2000;
|
|
32
50
|
export const RunTaskParamsSchema = z.object({
|
|
33
51
|
/**
|
|
34
52
|
* 项目绝对路径。**省略** = 无项目模式(issue #12):目前仅 ZCode 支持——任务在其 `default`
|
|
@@ -76,6 +94,11 @@ export const RunTaskParamsSchema = z.object({
|
|
|
76
94
|
allowCreateProject: z.boolean().optional(),
|
|
77
95
|
context: z.string().optional(),
|
|
78
96
|
taskTimeoutMs: z.number().int().positive().optional(),
|
|
97
|
+
/**
|
|
98
|
+
* 幂等键(issue #15):TTL(默认 24h)内重复提交同一键**恒返回原 taskId 与当前 meta**,
|
|
99
|
+
* 不新建任务;同一键携带不同参数会被 fail-closed 拒绝。不传 = 保持既有行为。
|
|
100
|
+
*/
|
|
101
|
+
idempotencyKey: IdempotencyKeySchema.optional(),
|
|
79
102
|
});
|
|
80
103
|
export const QueryTaskParamsSchema = z.object({
|
|
81
104
|
taskId: z.string().min(1),
|
|
@@ -115,6 +138,11 @@ export const VerifyTaskParamsSchema = z.object({
|
|
|
115
138
|
* 独立 projectPath 验收缺省不设 = 采集当前基线,仅做项目当前健康检查。
|
|
116
139
|
*/
|
|
117
140
|
baselineRef: z.string().min(1).optional(),
|
|
141
|
+
/**
|
|
142
|
+
* 幂等键(issue #15):重复提交同一键不重跑验收——执行中返回进行中提示,
|
|
143
|
+
* 已完成直接返回已有报告与轮次;同一键携带不同参数会被 fail-closed 拒绝。
|
|
144
|
+
*/
|
|
145
|
+
idempotencyKey: IdempotencyKeySchema.optional(),
|
|
118
146
|
});
|
|
119
147
|
export const ReworkTaskParamsSchema = z.object({
|
|
120
148
|
taskId: z.string().min(1),
|
|
@@ -146,9 +174,19 @@ export const ServerConfigSchema = z.object({
|
|
|
146
174
|
* 项目级 .tianshu-mcp/acceptance.json 的 verifyConcurrency 可覆盖。
|
|
147
175
|
*/
|
|
148
176
|
verifyConcurrency: z.number().int().min(1).max(4).default(2),
|
|
177
|
+
/**
|
|
178
|
+
* 技能自检安装(issue #16 加固)。
|
|
179
|
+
* `autoInstall`:
|
|
180
|
+
* - `true`(默认):包内技能与目标不一致且可证目标未被改动时,自动备份并覆盖为新版;
|
|
181
|
+
* - `"prompt"`:首次安装照常,**需变更时**不自动覆盖,只告警并在清单记 `pendingUpdate`,
|
|
182
|
+
* 待用户带 `--approve-skill-update` 重启放行(stdio server 无同步交互通道);
|
|
183
|
+
* - `false`:完全不自动安装。
|
|
184
|
+
* `backupKeep`:覆盖后保留的历史 `.bak-<时间戳>` 个数,`0` = 不清理(默认 3)。
|
|
185
|
+
*/
|
|
149
186
|
skills: z
|
|
150
187
|
.object({
|
|
151
|
-
autoInstall: z.boolean().default(true),
|
|
188
|
+
autoInstall: z.union([z.boolean(), z.literal("prompt")]).default(true),
|
|
189
|
+
backupKeep: z.number().int().min(0).max(50).default(3),
|
|
152
190
|
})
|
|
153
191
|
.default({}),
|
|
154
192
|
/**
|
|
@@ -163,6 +201,22 @@ export const ServerConfigSchema = z.object({
|
|
|
163
201
|
guiStopWaitMs: z.number().int().positive().default(15_000),
|
|
164
202
|
})
|
|
165
203
|
.default({}),
|
|
204
|
+
/**
|
|
205
|
+
* 幂等键映射(issue #15)。
|
|
206
|
+
* `ttlMs`:`<数据目录>/idempotency.json` 里键→taskId 记录的有效期,到期即视为未命中并清除;
|
|
207
|
+
* `maxEntries`:条目上限,超限按 `createdAt` 逐出最旧(防映射文件无限增长)。
|
|
208
|
+
*/
|
|
209
|
+
idempotency: z
|
|
210
|
+
.object({
|
|
211
|
+
ttlMs: z.number().int().positive().default(IDEMPOTENCY_TTL_DEFAULT_MS),
|
|
212
|
+
maxEntries: z
|
|
213
|
+
.number()
|
|
214
|
+
.int()
|
|
215
|
+
.min(1)
|
|
216
|
+
.max(100_000)
|
|
217
|
+
.default(IDEMPOTENCY_MAX_ENTRIES_DEFAULT),
|
|
218
|
+
})
|
|
219
|
+
.default({}),
|
|
166
220
|
});
|
|
167
221
|
/* ---------------- agent-profiles.json ---------------- */
|
|
168
222
|
export const ExecutableDiscoverySchema = z.object({
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* tianshu-mcp 入口:stdio 启动 MCP server。
|
|
4
|
-
* 用法:node dist/index.js
|
|
4
|
+
* 用法:node dist/index.js [--no-skill-install] [--approve-skill-update]
|
|
5
|
+
* (env TIANSHU_MCP_HOME 可选覆盖数据目录;env TIANSHU_MCP_NO_SKILL_INSTALL / TIANSHU_MCP_APPROVE_SKILL_UPDATE 等价)
|
|
5
6
|
* 退出:SIGINT/SIGTERM / stdin EOF → 归档任务 → 杀子进程 → 退出。
|
|
6
7
|
*/
|
|
7
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -19,7 +20,10 @@ async function main() {
|
|
|
19
20
|
const home = resolveDataHome();
|
|
20
21
|
const logger = await Logger.create(path.join(home, "logs"));
|
|
21
22
|
const skipSkillInstall = process.argv.includes("--no-skill-install") || process.env.TIANSHU_MCP_NO_SKILL_INSTALL === "1";
|
|
22
|
-
|
|
23
|
+
// issue #16:放行「需变更但按策略未自动覆盖」的技能目录(prompt 保留 / 来源不明)。
|
|
24
|
+
// 对已确证含用户本地修改的目标不生效;--no-skill-install 的否决权高于本参数。
|
|
25
|
+
const approveSkillUpdate = process.argv.includes("--approve-skill-update") || process.env.TIANSHU_MCP_APPROVE_SKILL_UPDATE === "1";
|
|
26
|
+
const assembly = await buildServer({ logger, skipSkillInstall, approveSkillUpdate });
|
|
23
27
|
const transport = new StdioServerTransport();
|
|
24
28
|
await assembly.server.connect(transport);
|
|
25
29
|
logger.info(`tianshu-mcp 已连接(stdio)。数据目录: ${home},工具数: ${TOOL_DEFS.length}`);
|
package/dist/mcp/formatter.js
CHANGED
|
@@ -51,6 +51,7 @@ export function metaFromTask(meta, extra) {
|
|
|
51
51
|
// guiResidualUnconfirmed。未确认时为 true——编排方据此禁止直接重派。
|
|
52
52
|
guiStopUnconfirmed: meta.guiResidualUnconfirmed === true || meta.interruptedCleanStop === false ? true : undefined,
|
|
53
53
|
lastRunSignal: meta.lastRunSignal,
|
|
54
|
+
idempotencyKey: meta.idempotencyKey,
|
|
54
55
|
...extra,
|
|
55
56
|
};
|
|
56
57
|
}
|