cordis-plugin-sofagent-rollback
出错逆序撤销——git snapshot → effect disposer(seam: agent/error)——桥接 @sofagent/core getHistoryFilePath——DSH(DeepSeek Harness)cordis plugin。sofagent 约束层在 DeepSeek Harness 生态的插件形态。
Install
npx skills add https://github.com/KongFangXun/sofagent/tree/main/engine/dsh-plugins/cordis-plugin-sofagent-rollback
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kongfangxun-sofagent@llmmart
git clone https://github.com/KongFangXun/sofagent.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole kongfangxun/sofagent collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
cordis-plugin-sofagent-rollback
出错逆序撤销——git snapshot → effect disposer(seam: agent/error)——桥接 @sofagent/core getHistoryFilePath
用途
装上之后:Agent 出错时按 git 快照逆序撤销本次会话改动。什么时候用:Agent 一次动了大量文件、需要一键回到干净状态。
接入点(seam: agent/error):桥接 @sofagent/core,缺依赖时该能力静默跳过;接入形态(声明 / 实现)见 SEAMS.md。
本插件随 sofagent 主线版本发布(SkillHub 通道:skillhub install cordis-plugin-sofagent-rollback 安装与检索;npm 通道未开通)。版本号与 sofagent 主线对齐。
相关链接
- sofagent 主仓:https://github.com/KongFangXun/sofagent
- 开发日志:docs/changelog/v1.4/v1.4.0.md(DSH 插件家族)
Files (sofagent)
-
src
-
index.test.ts 7.6 KB
// ============================================================ // cordis-plugin-rollback · 插件单测(v1.4.0 交付五 · v1.5.0 章十补接线面) // ============================================================ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import pluginDefault, { pluginMeta, capability, invoke } from './index'; import { listSnapshots, restoreSnapshot } from '@sofagent/core'; // 章十:快照两源(listSnapshots → restoreSnapshot)打桩成确定性替身—— // 接线正确性 = 定位口径(追加序取末条)与透传参数,与引擎内部实现无关。 vi.mock('@sofagent/core', () => ({ listSnapshots: vi.fn(() => [{ sha: 'sha-latest' }]), restoreSnapshot: vi.fn(async () => ['src/a.ts']), })); // 必要的 schemastery 替身:settings.register 分支的 require 会拿它构造 shape——缺席走 catch 降级 vi.mock('@deepseek-ai/schemastery', () => ({ boolean: () => ({ __type: 'boolean' }), string: () => ({ __type: 'string' }), object: (shape: unknown) => ({ __type: 'object', shape }), })); describe('cordis-plugin-sofagent-rollback', () => { it('插件元数据完整(id/version/description/seam)', () => { expect(pluginMeta.id).toBe('cordis-plugin-sofagent-rollback'); expect(pluginMeta.version).toBe(require('../package.json').version); // SSOT 对齐:版本跟随主版本(读 package.json) expect(pluginMeta.description.length).toBeGreaterThan(10); expect(pluginMeta.seam.length).toBeGreaterThan(0); }); it('能力说明非空', () => { expect(capability.length).toBeGreaterThan(0); }); it('invoke 可调用(成功或降级,不挂死)', async () => { const r = await invoke().catch((e) => e); expect(r).toBeDefined(); }); }); // ───────────────────────────────────────────────────────────────────────────── // v1.5.0 章十 · seam 事件接线(seam 从声明到实现) // 断言面:① 订阅真实存在 ② disposer 收进复合卸载契约 // ③ **默认关档不动作**(破坏性动作不得因接线而改变既有行为) // ④ 显式开档才走 listSnapshots → restoreSnapshot(追加序取末条 = 最新) // ⑤ 无快照 / 恢复失败 → 零动作、不抛(fail-open,不阻断宿主错误处理) // ───────────────────────────────────────────────────────────────────────────── describe('章十 · seam 事件接线', () => { const MANIFEST = require('../../plugins.json') as { plugins: Array<{ id: string; seamHandlers?: string[]; bridges?: Array<{ pkg: string; api: string }>; }>; }; const entry = MANIFEST.plugins.find((p) => p.id === 'cordis-plugin-sofagent-rollback'); const declared: string[] = entry?.seamHandlers ?? []; const ONE = ['agent/error']; const errSpy = () => vi.spyOn(console, 'error').mockImplementation(() => {}); beforeEach(() => { vi.clearAllMocks(); process.env.SOFAGENT_PROJECT_ROOT = '/proj'; vi.mocked(listSnapshots).mockImplementation(() => [{ sha: 'sha-latest' }]); vi.mocked(restoreSnapshot).mockImplementation(async () => ['src/a.ts']); }); afterEach(() => { vi.restoreAllMocks(); delete process.env.SOFAGENT_PROJECT_ROOT; }); /** * 宿主 ctx 替身。 * @param flag 档位值——`'true'` 显式开档;`'false'` 显式关档;`undefined` 走声明默认值(关) */ function hostCtx(flag?: string) { const subscribed = new Map<string, (...a: unknown[]) => unknown>(); const disposed: string[] = []; const on = vi.fn((event: string, handler: (...a: unknown[]) => unknown) => { subscribed.set(event, handler); return () => { disposed.push(event); }; }); const settings = flag === undefined ? undefined : { register: vi.fn(() => ({ get: () => ({ rollbackOnError: flag }), watch: () => () => undefined, })), }; return { ctx: { provide: vi.fn(() => () => undefined), on, settings }, subscribed, disposed }; } const errorPayload = () => ({ turn: 2, step: 3, error: new Error('agent 崩了') }); it('seamHandlers 与 plugins.json 声明同集合(三处对账的插件侧)', () => { expect(declared.slice().sort()).toEqual([...ONE].sort()); }); it('bridges 声明覆盖快照两源(listSnapshots / restoreSnapshot)', () => { const keys = (entry?.bridges ?? []).map((b) => `${b.pkg}.${b.api}`).sort(); expect(keys).toEqual(['@sofagent/core.listSnapshots', '@sofagent/core.restoreSnapshot'].sort()); }); it('apply 经 ctx.on 订阅 agent/error;复合 disposer 逐个反注册', async () => { const spy = errSpy(); const { ctx, subscribed, disposed } = hostCtx(); const disposer = (pluginDefault.apply as (c: unknown) => unknown)(ctx) as | (() => Promise<void>) | undefined; expect([...subscribed.keys()].sort()).toEqual([...ONE].sort()); expect(typeof disposer).toBe('function'); await disposer!(); expect(disposed.slice().sort()).toEqual([...ONE].sort()); spy.mockRestore(); }); it('默认关档(无 settings 面):订阅在位但零动作——接线不改变既有行为', async () => { const spy = errSpy(); const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); await expect(subscribed.get('agent/error')!(errorPayload())).resolves.toBeUndefined(); expect(listSnapshots).not.toHaveBeenCalled(); expect(restoreSnapshot).not.toHaveBeenCalled(); spy.mockRestore(); }); it("显式关档(rollbackOnError = 'false'):有快照也不撤销", async () => { const spy = errSpy(); const { ctx, subscribed } = hostCtx('false'); (pluginDefault.apply as (c: unknown) => unknown)(ctx); await subscribed.get('agent/error')!(errorPayload()); expect(listSnapshots).not.toHaveBeenCalled(); expect(restoreSnapshot).not.toHaveBeenCalled(); spy.mockRestore(); }); it('显式开档:取追加序末条(最新)快照恢复,参数逐字透传', async () => { const spy = errSpy(); vi.mocked(listSnapshots).mockImplementation(() => [{ sha: 'sha-old' }, { sha: 'sha-latest' }]); const { ctx, subscribed } = hostCtx('true'); (pluginDefault.apply as (c: unknown) => unknown)(ctx); await subscribed.get('agent/error')!(errorPayload()); expect(listSnapshots).toHaveBeenCalledWith('/proj'); expect(restoreSnapshot).toHaveBeenCalledWith('/proj', 'sha-latest'); // 末尾即最新 spy.mockRestore(); }); it('显式开档但无可用快照:不撤销(先跑一次审计才会建快照)', async () => { const spy = errSpy(); vi.mocked(listSnapshots).mockImplementation(() => []); const { ctx, subscribed } = hostCtx('true'); (pluginDefault.apply as (c: unknown) => unknown)(ctx); await subscribed.get('agent/error')!(errorPayload()); expect(restoreSnapshot).not.toHaveBeenCalled(); spy.mockRestore(); }); it('显式开档但恢复失败:不抛(fail-open,不阻断宿主错误处理)', async () => { const spy = errSpy(); vi.mocked(restoreSnapshot).mockImplementation(async () => { throw new Error('快照损坏'); }); const { ctx, subscribed } = hostCtx('true'); (pluginDefault.apply as (c: unknown) => unknown)(ctx); await expect(subscribed.get('agent/error')!(errorPayload())).resolves.toBeUndefined(); spy.mockRestore(); }); }); -
index.ts 4.2 KB
// cordis-plugin-sofagent-rollback · DSH 反向插件(v1.5.2:98 行样板收敛到 @sofagent/dsh-plugin-kit) // seam 挂载:agent/error # 语义:Agent 出错 → git snapshot 逆序撤销(默认关档) // 清单生成源 = engine/dsh-plugins/plugins.json(生成 package.json 的 description/sofagent/dsh 段与 cordis.patch.yml);本文件的 seam 字面量由生成器 --check 与之对账。 import { createSofagentPlugin, seamHelpers, type SeamHandler, type SeamHelpers, } from '@sofagent/dsh-plugin-kit'; /** 一次性日志表(接线自证 / 降级提示只打一次) */ const logged = new Set<string>(); function logOnce(helpers: SeamHelpers, key: string, message: string): void { if (logged.has(key)) return; logged.add(key); helpers.log(message); } const errMsg = (err: unknown): string => (err instanceof Error ? err.message : String(err)); /** 项目根:显式环境变量优先,缺省取宿主进程工作目录 */ const projectRoot = (): string => process.env.SOFAGENT_PROJECT_ROOT ?? process.cwd(); /** * seam 事件处理器(v1.5.0 章十)——`agent/error` 的**实现**(此前只有声明)。 * * 宿主契约:emit 派发,listener 约定 `(payload)`,payload 含 `{ agent, turn, step, error }`; * 返回值无意义(观察位)。 * * 🔴 **默认关档**(settings `rollbackOnError = 'false'`):自动撤销工作区是破坏性 * 动作,接线本身不应改变既有行为——要恢复就必须显式开档。关档时本事件位仍 * 真实订阅(装载状态可自证),只是不动作。 * 🔴 判定逻辑零改动:快照定位与恢复都是既有 @public API(`listSnapshots` / * `restoreSnapshot`),插件只做串联;快照序为追加序,「末尾即最新」。 */ const seamHandlers: Record<string, SeamHandler> = { 'agent/error': async (...args: unknown[]) => { const helpers = seamHelpers(args); if (helpers.flags().rollbackOnError !== true) { logOnce( helpers, 'error-rollback-disarmed', 'Agent 出错事件已接线;自动回滚档位关闭(rollbackOnError=false)——不撤销工作区(默认关,避免误伤未提交变更)', ); return; } try { const root = projectRoot(); const snapshots = await helpers.call('@sofagent/core', 'listSnapshots', root); if (!Array.isArray(snapshots) || snapshots.length === 0) { logOnce(helpers, 'error-no-snapshot', 'Agent 出错但无可用快照——不撤销(先跑一次审计才会建快照)'); return; } const latest = snapshots[snapshots.length - 1] as { sha?: unknown }; if (typeof latest?.sha !== 'string' || latest.sha === '') return; const restored = await helpers.call('@sofagent/core', 'restoreSnapshot', root, latest.sha); helpers.log( `出错逆序撤销:已恢复到快照 ${latest.sha.slice(0, 8)}(${Array.isArray(restored) ? restored.length : 0} 个文件)`, ); } catch (err) { logOnce(helpers, 'error-rollback-failed', `快照回滚失败(不阻断宿主错误处理):${errMsg(err)}`); } }, }; /** 插件声明(本文件唯一手写处;适配层红线由 kit 承担:ctx 鸭子类型 + 宿主 API 缺席降级不抛) */ const kit = createSofagentPlugin( { id: 'cordis-plugin-sofagent-rollback', seam: 'agent/error', seamSemantics: 'Agent 出错 → git snapshot 逆序撤销(默认关档,需显式开 rollbackOnError)', capability: '快照回溯(出事一键回滚)', bridgePkg: '@sofagent/core', bridgeApi: 'restoreSnapshot', description: '出错逆序撤销——git snapshot → effect disposer', // 默认关:自动撤销工作区是破坏性动作,接线不改变既有行为 settingsExtra: { rollbackOnError: 'false' }, seamHandlers, }, require('../package.json') as { version?: string }, ); export const pluginMeta = kit.pluginMeta; // 插件元数据(DSH profile/注册表消费) export const capability = kit.capability; // 依赖的 sofagent 能力说明(DSH skill 引导链展示) export const invoke = kit.invoke; // 桥接 @sofagent/* 公共 API(懒加载 + 降级不抛) export default kit.plugin; // DSH Cordis 插件契约(apply 三段式由 kit 提供)
-
-
cordis.patch.yml 558 B
# sofagent cordis-plugin-sofagent-rollback bundle patch v1.5.2——注册为 DSH profile layer - insert: - id: sofagent-rollback name: 'cordis-plugin-sofagent-rollback' inject: [settings, dynamicCordisRunner] config: seam: "agent/error" # 语义:Agent 出错 → git snapshot 逆序撤销(默认关档,需显式开 rollbackOnError) description: "给宿主加上快照回溯——Agent 出错时按 git 快照逆序撤销本次改动——桥接 @sofagent/core restoreSnapshot / @sofagent/core listSnapshots" -
package.json 1.6 KB
{ "name": "cordis-plugin-sofagent-rollback", "version": "1.5.2", "engines": { "node": ">=18" }, "description": "给宿主加上快照回溯——Agent 出错时按 git 快照逆序撤销本次改动(seam: agent/error)——桥接 @sofagent/core restoreSnapshot / @sofagent/core listSnapshots", "license": "MIT", "author": "KongFangXun", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "test": "vitest run src/index.test.ts" }, "keywords": [ "sofagent", "dsh", "cordis-plugin" ], "sofagent": { "type": "dsh-plugin", "family": "cordis", "seam": "agent/error", "seamSemantics": "Agent 出错 → git snapshot 逆序撤销(默认关档,需显式开 rollbackOnError)", "seamHandlers": [ "agent/error" ] }, "//optionalDependencies": "v1.4.5 T6 (R4):src/index.ts 惰性 await import(懒加载 + 缺依赖降级不抛;v1.4.8 起该样板由 @sofagent/dsh-plugin-kit 统一封装;v1.4.9 P2 起厚插件多包 bridges 按序解析、部分可用即部分成功),此前未声明任何依赖——对齐 root package.json F-18 optionalDependencies 先例。", "optionalDependencies": { "@sofagent/core": "1.5.2" }, "devDependencies": { "typescript": "^7.0.2", "vitest": "^5.0.0" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }, "files": [ "dist/", "cordis.patch.yml", "SKILL.md", "!dist/**/*.js.map", "!dist/**/*.d.ts.map", "!dist/**/*.test.*" ], "dependencies": { "@sofagent/dsh-plugin-kit": "1.5.2" } } -
SKILL.md 1.2 KB
--- name: cordis-plugin-sofagent-rollback slug: cordis-plugin-sofagent-rollback version: 1.5.2 displayName: cordis-plugin-sofagent-rollback description: > 出错逆序撤销——git snapshot → effect disposer(seam: agent/error)——桥接 @sofagent/core getHistoryFilePath——DSH(DeepSeek Harness)cordis plugin。sofagent 约束层在 DeepSeek Harness 生态的插件形态。 --- # cordis-plugin-sofagent-rollback 出错逆序撤销——git snapshot → effect disposer(seam: agent/error)——桥接 @sofagent/core getHistoryFilePath ## 用途 **装上之后**:Agent 出错时按 git 快照逆序撤销本次会话改动。**什么时候用**:Agent 一次动了大量文件、需要一键回到干净状态。 **接入点**(seam: agent/error):桥接 `@sofagent/core`,缺依赖时该能力静默跳过;接入形态(声明 / 实现)见 [SEAMS.md](../SEAMS.md)。 本插件随 sofagent 主线版本发布(SkillHub 通道:`skillhub install cordis-plugin-sofagent-rollback` 安装与检索;npm 通道未开通)。版本号与 sofagent 主线对齐。 ## 相关链接 - sofagent 主仓:https://github.com/KongFangXun/sofagent - 开发日志:docs/changelog/v1.4/v1.4.0.md(DSH 插件家族) -
tsconfig.json 185 B
{ "extends": "../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": [ "src" ], "exclude": [ "src/**/*.test.ts" ] }
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.