xitto-kernel 0.3.5 → 0.3.8

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.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8
4
+
5
+ - **技能自我維護(用量戳記 + 漂移偵測)**:結晶後不再靜止,技能庫會自我體檢。
6
+ - **A 用量戳記**:`skill` 載入時記 `usedCount` / `lastUsedAt`(寫進 frontmatter,不動 body)
7
+ - **B 漂移偵測**:新增 `skills_check` 工具 + `api.skills.check()`——重跑每個技能存的 verify,
8
+ 仍 exit 0 標 `ok`、失效標 `stale`(清/設 frontmatter);無 verify 區塊的技能回 `no-verify`(不誤判)
9
+ - prompt 與 `/skills` 清單顯示用量與 `⚠ 已失效待修`;`/skills check` 觸發複查
10
+ - frontmatter 簡易解析/patch(splitFront/joinFront/extractVerify);複用 v0.3.7 存下的 verify
11
+ - 4 個新測試(用量累加 / ok→stale→修復 / no-verify 不誤判 / api.skills.check)+ 真實 verify 端到端
12
+ (載入計次、刪檔→stale、復原→ok)。測試 143/143。
13
+
14
+ ## 0.3.7
15
+
16
+ - **技能結晶政策閘門(驗證才算數)**:每個自寫技能新增時必須有明確目標 + 通過的驗證,否則不落地。
17
+ - `skill_save` 新增必填 `goal`(明確目標)與 `verify`(驗證指令)
18
+ - **verify 在沙箱實際執行,exit 0 才新增**;未過則拒絕並回傳輸出供 agent 修正
19
+ - 危險驗證指令一律擋(複用 `dangerousReason`/`sandboxViolation`);開沙箱時 Seatbelt 包執行
20
+ - 落地檔記 `goal`/`verified: true`/`verifiedAt` + `## 驗證(已通過)` 區塊(為日後重驗/衰減鋪路)
21
+ - kernel 新增 `runVerify`(注入 createSkills);確保結晶的是「已驗證的成功」而非「宣稱的成功」
22
+ - 7 個測試 + 真實 runVerify 端到端(通過→新增 / 失敗→拒絕 / 危險→擋 / 缺 goal→拒)
23
+
24
+ ## 0.3.6
25
+
26
+ - **自我結晶技能(結晶層)**:agent 摸出可重複流程時自己寫成技能,跨任務/跨 session 複用。
27
+ - 新增 kernel 內建工具 `skill_save`(把流程結晶成 `.xitto-kernel/<pack>/skills/<name>.md`,含 frontmatter description)
28
+ - `skill` 載入改為**熱掃描**:本 session 剛結晶的技能即時可載;未來 session 自動列入「可用技能」
29
+ - `skill`/`skill_save` **永遠可用**(即使尚無技能,才能結晶第一個);name slug 化防路徑穿越,同名覆蓋
30
+ - 漸進揭露不變:prompt 只列名稱+簡述,需要時才載全文
31
+ - `/skills`(查看)、`/skills forget <名>`;`api.skills.{list,remove,reload,path}`
32
+ - 6 個測試 + 真實 model 端到端閉環(結晶 → 落地 → 新 session 列出並載入全文)
33
+ - 至此「執行中沉澱經驗」反射/程序/結晶三層皆落地(事實/情節層待後續)
34
+
3
35
  ## 0.3.5
4
36
 
5
37
  - **執行中沉澱經驗 — 專案手冊(程序層)**:agent 摸清專案「做事方法」時自己記下來,跨 session 自動載入。
package/README.md CHANGED
@@ -54,6 +54,8 @@ xitto-kernel --sandbox # 啟動就開 Seatbelt 沙箱
54
54
 
