@netpilot/skills 0.4.0 → 0.7.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +3 -1
- package/agents/codex/backend-reviewer.toml +4 -2
- package/agents/codex/frontend-reviewer.toml +2 -1
- package/agents/codex/migration-reviewer.toml +14 -0
- package/agents/codex/security-reviewer.toml +13 -0
- package/docs/agent-authoring.md +15 -0
- package/package.json +1 -1
- package/scripts/sync.mjs +354 -17
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"owner": {
|
|
5
5
|
"name": "NetPilot"
|
|
6
6
|
},
|
|
7
|
-
"description": "NetPilot
|
|
7
|
+
"description": "NetPilot AI 工程协作插件目录",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "netpilot-skills",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "中文工程协作 skills 与可组合工作流",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.7.0",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "NetPilot"
|
|
16
16
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "netpilot-skills",
|
|
4
4
|
"displayName": "NetPilot Skills",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.7.0",
|
|
6
6
|
"description": "面向 Claude Code 的中文工程协作 skills 与可组合工作流",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "NetPilot"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.7.0
|
|
8
|
+
|
|
9
|
+
- 新增用户级 `security-reviewer` 与 `migration-reviewer`:前者按应用、Agent/Tool 与供应链 profile 审查信任边界,后者按 Expand → Migrate → Contract 审查 schema、回填、兼容切换与恢复;两者均保持 high / read-only 和统一 P0–P3 证据契约。
|
|
10
|
+
- 扩展 `backend-reviewer` 的 API 消费者兼容、查询、索引、Redis 与 fail-safe 检查,扩展 `frontend-reviewer` 的 contract 漂移、服务端授权和浏览器 console/network 证据要求。
|
|
11
|
+
- 在 Agent 编写规范中固定六个默认角色的主任务路由、去重与串行降级边界;打包、README 与 roster 测试同步覆盖 6 个 Codex Agents。
|
|
12
|
+
- 与 `@netpilot/harness` 协调发布同一 `v0.7.0` 标签;Harness 只路由用户级安全与 migration reviewer,不在项目 `.codex/agents` 复制同名角色。
|
|
13
|
+
|
|
14
|
+
## 0.6.0
|
|
15
|
+
|
|
16
|
+
- 将用户级与项目级安装状态迁移到中性的 `.agents/.state/skills-installer`;旧 `.netpilot-skills` 在 apply 时经锁保护、目录身份复核后原子迁移,preview 仅报告计划。旧版客户端留下的精确空目录可安全自愈,真实双状态或目标冲突仍整批停止。
|
|
17
|
+
- 项目安装继续只写入 `.agents/skills`,不安装项目级 Agents;用户级默认仍安装 Codex Skills 与少量 Codex Agents。
|
|
18
|
+
- 与 `@netpilot/harness` 协调发布同一 `v0.6.0` 标签,便于在新电脑或新项目中识别兼容的一组安装器能力;两个包仍保持独立职责和发布契约。
|
|
19
|
+
|
|
7
20
|
## 0.4.0
|
|
8
21
|
|
|
9
22
|
- 将工程与生产力方法扩展为 21 个去个人化 skills,按上游原有结构恢复完整工作流、步骤内门禁、方法自身的反模式,以及有实际价值的 `references/` 和跨平台脚本;不再为每个 skill 强加统一尾部章节。
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ npm install --global @netpilot/skills
|
|
|
42
42
|
netpilot-skills
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
安装器不会覆盖未受管理或用户修改过的同名文件;发生冲突时整批停止。同步状态保存在 `.agents/.state/skills-installer`:用户安装位于 home,项目安装位于项目根;旧 `.netpilot-skills` 会在下一次实际安装时原子迁移,预览只展示计划。旧版安装器在新状态旁留下的空旧目录会安全自愈,含内容的双状态仍会阻断。项目范围只安装 Skills,项目级 Agents 仍由 [NetPilot Harness](https://github.com/netpilot-z/harness) 管理。
|
|
46
46
|
|
|
47
47
|
## 核心用法
|
|
48
48
|
|
|
@@ -93,6 +93,8 @@ Codex 使用 `$skill-name`;Claude Code 用户级安装使用 `/skill-name`。
|
|
|
93
93
|
| --- | --- | --- |
|
|
94
94
|
| `frontend-reviewer` | 前端状态、交互、可访问性和测试审查 | high / read-only |
|
|
95
95
|
| `backend-reviewer` | 接口、权限、事务和数据一致性审查 | high / read-only |
|
|
96
|
+
| `security-reviewer` | 应用、Agent、Tool 与供应链安全审查 | high / read-only |
|
|
97
|
+
| `migration-reviewer` | schema、回填、兼容切换与恢复审查 | high / read-only |
|
|
96
98
|
| `architecture-designer` | 模块边界、依赖和迁移方案比较 | high / read-only |
|
|
97
99
|
| `test-verifier` | 执行测试、类型检查、lint 和构建 | medium / workspace-write |
|
|
98
100
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
name = "backend-reviewer"
|
|
2
|
-
description = "
|
|
2
|
+
description = "只读审查后端、接口与数据访问变更的契约兼容、权限、事务并发、幂等性、可观测性和测试风险。"
|
|
3
3
|
model_reasoning_effort = "high"
|
|
4
4
|
sandbox_mode = "read-only"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
像后端代码所有者一样审查,优先正确性、安全边界和数据完整性。
|
|
7
7
|
先读取适用的 AGENTS.md、规格、后端标准、接口契约、数据模型和完整 diff,再追踪必要的调用方、迁移与测试。
|
|
8
|
-
|
|
8
|
+
重点检查输入验证、认证授权、租户隔离、事务边界、并发与竞态、幂等、错误语义、有限重试、资源释放、日志与指标以及回归测试。
|
|
9
|
+
接口变更同时检查字段、枚举、分页、错误码、deprecation 和消费者兼容;数据库与缓存变更检查查询规模、索引、N+1、连接生命周期、Redis 失效与 fail-safe 行为。
|
|
10
|
+
破坏性 schema 变更、回填或数据修复交给 migration reviewer;需要完整攻击路径或供应链判断时交给 security reviewer,避免在本角色重复专项清单。
|
|
9
11
|
不要修改文件,不要执行生产或数据写入,不要把缺少个人偏好的抽象当成问题。
|
|
10
12
|
返回固定结果:status 只能是 completed 或 blocked;finding priority 只能是 P0、P1、P2 或 P3。
|
|
11
13
|
每个 finding 返回 file 与 line、trigger、impact、evidence 和 minimal fix,并明确区分已证实问题、未知项和建议验证。
|
|
@@ -5,7 +5,8 @@ sandbox_mode = "read-only"
|
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
像前端代码所有者一样审查,但只报告有证据、可触发且值得修复的问题。
|
|
7
7
|
先读取适用的 AGENTS.md、规格、前端标准、设计系统约束和完整 diff,再检查必要的调用方与测试。
|
|
8
|
-
|
|
8
|
+
重点检查行为正确性、状态同步、竞态与取消、loading、error、empty 与 success 状态、键盘与屏幕阅读器可用性、组件职责、渲染成本以及测试缺口。
|
|
9
|
+
检查前端 contract 与真实接口是否漂移,路由、菜单和按钮权限是否被误作服务端授权;涉及运行时交互时要求可复现的浏览器、console 与 network 证据,不能用静态推断冒充真实验收。
|
|
9
10
|
不要修改文件,不要安装依赖,不要把纯风格偏好或无法证明影响的猜测列为 finding。
|
|
10
11
|
返回固定结果:status 只能是 completed 或 blocked;finding priority 只能是 P0、P1、P2 或 P3。
|
|
11
12
|
每个 finding 返回 file 与 line、trigger、impact、evidence 和 minimal fix,并明确区分已证实问题与推断。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name = "migration-reviewer"
|
|
2
|
+
description = "只读审查 schema migration、回填、数据修复、兼容切换与旧路径删除的安全性和可恢复性。"
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
sandbox_mode = "read-only"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
只做迁移审查,不修改文件,不执行数据库或数据写入,不连接生产环境。
|
|
7
|
+
先读取适用的 AGENTS.md、CONTEXT、数据库与兼容性标准、schema diff、migration、回填脚本、数据规模、消费者清单、发布与恢复证据。
|
|
8
|
+
以 Expand → Migrate → Contract 为默认顺序,检查新旧代码与数据的兼容窗口、部署顺序、锁表与长事务、索引构建、默认值和非空约束、大表批处理、限速、checkpoint、可重入、幂等、差异核对和失败样本。
|
|
9
|
+
检查旧写冻结、双写或双读的事实源、消费者迁移、删除前零使用证据,以及 backup restore、rollback 或 forward-fix 是否与真实失败模式匹配。
|
|
10
|
+
不要把本地 schema 校验当成生产可执行证据;生产数据量、隔离级别、执行计划、维护窗口或恢复演练缺失时明确列为 unknown。
|
|
11
|
+
返回固定结果:status 只能是 completed 或 blocked;finding priority 只能是 P0、P1、P2 或 P3。
|
|
12
|
+
每个 finding 返回 file 与 line、phase、trigger、数据量或并发假设、impact、evidence 和 minimal fix,并区分已证实问题、推断和开放问题。
|
|
13
|
+
没有可执行问题时显式返回 no findings;最后列出 unknowns、未覆盖消费者与未执行验证。
|
|
14
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name = "security-reviewer"
|
|
2
|
+
description = "只读审查应用、Agent 与供应链变更中的信任边界、权限、敏感数据、注入和特权工具风险。"
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
sandbox_mode = "read-only"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
只做安全审查,不修改文件,不执行外部写入、生产操作或攻击性探测。
|
|
7
|
+
先读取适用的 AGENTS.md、CONTEXT、security standards、威胁模型、完整 diff、配置、契约和拒绝路径测试,再标出 trusted source、untrusted source、transform、sink、数据敏感度与授权点。
|
|
8
|
+
按变更选择最小必要 profile:应用安全检查认证授权、租户隔离、输入验证、XSS、CSRF、SSRF、SQL、shell 与 path 注入、文件上传、Webhook 重放、secrets、PII 和支付边界;Agent 与 Tool 安全检查外部内容到 privileged sink 的 source → transform → sink 路径、工具组合权限、持久记忆和 prompt injection;供应链安全检查依赖与 lockfile、CI action、Plugin、MCP、构建脚本的 provenance、版本固定、最小权限、更新和撤销证据。
|
|
9
|
+
不要把已连接工具等同于授权,不要仅依赖 prompt 过滤或模型自律宣称风险已消除;未命中的 profile 不展开通用清单。
|
|
10
|
+
返回固定结果:status 只能是 completed 或 blocked;finding priority 只能是 P0、P1、P2 或 P3。
|
|
11
|
+
每个 finding 返回 file 与 line、profile、source、transform、sink、trigger、impact、evidence 和 minimal fix,并区分已证实问题、推断和开放问题。
|
|
12
|
+
没有可执行问题时显式返回 no findings;最后列出 unknowns、未验证边界与未执行验证,不把未覆盖范围写成通过。
|
|
13
|
+
"""
|
package/docs/agent-authoring.md
CHANGED
|
@@ -48,6 +48,21 @@ Agent 不应成为完整工作流、项目规范副本或长期人格。实现
|
|
|
48
48
|
|
|
49
49
|
只读 Agent 返回事实、可定位证据、推断、未知项和建议下一步。Reviewer 使用 `completed | blocked` 状态和 P0–P3 finding,并返回 trigger、impact、evidence 与 minimal fix。Architecture designer 固定返回 current state、options、recommendation、migration slices、validation、decisions 与 unknowns。Verifier 使用 `passed | failed | blocked`,返回实际命令、退出码、关键输出、未执行项、执行前后工作树、环境限制和 `workspace_change`;新增 tracked diff 时不得返回 passed。不得把建议写成已经验证的结论。
|
|
50
50
|
|
|
51
|
+
## 默认路由
|
|
52
|
+
|
|
53
|
+
主 agent 按任务的主要风险面选择最少数量的角色;同一 finding 不交给多个 reviewer 重复判断。Agent 不可用时由主 agent 串行执行相同检查,并保留相同证据与失败标准。
|
|
54
|
+
|
|
55
|
+
| 主任务类型 | Agent | 不应代替 |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| 模块 Interface、依赖方向、数据流与迁移方案比较 | `architecture-designer` | diff finding 或实现 |
|
|
58
|
+
| 后端、接口、事务、查询与缓存审查 | `backend-reviewer` | 专项安全或 migration 审查 |
|
|
59
|
+
| 前端状态、交互、可访问性与浏览器证据审查 | `frontend-reviewer` | 真实浏览器验收或服务端授权 |
|
|
60
|
+
| 应用、Agent、Tool 与供应链安全审查 | `security-reviewer` | 渗透测试或外部 mutation |
|
|
61
|
+
| schema、回填、数据修复与兼容切换审查 | `migration-reviewer` | 数据库写入或生产执行 |
|
|
62
|
+
| 执行已有测试、typecheck、lint 与 build | `test-verifier` | 测试设计审查或实现修复 |
|
|
63
|
+
|
|
64
|
+
普通代码定位继续使用内置 `explorer`,实现继续使用内置 `worker`。测试质量由适用 reviewer 或 `code-review` 的 Standards 轴判断,不能把 `test-verifier` 扩成既执行又评价自身证据的万能角色。
|
|
65
|
+
|
|
51
66
|
## 双宿主策略
|
|
52
67
|
|
|
53
68
|
Skills 保持 Codex 与 Claude Code 共用单源。Agent 配置是宿主专用适配,不强求两种宿主使用相同格式、模型名或权限语义。真正支持 Claude Code Agent 时,在独立宿主目录中实现并建立能力映射,不复制 Skill 工作流。
|
package/package.json
CHANGED
package/scripts/sync.mjs
CHANGED
|
@@ -24,6 +24,10 @@ const HOST_DESTINATIONS = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const CODEX_AGENT_DESTINATION = [".codex", "agents"];
|
|
27
|
+
const CURRENT_STATE_DESTINATION = [".agents", ".state", "skills-installer"];
|
|
28
|
+
const LEGACY_STATE_DESTINATION = [".netpilot-skills"];
|
|
29
|
+
const LAYOUT_LOCK_DIRECTORY = [".agents", ".state"];
|
|
30
|
+
const LAYOUT_LOCK_NAME = "skills-installer.layout.lock";
|
|
27
31
|
|
|
28
32
|
const ALL_HOSTS = Object.freeze(Object.keys(HOST_DESTINATIONS));
|
|
29
33
|
const ALL_COMPONENTS = Object.freeze(["skills", "agents"]);
|
|
@@ -354,6 +358,100 @@ async function assertSafeHomeRoot(homeDir) {
|
|
|
354
358
|
]);
|
|
355
359
|
}
|
|
356
360
|
|
|
361
|
+
function resolveStatePaths(homeDir) {
|
|
362
|
+
const currentStateDir = path.join(homeDir, ...CURRENT_STATE_DESTINATION);
|
|
363
|
+
const legacyStateDir = path.join(homeDir, ...LEGACY_STATE_DESTINATION);
|
|
364
|
+
return {
|
|
365
|
+
currentStateDir,
|
|
366
|
+
currentManifestPath: path.join(currentStateDir, "manifest.json"),
|
|
367
|
+
legacyStateDir,
|
|
368
|
+
legacyManifestPath: path.join(legacyStateDir, "manifest.json"),
|
|
369
|
+
layoutLockDir: path.join(homeDir, ...LAYOUT_LOCK_DIRECTORY),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function stateLayoutConflict(homeDir, reason, targetPath = homeDir) {
|
|
374
|
+
return new SyncConflictError([
|
|
375
|
+
{
|
|
376
|
+
host: "state",
|
|
377
|
+
component: "layout",
|
|
378
|
+
assetName: "state-layout",
|
|
379
|
+
skillName: "state-layout",
|
|
380
|
+
relativePath: ".",
|
|
381
|
+
targetPath,
|
|
382
|
+
action: "conflict",
|
|
383
|
+
reason,
|
|
384
|
+
},
|
|
385
|
+
]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function inspectStateDirectory(homeDir, targetPath) {
|
|
389
|
+
const ancestor = await inspectAncestorPath(homeDir, targetPath);
|
|
390
|
+
if (!ancestor.safe) {
|
|
391
|
+
throw stateLayoutConflict(
|
|
392
|
+
homeDir,
|
|
393
|
+
`${ancestor.reason}:${ancestor.path}`,
|
|
394
|
+
targetPath,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const stats = await lstat(targetPath);
|
|
399
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
400
|
+
throw stateLayoutConflict(
|
|
401
|
+
homeDir,
|
|
402
|
+
"状态路径必须是非符号链接目录",
|
|
403
|
+
targetPath,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
exists: true,
|
|
408
|
+
path: targetPath,
|
|
409
|
+
identity: { dev: stats.dev, ino: stats.ino },
|
|
410
|
+
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error.code === "ENOENT") return { exists: false, path: targetPath };
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function sameStateDirectoryIdentity(left, right) {
|
|
418
|
+
return left?.dev === right?.dev && left?.ino === right?.ino;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function inspectStateLayout(homeDir, paths = resolveStatePaths(homeDir)) {
|
|
422
|
+
const [current, legacy] = await Promise.all([
|
|
423
|
+
inspectStateDirectory(homeDir, paths.currentStateDir),
|
|
424
|
+
inspectStateDirectory(homeDir, paths.legacyStateDir),
|
|
425
|
+
]);
|
|
426
|
+
if (current.exists && legacy.exists) {
|
|
427
|
+
const legacyEntries = await readdir(legacy.path);
|
|
428
|
+
const latestLegacy = await inspectStateDirectory(homeDir, legacy.path);
|
|
429
|
+
if (
|
|
430
|
+
legacyEntries.length === 0 &&
|
|
431
|
+
latestLegacy.exists &&
|
|
432
|
+
sameStateDirectoryIdentity(legacy.identity, latestLegacy.identity)
|
|
433
|
+
) {
|
|
434
|
+
return {
|
|
435
|
+
kind: "current-with-empty-legacy",
|
|
436
|
+
current,
|
|
437
|
+
legacy: latestLegacy,
|
|
438
|
+
paths,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
throw stateLayoutConflict(
|
|
442
|
+
homeDir,
|
|
443
|
+
"新旧状态目录同时存在,拒绝自动合并 ownership",
|
|
444
|
+
paths.currentStateDir,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
kind: current.exists ? "current" : legacy.exists ? "legacy" : "fresh",
|
|
449
|
+
current,
|
|
450
|
+
legacy,
|
|
451
|
+
paths,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
357
455
|
function parseLockRecord(content) {
|
|
358
456
|
const trimmed = content.trim();
|
|
359
457
|
if (/^[1-9][0-9]*$/u.test(trimmed)) {
|
|
@@ -411,9 +509,13 @@ async function moveLockForCompare({ lockPath, expectedContent, stateDir, purpose
|
|
|
411
509
|
throw error;
|
|
412
510
|
}
|
|
413
511
|
|
|
414
|
-
async function acquireSyncLock({
|
|
415
|
-
|
|
416
|
-
|
|
512
|
+
async function acquireSyncLock({
|
|
513
|
+
homeDir,
|
|
514
|
+
stateDir,
|
|
515
|
+
lockName = "sync.lock",
|
|
516
|
+
beforeLockPublish,
|
|
517
|
+
}) {
|
|
518
|
+
let stateSafety = await inspectAncestorPath(homeDir, stateDir);
|
|
417
519
|
if (!stateSafety.safe) {
|
|
418
520
|
throw new SyncConflictError([
|
|
419
521
|
{
|
|
@@ -421,15 +523,24 @@ async function acquireSyncLock({ homeDir, stateDir, transactionHooks = {} }) {
|
|
|
421
523
|
component: "lock",
|
|
422
524
|
assetName: "sync",
|
|
423
525
|
skillName: "sync",
|
|
424
|
-
relativePath:
|
|
425
|
-
targetPath: path.join(stateDir,
|
|
526
|
+
relativePath: lockName,
|
|
527
|
+
targetPath: path.join(stateDir, lockName),
|
|
426
528
|
action: "conflict",
|
|
427
529
|
reason: `${stateSafety.reason}:${stateSafety.path}`,
|
|
428
530
|
},
|
|
429
531
|
]);
|
|
430
532
|
}
|
|
533
|
+
await mkdir(stateDir, { recursive: true });
|
|
534
|
+
stateSafety = await inspectAncestorPath(homeDir, stateDir);
|
|
535
|
+
if (!stateSafety.safe) {
|
|
536
|
+
throw stateLayoutConflict(
|
|
537
|
+
homeDir,
|
|
538
|
+
`${stateSafety.reason}:${stateSafety.path}`,
|
|
539
|
+
stateDir,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
431
542
|
|
|
432
|
-
const lockPath = path.join(stateDir,
|
|
543
|
+
const lockPath = path.join(stateDir, lockName);
|
|
433
544
|
const owner = randomUUID();
|
|
434
545
|
const recordContent = `${JSON.stringify({
|
|
435
546
|
schemaVersion: 1,
|
|
@@ -437,7 +548,10 @@ async function acquireSyncLock({ homeDir, stateDir, transactionHooks = {} }) {
|
|
|
437
548
|
owner,
|
|
438
549
|
createdAt: new Date().toISOString(),
|
|
439
550
|
})}\n`;
|
|
440
|
-
const stagedLockPath = path.join(
|
|
551
|
+
const stagedLockPath = path.join(
|
|
552
|
+
stateDir,
|
|
553
|
+
`${lockName.replaceAll(".", "-")}-${owner}.tmp`,
|
|
554
|
+
);
|
|
441
555
|
await writeFile(stagedLockPath, recordContent, { encoding: "utf8", flag: "wx" });
|
|
442
556
|
if ((await readFile(stagedLockPath, "utf8")) !== recordContent) {
|
|
443
557
|
await rm(stagedLockPath, { force: true });
|
|
@@ -445,7 +559,7 @@ async function acquireSyncLock({ homeDir, stateDir, transactionHooks = {} }) {
|
|
|
445
559
|
}
|
|
446
560
|
|
|
447
561
|
try {
|
|
448
|
-
await
|
|
562
|
+
await beforeLockPublish?.();
|
|
449
563
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
450
564
|
try {
|
|
451
565
|
await link(stagedLockPath, lockPath);
|
|
@@ -1303,6 +1417,111 @@ async function applyOperationsTransactional({
|
|
|
1303
1417
|
}
|
|
1304
1418
|
}
|
|
1305
1419
|
|
|
1420
|
+
async function migrateLegacyState({
|
|
1421
|
+
homeDir,
|
|
1422
|
+
paths,
|
|
1423
|
+
transactionHooks,
|
|
1424
|
+
}) {
|
|
1425
|
+
let syncLock = await acquireSyncLock({
|
|
1426
|
+
homeDir,
|
|
1427
|
+
stateDir: paths.legacyStateDir,
|
|
1428
|
+
beforeLockPublish: transactionHooks.beforeLockPublish,
|
|
1429
|
+
});
|
|
1430
|
+
let moved = false;
|
|
1431
|
+
try {
|
|
1432
|
+
const freshLayout = await inspectStateLayout(homeDir, paths);
|
|
1433
|
+
if (freshLayout.kind !== "legacy") {
|
|
1434
|
+
throw stateLayoutConflict(
|
|
1435
|
+
homeDir,
|
|
1436
|
+
"获取旧状态锁后布局发生变化,拒绝继续迁移",
|
|
1437
|
+
paths.legacyStateDir,
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
const lockedSourceIdentity = freshLayout.legacy.identity;
|
|
1441
|
+
const legacyManifest = await inspectTarget(paths.legacyManifestPath);
|
|
1442
|
+
if (!legacyManifest.exists || !legacyManifest.regularFile) {
|
|
1443
|
+
throw stateLayoutConflict(
|
|
1444
|
+
homeDir,
|
|
1445
|
+
"旧状态目录缺少可信的普通 manifest.json",
|
|
1446
|
+
paths.legacyManifestPath,
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
await readManifest(paths.legacyManifestPath);
|
|
1450
|
+
await transactionHooks.beforeStateMigrationRename?.();
|
|
1451
|
+
const beforeRename = await inspectStateLayout(homeDir, paths);
|
|
1452
|
+
if (
|
|
1453
|
+
beforeRename.kind !== "legacy" ||
|
|
1454
|
+
!sameStateDirectoryIdentity(
|
|
1455
|
+
lockedSourceIdentity,
|
|
1456
|
+
beforeRename.legacy.identity,
|
|
1457
|
+
)
|
|
1458
|
+
) {
|
|
1459
|
+
throw stateLayoutConflict(
|
|
1460
|
+
homeDir,
|
|
1461
|
+
"旧状态目录身份在迁移提交前发生变化",
|
|
1462
|
+
paths.currentStateDir,
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
try {
|
|
1466
|
+
await rename(paths.legacyStateDir, paths.currentStateDir);
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
if (error.code === "EXDEV") {
|
|
1469
|
+
throw stateLayoutConflict(
|
|
1470
|
+
homeDir,
|
|
1471
|
+
"新旧状态目录不在同一文件系统,拒绝退化为非原子复制",
|
|
1472
|
+
paths.currentStateDir,
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
if (["EEXIST", "ENOTEMPTY"].includes(error.code)) {
|
|
1476
|
+
throw stateLayoutConflict(
|
|
1477
|
+
homeDir,
|
|
1478
|
+
"新状态目录在原子迁移前已出现",
|
|
1479
|
+
paths.currentStateDir,
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
throw error;
|
|
1483
|
+
}
|
|
1484
|
+
moved = true;
|
|
1485
|
+
syncLock = {
|
|
1486
|
+
...syncLock,
|
|
1487
|
+
stateDir: paths.currentStateDir,
|
|
1488
|
+
lockPath: path.join(paths.currentStateDir, "sync.lock"),
|
|
1489
|
+
};
|
|
1490
|
+
const migratedLayout = await inspectStateLayout(homeDir, paths);
|
|
1491
|
+
if (
|
|
1492
|
+
migratedLayout.kind !== "current" ||
|
|
1493
|
+
!sameStateDirectoryIdentity(
|
|
1494
|
+
lockedSourceIdentity,
|
|
1495
|
+
migratedLayout.current.identity,
|
|
1496
|
+
)
|
|
1497
|
+
) {
|
|
1498
|
+
const error = new Error("状态目录迁移后的目标身份不一致");
|
|
1499
|
+
error.preserveEvidence = true;
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
const migratedManifest = await inspectTarget(paths.currentManifestPath);
|
|
1503
|
+
if (
|
|
1504
|
+
!migratedManifest.regularFile ||
|
|
1505
|
+
migratedManifest.hash !== legacyManifest.hash
|
|
1506
|
+
) {
|
|
1507
|
+
const error = new Error("状态目录迁移后 manifest digest 不一致");
|
|
1508
|
+
error.preserveEvidence = true;
|
|
1509
|
+
throw error;
|
|
1510
|
+
}
|
|
1511
|
+
await transactionHooks.afterStateMigrationRename?.();
|
|
1512
|
+
return syncLock;
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
try {
|
|
1515
|
+
await releaseSyncLock(syncLock);
|
|
1516
|
+
} catch (releaseError) {
|
|
1517
|
+
error.releaseError = releaseError;
|
|
1518
|
+
error.preserveEvidence = true;
|
|
1519
|
+
}
|
|
1520
|
+
if (moved) error.stateMigrationCommitted = true;
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1306
1525
|
export async function syncSkills({
|
|
1307
1526
|
rootDir,
|
|
1308
1527
|
homeDir,
|
|
@@ -1329,14 +1548,114 @@ export async function syncSkills({
|
|
|
1329
1548
|
const resolvedRoot = path.resolve(rootDir);
|
|
1330
1549
|
const resolvedHome = path.resolve(homeDir);
|
|
1331
1550
|
await assertSafeHomeRoot(resolvedHome);
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1551
|
+
const statePaths = resolveStatePaths(resolvedHome);
|
|
1552
|
+
let stateLayout = await inspectStateLayout(resolvedHome, statePaths);
|
|
1553
|
+
let stateDir =
|
|
1554
|
+
stateLayout.kind === "legacy"
|
|
1555
|
+
? statePaths.legacyStateDir
|
|
1556
|
+
: statePaths.currentStateDir;
|
|
1557
|
+
let manifestPath =
|
|
1558
|
+
stateLayout.kind === "legacy"
|
|
1559
|
+
? statePaths.legacyManifestPath
|
|
1560
|
+
: statePaths.currentManifestPath;
|
|
1561
|
+
let stateMigration =
|
|
1562
|
+
stateLayout.kind === "legacy"
|
|
1563
|
+
? {
|
|
1564
|
+
action: "move",
|
|
1565
|
+
from: statePaths.legacyStateDir,
|
|
1566
|
+
to: statePaths.currentStateDir,
|
|
1567
|
+
}
|
|
1568
|
+
: null;
|
|
1569
|
+
let stateRepair =
|
|
1570
|
+
stateLayout.kind === "current-with-empty-legacy"
|
|
1571
|
+
? {
|
|
1572
|
+
action: "remove-empty-legacy-state",
|
|
1573
|
+
target: statePaths.legacyStateDir,
|
|
1574
|
+
}
|
|
1575
|
+
: null;
|
|
1576
|
+
let layoutLock = null;
|
|
1577
|
+
let syncLock = null;
|
|
1337
1578
|
let syncError = null;
|
|
1338
1579
|
|
|
1339
1580
|
try {
|
|
1581
|
+
if (apply) {
|
|
1582
|
+
layoutLock = await acquireSyncLock({
|
|
1583
|
+
homeDir: resolvedHome,
|
|
1584
|
+
stateDir: statePaths.layoutLockDir,
|
|
1585
|
+
lockName: LAYOUT_LOCK_NAME,
|
|
1586
|
+
beforeLockPublish: transactionHooks.beforeLayoutLockPublish,
|
|
1587
|
+
});
|
|
1588
|
+
stateLayout = await inspectStateLayout(resolvedHome, statePaths);
|
|
1589
|
+
if (stateLayout.kind === "current-with-empty-legacy") {
|
|
1590
|
+
stateRepair = {
|
|
1591
|
+
action: "remove-empty-legacy-state",
|
|
1592
|
+
target: statePaths.legacyStateDir,
|
|
1593
|
+
};
|
|
1594
|
+
const expectedCurrentIdentity = stateLayout.current.identity;
|
|
1595
|
+
try {
|
|
1596
|
+
await rmdir(statePaths.legacyStateDir);
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
if (["ENOTEMPTY", "EEXIST"].includes(error.code)) {
|
|
1599
|
+
throw stateLayoutConflict(
|
|
1600
|
+
resolvedHome,
|
|
1601
|
+
"空旧状态目录在清理前出现内容,拒绝继续",
|
|
1602
|
+
statePaths.legacyStateDir,
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
const repairedLayout = await inspectStateLayout(
|
|
1608
|
+
resolvedHome,
|
|
1609
|
+
statePaths,
|
|
1610
|
+
);
|
|
1611
|
+
if (
|
|
1612
|
+
repairedLayout.kind !== "current" ||
|
|
1613
|
+
!sameStateDirectoryIdentity(
|
|
1614
|
+
expectedCurrentIdentity,
|
|
1615
|
+
repairedLayout.current.identity,
|
|
1616
|
+
)
|
|
1617
|
+
) {
|
|
1618
|
+
throw stateLayoutConflict(
|
|
1619
|
+
resolvedHome,
|
|
1620
|
+
"清理旧状态残留后当前状态目录身份发生变化",
|
|
1621
|
+
statePaths.currentStateDir,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
stateLayout = repairedLayout;
|
|
1625
|
+
}
|
|
1626
|
+
if (stateLayout.kind === "legacy") {
|
|
1627
|
+
stateMigration = {
|
|
1628
|
+
action: "move",
|
|
1629
|
+
from: statePaths.legacyStateDir,
|
|
1630
|
+
to: statePaths.currentStateDir,
|
|
1631
|
+
};
|
|
1632
|
+
syncLock = await migrateLegacyState({
|
|
1633
|
+
homeDir: resolvedHome,
|
|
1634
|
+
paths: statePaths,
|
|
1635
|
+
transactionHooks,
|
|
1636
|
+
});
|
|
1637
|
+
} else {
|
|
1638
|
+
syncLock = await acquireSyncLock({
|
|
1639
|
+
homeDir: resolvedHome,
|
|
1640
|
+
stateDir: statePaths.currentStateDir,
|
|
1641
|
+
beforeLockPublish: transactionHooks.beforeLockPublish,
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
const lockedLayout = await inspectStateLayout(
|
|
1645
|
+
resolvedHome,
|
|
1646
|
+
statePaths,
|
|
1647
|
+
);
|
|
1648
|
+
if (lockedLayout.kind !== "current") {
|
|
1649
|
+
throw stateLayoutConflict(
|
|
1650
|
+
resolvedHome,
|
|
1651
|
+
"获取同步锁后项目状态布局发生变化,拒绝继续",
|
|
1652
|
+
statePaths.currentStateDir,
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
stateLayout = lockedLayout;
|
|
1656
|
+
stateDir = statePaths.currentStateDir;
|
|
1657
|
+
manifestPath = statePaths.currentManifestPath;
|
|
1658
|
+
}
|
|
1340
1659
|
if (syncLock) await transactionHooks.afterLockAcquired?.();
|
|
1341
1660
|
const stateSafety = await inspectAncestorPath(resolvedHome, stateDir);
|
|
1342
1661
|
if (!stateSafety.safe) {
|
|
@@ -1468,7 +1787,12 @@ export async function syncSkills({
|
|
|
1468
1787
|
const result = {
|
|
1469
1788
|
mode: apply ? "apply" : "dry-run",
|
|
1470
1789
|
homeDir: resolvedHome,
|
|
1471
|
-
manifestPath,
|
|
1790
|
+
manifestPath: statePaths.currentManifestPath,
|
|
1791
|
+
...(manifestPath !== statePaths.currentManifestPath
|
|
1792
|
+
? { sourceManifestPath: manifestPath }
|
|
1793
|
+
: {}),
|
|
1794
|
+
...(stateMigration ? { stateMigration } : {}),
|
|
1795
|
+
...(stateRepair ? { stateRepair } : {}),
|
|
1472
1796
|
operations,
|
|
1473
1797
|
conflicts,
|
|
1474
1798
|
};
|
|
@@ -1488,18 +1812,21 @@ export async function syncSkills({
|
|
|
1488
1812
|
syncError = error;
|
|
1489
1813
|
throw error;
|
|
1490
1814
|
} finally {
|
|
1491
|
-
|
|
1815
|
+
let releaseFailure = null;
|
|
1816
|
+
for (const lock of [syncLock, layoutLock]) {
|
|
1817
|
+
if (!lock) continue;
|
|
1492
1818
|
try {
|
|
1493
|
-
await releaseSyncLock(
|
|
1819
|
+
await releaseSyncLock(lock);
|
|
1494
1820
|
} catch (releaseError) {
|
|
1495
1821
|
if (syncError) {
|
|
1496
1822
|
syncError.releaseError = releaseError;
|
|
1497
1823
|
syncError.preserveEvidence = true;
|
|
1498
1824
|
} else {
|
|
1499
|
-
|
|
1825
|
+
releaseFailure ??= releaseError;
|
|
1500
1826
|
}
|
|
1501
1827
|
}
|
|
1502
1828
|
}
|
|
1829
|
+
if (!syncError && releaseFailure) throw releaseFailure;
|
|
1503
1830
|
}
|
|
1504
1831
|
}
|
|
1505
1832
|
|
|
@@ -1518,6 +1845,16 @@ Claude Code 目标:~/.claude/skills(选择 --host claude 时默认只安装
|
|
|
1518
1845
|
}
|
|
1519
1846
|
|
|
1520
1847
|
function printPlan(result) {
|
|
1848
|
+
if (result.stateRepair) {
|
|
1849
|
+
console.log(
|
|
1850
|
+
`[remove-empty] state ${result.stateRepair.target}`,
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
if (result.stateMigration) {
|
|
1854
|
+
console.log(
|
|
1855
|
+
`[move] state ${result.stateMigration.from} -> ${result.stateMigration.to}`,
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1521
1858
|
for (const operation of result.operations) {
|
|
1522
1859
|
const reason = operation.reason ? `(${operation.reason})` : "";
|
|
1523
1860
|
const component = operation.component === "skills" ? "skills" : operation.component;
|