55
55
  **執行中沉澱經驗(專案手冊)**:agent 摸清「這個專案怎麼做事」(建置/測試/部署指令、慣例、必經步驟、踩過的坑與修法)時,會用 `playbook_update` 按 topic 記進 `.xitto-kernel/<pack>/playbook.md`(同 topic 覆蓋,天然去重);**下次 session 自動載入系統提示,不必重新摸索**。因檔案綁 cwd,手冊天然只對這個專案生效。`/playbook` 查看、`/playbook forget <主題>`、`/playbook clear`。分工:`memory` 存事實/偏好/決策(扁平),`playbook` 存可重複的程序知識(按主題)。
56
56
 
57
+ **自我結晶技能(結晶層,須驗證)**:摸出一套可重複的操作流程/SOP 時,agent 用 `skill_save` 把它**寫成新技能**(markdown)存進 `.xitto-kernel/<pack>/skills/`。**政策閘門:每個技能新增時必須附 (1) `goal` 明確目標 (2) `verify` 一條驗證指令——verify 會在沙箱實際執行,通過(exit 0)才落地**,否則拒絕並回傳輸出讓 agent 修正(危險指令一律擋下)。確保結晶的是「已驗證的成功」而非「宣稱的成功」。**本 session 立即可用 `skill` 按名載入(熱掃描),未來 session 自動列入「可用技能」**(漸進揭露:prompt 只列名稱+簡述,需要時才載全文)。**自我維護**:載入會記用量(`usedCount`);`skills_check`/`/skills check` 重跑每個技能存的 verify 偵測**漂移**——專案變動後失效的標 `⚠ stale` 浮上來讓你修或刪,保持技能庫可信(失效的在 prompt 標注、別誤用)。`/skills` 查看(含用量/失效)、`/skills forget <名>` 移除。分工:`playbook` 是專案事實性 know-how,`skill` 是可跨任務複用且**已驗證**的操作流程。這層讓 agent 像 Voyager 一樣**長出自己的技能庫**——但每條都經驗證、會自我體檢,且跑在 kernel 的沙箱 + 漸進信任治理裡。
58
+
57
59
  **通用自主 agent(給目標、自己做到完成)**
58
60
  ```bash
59
61
  xitto-kernel --pack general --yes --goal "抓取 example.com 摘要成繁中寫進 summary.txt"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xitto-kernel",
3
- "version": "0.3.5",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "領域無關的 agent 底座(kernel + 可插拔 DomainPack),從 xitto-code 抽象而來",
6
6
  "keywords": [
package/src/app/cli.js CHANGED
@@ -176,6 +176,7 @@ export function runCli({ pack, model, getApiKey, sandbox = false, resume = null,
176
176
  ' /trust [forget <項>|clear] 已信任的工具/命令(漸進放權,跨 session)',
177
177
  ' /memory 顯示跨 session 記憶',
178
178
  ' /playbook [forget <主題>|clear] 專案手冊(agent 沉澱的程序知識,跨 session)',
179
+ ' /skills [check|forget <名>] 已結晶技能(用量/失效標示;check 重跑 verify 偵測漂移)',
179
180
  ' /sessions 列出已保存的對話',
180
181
  ' /resume [id] 續接對話(不給 id=最近一次)',
181
182
  ' /clear 清除歷史(開新 session)',
@@ -202,6 +203,28 @@ export function runCli({ pack, model, getApiKey, sandbox = false, resume = null,
202
203
  if (kernel.playbook.path) out(c.gray(` ↳ ${kernel.playbook.path}(清除:/playbook forget <主題>)\n`));
203
204
  return true;
204
205
  }
206
+ case '/skills': {
207
+ const rest = input.trim().slice(cmd.length).trim();
208
+ if (rest.startsWith('forget ')) {
209
+ const n = rest.slice('forget '.length).trim();
210
+ const r = kernel.skills.remove(n);
211
+ out(r.removed ? c.gray(`(已移除技能「${r.removed}」)\n`) : c.yellow(`找不到技能「${n}」\n`));
212
+ return true;
213
+ }
214
+ if (rest === 'check') {
215
+ out(c.gray('複查中(重跑各技能 verify)…\n'));
216
+ kernel.skills.check().then((res) => {
217
+ if (!res.length) { out(c.gray('(尚無技能可複查)\n')); return; }
218
+ out(res.map((r) => (r.status === 'ok' ? c.green(' ✓ ') : r.status === 'stale' ? c.red(' ✗ ') : c.gray(' - ')) + r.name + c.gray(`(${r.status})`)).join('\n') + '\n');
219
+ });
220
+ return true;
221
+ }
222
+ const sk = kernel.skills.list();
223
+ if (!sk.length) { out(c.gray('(尚無技能;agent 摸出可重複流程時會用 skill_save 結晶)\n')); return true; }
224
+ out(sk.map((s) => (s.stale ? c.red(' ⚠ ') : c.cyan(' • ')) + s.name + c.gray(`:${s.desc}${s.used ? ` · 用過 ${s.used} 次` : ''}${s.stale ? ' · 已失效待修' : ''}`)).join('\n') + '\n');
225
+ if (kernel.skills.path) out(c.gray(` ↳ ${kernel.skills.path}(複查:/skills check · 移除:/skills forget <名>)\n`));
226
+ return true;
227
+ }
205
228
  case '/trust': {
206
229
  const rest = input.trim().slice(cmd.length).trim();
207
230
  if (rest === 'clear') { kernel.permissions.clear(); out(c.gray('(已清除全部信任)\n')); return true; }
@@ -267,7 +290,7 @@ export function runCli({ pack, model, getApiKey, sandbox = false, resume = null,
267
290
  };
268
291
 
269
292
  // 斜線指令 tab 補全
270
- const SLASH = ['/help', '/goal ', '/sandbox', '/auto', '/plan', '/undo', '/tools', '/trust', '/memory', '/playbook', '/sessions', '/resume', '/clear', '/exit'];
293
+ const SLASH = ['/help', '/goal ', '/sandbox', '/auto', '/plan', '/undo', '/tools', '/trust', '/memory', '/playbook', '/skills', '/sessions', '/resume', '/clear', '/exit'];
271
294
  const completer = (line) => {
272
295
  if (!line.startsWith('/')) return [[], line];
273
296
  const hits = SLASH.filter((s) => s.startsWith(line));
@@ -9,7 +9,9 @@ import { createToolRegistry, deriveMutatingTools, isSandboxable } from './tool-r
9
9
  import { composeGuards } from './guard-chain.js';
10
10
  import { createPermissionStep } from './security/permission-step.js';
11
11
  import { fileAllowStore, memoryAllowStore } from './security/allow-store.js';
12
- import { normalizeSandbox, wrapWithSeatbelt } from './security/sandbox.js';
12
+ import { normalizeSandbox, wrapWithSeatbelt, sandboxViolation } from './security/sandbox.js';
13
+ import { dangerousReason } from './security/danger.js';
14
+ import { spawnSync } from 'node:child_process';
13
15
  import { createMemory } from './memory.js';
14
16
  import { createPlaybook } from './playbook.js';
15
17
  import { createTodo } from './todo.js';
@@ -109,13 +111,31 @@ export function createKernel(pack, config = {}) {
109
111
  const todo = createTodo();
110
112
  const sessionsDir = join(dataDir, 'sessions');
111
113
  const hooks = loadHooks(join(dataDir, 'settings.json')); // PreToolUse/PostToolUse
112
- const skills = createSkills(join(dataDir, 'skills')); // 漸進揭露技能
113
114
 
114
115
  // 沙箱策略:config.sandbox > pack.permissionPolicy.sandbox > 預設(關)。
115
116
  const sandboxCfg = normalizeSandbox(config.sandbox ?? pack.permissionPolicy?.sandbox);
116
117
  const getSandbox = config.getSandbox || (() => !!sandboxCfg.enabled);
117
118
  const getSandboxConfig = () => sandboxCfg;
118
119
 
120
+ // 技能驗證器:結晶新技能前,先在沙箱跑一條驗證指令,須 exit 0 才准新增(結晶=已驗證的成功)。
121
+ // 靜態安全:危險指令一律擋;開沙箱時併查靜態策略,再用 Seatbelt 包執行。
122
+ const runVerify = (command, { timeoutMs = 60000 } = {}) => {
123
+ const cmd = String(command || '').trim();
124
+ if (!cmd) return { ok: false, blocked: true, reason: 'verify 指令為空' };
125
+ const danger = dangerousReason(cmd);
126
+ if (danger) return { ok: false, blocked: true, reason: `驗證指令危險(${danger}),拒絕執行` };
127
+ if (getSandbox()) {
128
+ const v = sandboxViolation(cmd, getSandboxConfig());
129
+ if (v) return { ok: false, blocked: true, reason: `驗證指令違反沙箱策略(${v})` };
130
+ }
131
+ const finalCmd = (getSandbox() ? wrapWithSeatbelt(cmd, { cwd, cfg: getSandboxConfig() }) : null) || cmd;
132
+ const r = spawnSync(finalCmd, { shell: true, cwd, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 });
133
+ const output = ((r.stdout || '') + (r.stderr || '')).trim().slice(0, 4000);
134
+ if (r.error) return { ok: false, code: null, output: (output + ' ' + r.error.message).trim() };
135
+ return { ok: r.status === 0, code: r.status, output: output || '(no output)' };
136
+ };
137
+ const skills = createSkills(join(dataDir, 'skills'), { verifyRunner: runVerify }); // 漸進揭露 + 結晶(須驗證)
138
+
119
139
  // 工具:pack 工具(sandboxable 包 Seatbelt、mutating+path 加 undo 快照)+ kernel 內建記憶工具 + spawn_agent。
120
140
  const undoStack = [];
121
141
  const baseTools = [
@@ -123,7 +143,7 @@ export function createKernel(pack, config = {}) {
123
143
  ...memory.tools,
124
144
  ...playbook.tools,
125
145
  todo.tool,
126
- ...(skills.tool ? [skills.tool] : []),
146
+ ...skills.tools,
127
147
  ...(config.extraTools || []), // 外部注入(MCP 工具等):由 app 層先 async 載入再傳入
128
148
  ];
129
149
  // spawn_agent:派唯讀子 agent。其可用工具 = 所有唯讀工具(不含 spawn_agent 自己,避免遞迴)。
@@ -199,6 +219,8 @@ export function createKernel(pack, config = {}) {
199
219
  memory,
200
220
  // 專案手冊(程序層沉澱):列出 / 更新 / 移除 / 全清;path 為落地檔。
201
221
  playbook: { list: playbook.list, update: playbook.update, remove: playbook.remove, clear: playbook.clear, load: playbook.load, path: join(dataDir, 'playbook.md') },
222
+ // 技能(結晶層 + 自我維護):列出 / 移除 / 重掃 / 漂移複查;path 為技能資料夾。
223
+ skills: { list: skills.list, remove: skills.remove, reload: skills.reload, check: skills.check, path: join(dataDir, 'skills') },
202
224
  todo: { get: todo.get },
203
225
  /** 撤銷上一次檔案改動(write/edit):還原內容,新建的檔則刪除。 */
204
226
  undo: () => {
@@ -1,37 +1,161 @@
1
- // Skills(漸進揭露)— kernel 內建。.xitto-kernel/<pack>/skills/*.md 每檔一個技能。
1
+ // Skills(漸進揭露 + 結晶層 + 自我維護)— kernel 內建。.xitto-kernel/<pack>/skills/*.md 每檔一個技能。
2
2
  // system prompt 只列「名稱 + 簡述」;agent 用 skill 工具按名載入完整步驟。對標 xitto-code skills。
3
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ // 結晶層:agent 把重複流程用 skill_save 寫成新技能(須附 goal + 通過 verify 才落地)。
4
+ // 自我維護:載入時記使用戳記(usedCount/lastUsedAt);skills_check 重跑各技能 verify 偵測漂移(stale)。
5
+ // 技能是 markdown 指令(非可執行碼),自寫安全——名稱 slug 化防穿越,內容只是日後注入的提示文字。
6
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
4
7
  import { join } from 'node:path';
5
8
 
6
9
  const txt = (o) => ({ content: [{ type: 'text', text: typeof o === 'string' ? o : JSON.stringify(o) }] });
7
10
 
11
+ // 簡易 frontmatter(key: value 行)解析/序列化 + 不動 body 的 patch。
12
+ function splitFront(md) {
13
+ const m = md.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
14
+ if (!m) return { fm: {}, body: md };
15
+ const fm = {};
16
+ for (const line of m[1].split('\n')) { const i = line.indexOf(':'); if (i > 0) fm[line.slice(0, i).trim()] = line.slice(i + 1).trim(); }
17
+ return { fm, body: m[2] };
18
+ }
19
+ function joinFront(fm, body) {
20
+ const keys = Object.keys(fm);
21
+ if (!keys.length) return body;
22
+ return '---\n' + keys.map((k) => `${k}: ${fm[k]}`).join('\n') + '\n---\n\n' + body.replace(/^\n+/, '');
23
+ }
24
+ // verify 指令取自 `## 驗證…` 的 fenced sh 區塊(skill_save 寫入的格式;保留原樣含多行)。
25
+ function extractVerify(md) {
26
+ const m = md.match(/##\s*驗證[^\n]*\n```(?:sh|bash)?\n([\s\S]*?)\n```/);
27
+ return m ? m[1].trim() : null;
28
+ }
29
+
8
30
  const firstDesc = (body) => {
9
31
  const fm = body.match(/^description:\s*(.+)$/mi);
10
32
  if (fm) return fm[1].trim();
11
33
  return (body.split('\n').map((l) => l.replace(/^#+\s*/, '').trim()).find(Boolean)) || '';
12
34
  };
13
35
 
14
- export function createSkills(dir) {
15
- const skills = [];
16
- if (existsSync(dir)) {
17
- for (const f of readdirSync(dir).filter((x) => x.endsWith('.md'))) {
18
- try { const body = readFileSync(join(dir, f), 'utf8'); skills.push({ name: f.replace(/\.md$/, ''), desc: firstDesc(body), body }); } catch { /* 略 */ }
36
+ const slug = (s) => String(s || '').trim().toLowerCase()
37
+ .replace(/[^a-z0-9一-龥_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
38
+
39
+ export function createSkills(dir, { verifyRunner } = {}) {
40
+ const fileOf = (name) => join(dir, `${name}.md`);
41
+ const readAll = () => {
42
+ const out = [];
43
+ if (existsSync(dir)) {
44
+ for (const f of readdirSync(dir).filter((x) => x.endsWith('.md'))) {
45
+ try {
46
+ const md = readFileSync(join(dir, f), 'utf8');
47
+ const { fm } = splitFront(md);
48
+ out.push({ name: f.replace(/\.md$/, ''), desc: firstDesc(md), body: md, used: Number(fm.usedCount) || 0, stale: fm.stale === 'true' });
49
+ } catch { /* 略 */ }
50
+ }
19
51
  }
20
- }
52
+ return out;
53
+ };
54
+ const patch = (name, p) => {
55
+ const file = fileOf(name);
56
+ if (!existsSync(file)) return false;
57
+ try { const { fm, body } = splitFront(readFileSync(file, 'utf8')); Object.assign(fm, p); writeFileSync(file, joinFront(fm, body)); return true; } catch { return false; }
58
+ };
59
+
60
+ let skills = readAll(); // 啟動快照(供 system prompt 列名用)
61
+ const label = (s) => `- ${s.name}:${s.desc}${s.used ? `(用過 ${s.used} 次)` : ''}${s.stale ? ' ⚠ 已失效待修' : ''}`;
21
62
 
22
63
  const promptSection = () => (skills.length
23
- ? '\n\n# 可用技能(需要時用 skill 工具按名載入完整步驟)\n' + skills.map((s) => `- ${s.name}:${s.desc}`).join('\n')
24
- : '');
64
+ ? '\n\n# 可用技能(需要時用 skill 按名載入全文;摸出可重複流程可用 skill_save 結晶;⚠ 失效的先別用,可 skills_check 複查)\n' + skills.map(label).join('\n')
65
+ : '\n\n# 技能\n尚無已存技能。摸出一套可重複的流程時,用 skill_save 把它結晶成技能(須附 goal + 通過 verify),之後即可按名複用。');
25
66
 
26
- const tool = skills.length ? {
67
+ // 載入技能:rescan 找到剛存的;記使用戳記(A)
68
+ const loadTool = {
27
69
  name: 'skill', label: '載入技能', readOnly: true,
28
70
  description: '按名載入一個技能的完整步驟(漸進揭露:prompt 只列名稱+簡述,需要時才載全文)。',
29
71
  parameters: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
30
72
  execute: async (_id, { name }) => {
31
- const s = skills.find((x) => x.name === name);
32
- return txt(s ? s.body : { error: '找不到技能', name, available: skills.map((x) => x.name) });
73
+ skills = readAll();
74
+ const s = skills.find((x) => x.name === name) || skills.find((x) => x.name === slug(name));
75
+ if (!s) return txt({ error: '找不到技能', name, available: skills.map((x) => x.name) });
76
+ patch(s.name, { usedCount: s.used + 1, lastUsedAt: new Date().toISOString() });
77
+ skills = readAll();
78
+ return txt(s.body);
79
+ },
80
+ };
81
+
82
+ // 結晶層:把可重複流程寫成新技能。政策——須附 goal + 通過的 verify 才落地(verify 在沙箱實跑)。
83
+ const saveTool = {
84
+ name: 'skill_save', label: '結晶技能', readOnly: true,
85
+ description: '把一套你摸出來、會重複用到的流程「結晶」成可複用技能。政策:每個技能必須附 (1) goal 明確目標 (2) verify 一條可驗證它有效的指令——verify 會被實際執行,通過(exit 0)才會新增,否則拒絕並回傳輸出讓你修正。確保結晶的是「已驗證的成功」。與 playbook 的差別:playbook 是專案事實性 know-how,skill 是可複用且已驗證的操作流程/SOP。',
86
+ parameters: {
87
+ type: 'object',
88
+ properties: {
89
+ name: { type: 'string', description: '技能短名(會 slug 化為檔名)' },
90
+ goal: { type: 'string', description: '這個技能要達成的明確目標(一句話)' },
91
+ body: { type: 'string', description: '完整步驟內容(markdown)' },
92
+ verify: { type: 'string', description: '一條能驗證此技能/成果有效的 shell 指令(須 exit 0;如 `npm test`、`test -f dist/app.js`)。會被實際執行。' },
93
+ description: { type: 'string', description: '可選;省略則用 goal 當簡述' },
94
+ },
95
+ required: ['name', 'goal', 'body', 'verify'],
96
+ },
97
+ execute: async (_id, { name, goal, body, verify, description }) => {
98
+ const nm = slug(name);
99
+ if (!nm) return txt({ error: 'name 不合法(需含中英數)' });
100
+ if (!goal || !String(goal).trim()) return txt({ error: '缺 goal:每個技能必須有明確目標' });
101
+ if (!body || !String(body).trim()) return txt({ error: 'body 不可為空' });
102
+ if (!verify || !String(verify).trim()) return txt({ error: '缺 verify:必須提供可驗證有效的指令(測試完成才能新增)' });
103
+ if (typeof verifyRunner !== 'function') return txt({ error: '此環境不支援技能驗證,無法新增(須在 kernel 內執行)' });
104
+
105
+ const vr = await verifyRunner(String(verify).trim());
106
+ if (vr.blocked) return txt({ error: '驗證被安全策略擋下,未新增', reason: vr.reason, verify });
107
+ if (!vr.ok) return txt({ error: '驗證未通過,未新增技能。請修正步驟或指令後重試。', exitCode: vr.code, output: vr.output, verify });
108
+
109
+ const now = new Date().toISOString();
110
+ const desc = (String(description || goal)).replace(/\s+/g, ' ').trim().slice(0, 120);
111
+ const content =
112
+ `---\ndescription: ${desc}\ngoal: ${String(goal).replace(/\s+/g, ' ').trim()}\nverified: true\nverifiedAt: ${now}\n---\n\n` +
113
+ `## 目標\n${String(goal).trim()}\n\n${String(body).trim()}\n\n## 驗證(已通過 exit 0)\n\`\`\`sh\n${String(verify).trim()}\n\`\`\`\n`;
114
+ try {
115
+ mkdirSync(dir, { recursive: true });
116
+ const existed = existsSync(fileOf(nm));
117
+ writeFileSync(fileOf(nm), content);
118
+ skills = readAll();
119
+ return txt({ [existed ? 'updated' : 'saved']: nm, verified: true, verifyOutput: vr.output, hint: '驗證通過,已結晶為技能;本 session 可用 skill 按名載入,下次 session 自動列入。' });
120
+ } catch (e) { return txt({ error: e.message }); }
33
121
  },
34
- } : null;
122
+ };
123
+
124
+ // 漂移偵測(B):重跑每個技能存的 verify → 標 ✓ 仍有效 / ✗ 已失效(stale)。
125
+ const check = async () => {
126
+ const now = new Date().toISOString();
127
+ const results = [];
128
+ for (const s of readAll()) {
129
+ const verify = extractVerify(s.body);
130
+ if (!verify) { results.push({ name: s.name, status: 'no-verify' }); continue; }
131
+ if (typeof verifyRunner !== 'function') { results.push({ name: s.name, status: 'unchecked' }); continue; }
132
+ const vr = await verifyRunner(verify);
133
+ const ok = vr.ok && !vr.blocked;
134
+ patch(s.name, ok ? { stale: 'false', lastCheckedAt: now } : { stale: 'true', staleSince: now });
135
+ results.push({ name: s.name, status: vr.blocked ? 'blocked' : ok ? 'ok' : 'stale', exitCode: vr.code });
136
+ }
137
+ skills = readAll();
138
+ return results;
139
+ };
140
+ const checkTool = {
141
+ name: 'skills_check', label: '複查技能', readOnly: true,
142
+ description: '重新驗證所有已結晶技能(重跑各自的 verify),回報哪些仍有效、哪些已失效(stale)。專案變動後用來清理過時技能,避免誤用。',
143
+ parameters: { type: 'object', properties: {} },
144
+ execute: async () => txt({ checked: await check() }),
145
+ };
146
+
147
+ const remove = (name) => {
148
+ const nm = slug(name); const file = fileOf(nm);
149
+ if (!existsSync(file)) return { error: '找不到技能', name };
150
+ try { unlinkSync(file); skills = readAll(); return { removed: nm }; } catch (e) { return { error: e.message }; }
151
+ };
35
152
 
36
- return { skills, promptSection, tool };
153
+ return {
154
+ skills, promptSection,
155
+ tool: loadTool,
156
+ tools: [loadTool, saveTool, checkTool],
157
+ list: () => readAll().map(({ name, desc, used, stale }) => ({ name, desc, used, stale })),
158
+ check, remove,
159
+ reload: () => { skills = readAll(); return skills; },
160
+ };
37
161
  }