xitto-kernel 0.3.5 → 0.3.7

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.7
4
+
5
+ - **技能結晶政策閘門(驗證才算數)**:每個自寫技能新增時必須有明確目標 + 通過的驗證,否則不落地。
6
+ - `skill_save` 新增必填 `goal`(明確目標)與 `verify`(驗證指令)
7
+ - **verify 在沙箱實際執行,exit 0 才新增**;未過則拒絕並回傳輸出供 agent 修正
8
+ - 危險驗證指令一律擋(複用 `dangerousReason`/`sandboxViolation`);開沙箱時 Seatbelt 包執行
9
+ - 落地檔記 `goal`/`verified: true`/`verifiedAt` + `## 驗證(已通過)` 區塊(為日後重驗/衰減鋪路)
10
+ - kernel 新增 `runVerify`(注入 createSkills);確保結晶的是「已驗證的成功」而非「宣稱的成功」
11
+ - 7 個測試 + 真實 runVerify 端到端(通過→新增 / 失敗→拒絕 / 危險→擋 / 缺 goal→拒)
12
+
13
+ ## 0.3.6
14
+
15
+ - **自我結晶技能(結晶層)**:agent 摸出可重複流程時自己寫成技能,跨任務/跨 session 複用。
16
+ - 新增 kernel 內建工具 `skill_save`(把流程結晶成 `.xitto-kernel/<pack>/skills/<name>.md`,含 frontmatter description)
17
+ - `skill` 載入改為**熱掃描**:本 session 剛結晶的技能即時可載;未來 session 自動列入「可用技能」
18
+ - `skill`/`skill_save` **永遠可用**(即使尚無技能,才能結晶第一個);name slug 化防路徑穿越,同名覆蓋
19
+ - 漸進揭露不變:prompt 只列名稱+簡述,需要時才載全文
20
+ - `/skills`(查看)、`/skills forget <名>`;`api.skills.{list,remove,reload,path}`
21
+ - 6 個測試 + 真實 model 端到端閉環(結晶 → 落地 → 新 session 列出並載入全文)
22
+ - 至此「執行中沉澱經驗」反射/程序/結晶三層皆落地(事實/情節層待後續)
23
+
3
24
  ## 0.3.5
4
25
 
5
26
  - **執行中沉澱經驗 — 專案手冊(程序層)**: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 只列名稱+簡述,需要時才載全文)。`/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.7",
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 [forget <名>] 已結晶的技能(agent 自寫的可複用流程)',
179
180
  ' /sessions 列出已保存的對話',
180
181
  ' /resume [id] 續接對話(不給 id=最近一次)',
181
182
  ' /clear 清除歷史(開新 session)',
@@ -202,6 +203,20 @@ 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
+ const sk = kernel.skills.list();
215
+ if (!sk.length) { out(c.gray('(尚無技能;agent 摸出可重複流程時會用 skill_save 結晶)\n')); return true; }
216
+ out(sk.map((s) => c.cyan(` • ${s.name}`) + c.gray(`:${s.desc}`)).join('\n') + '\n');
217
+ if (kernel.skills.path) out(c.gray(` ↳ ${kernel.skills.path}(移除:/skills forget <名>)\n`));
218
+ return true;
219
+ }
205
220
  case '/trust': {
206
221
  const rest = input.trim().slice(cmd.length).trim();
207
222
  if (rest === 'clear') { kernel.permissions.clear(); out(c.gray('(已清除全部信任)\n')); return true; }
@@ -267,7 +282,7 @@ export function runCli({ pack, model, getApiKey, sandbox = false, resume = null,
267
282
  };
268
283
 
269
284
  // 斜線指令 tab 補全
270
- const SLASH = ['/help', '/goal ', '/sandbox', '/auto', '/plan', '/undo', '/tools', '/trust', '/memory', '/playbook', '/sessions', '/resume', '/clear', '/exit'];
285
+ const SLASH = ['/help', '/goal ', '/sandbox', '/auto', '/plan', '/undo', '/tools', '/trust', '/memory', '/playbook', '/skills', '/sessions', '/resume', '/clear', '/exit'];
271
286
  const completer = (line) => {
272
287
  if (!line.startsWith('/')) return [[], line];
273
288
  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, path: join(dataDir, 'skills') },
202
224
  todo: { get: todo.get },
203
225
  /** 撤銷上一次檔案改動(write/edit):還原內容,新建的檔則刪除。 */
204
226
  undo: () => {
@@ -1,6 +1,8 @@
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 寫成新技能(熱掃描,當下即可載入;下次 session 自動列出)。
4
+ // 技能是 markdown 指令(非可執行碼),自寫安全——名稱 slug 化防路徑穿越,內容只是日後注入的提示文字。
5
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
4
6
  import { join } from 'node:path';
5
7
 
6
8
  const txt = (o) => ({ content: [{ type: 'text', text: typeof o === 'string' ? o : JSON.stringify(o) }] });
@@ -11,27 +13,96 @@ const firstDesc = (body) => {
11
13
  return (body.split('\n').map((l) => l.replace(/^#+\s*/, '').trim()).find(Boolean)) || '';
12
14
  };
13
15
 
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 { /* 略 */ }
16
+ // 技能名 安全檔名 slug(防 ../ 穿越;保留中英數與連字號)
17
+ const slug = (s) => String(s || '').trim().toLowerCase()
18
+ .replace(/[^a-z0-9一-龥_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
19
+
20
+ export function createSkills(dir, { verifyRunner } = {}) {
21
+ const readAll = () => {
22
+ const out = [];
23
+ if (existsSync(dir)) {
24
+ for (const f of readdirSync(dir).filter((x) => x.endsWith('.md'))) {
25
+ try { const body = readFileSync(join(dir, f), 'utf8'); out.push({ name: f.replace(/\.md$/, ''), desc: firstDesc(body), body }); } catch { /* 略 */ }
26
+ }
19
27
  }
20
- }
28
+ return out;
29
+ };
30
+
31
+ let skills = readAll(); // 啟動快照(供 system prompt 的 promptSection 列名用)
21
32
 
22
33
  const promptSection = () => (skills.length
23
- ? '\n\n# 可用技能(需要時用 skill 工具按名載入完整步驟)\n' + skills.map((s) => `- ${s.name}:${s.desc}`).join('\n')
24
- : '');
34
+ ? '\n\n# 可用技能(需要時用 skill 工具按名載入完整步驟;摸出可重複流程可用 skill_save 結晶成新技能)\n' + skills.map((s) => `- ${s.name}:${s.desc}`).join('\n')
35
+ : '\n\n# 技能\n尚無已存技能。摸出一套可重複的流程時,用 skill_save 把它結晶成技能,之後(與未來 session)即可按名複用。');
25
36
 
26
- const tool = skills.length ? {
37
+ // 載入技能:每次 rescan 找得到本 session 剛 skill_save 的技能
38
+ const loadTool = {
27
39
  name: 'skill', label: '載入技能', readOnly: true,
28
40
  description: '按名載入一個技能的完整步驟(漸進揭露:prompt 只列名稱+簡述,需要時才載全文)。',
29
41
  parameters: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
30
42
  execute: async (_id, { name }) => {
31
- const s = skills.find((x) => x.name === name);
43
+ skills = readAll();
44
+ const s = skills.find((x) => x.name === name) || skills.find((x) => x.name === slug(name));
32
45
  return txt(s ? s.body : { error: '找不到技能', name, available: skills.map((x) => x.name) });
33
46
  },
34
- } : null;
47
+ };
48
+
49
+ // 結晶層:把可重複流程寫成新技能。政策——每個技能新增時必須有「明確目標」+「通過的驗證」才算數。
50
+ // verify 指令會在沙箱實跑,exit 0 才落地(結晶=已驗證的成功,不是宣稱的成功)。下次 session 自動列入。
51
+ const saveTool = {
52
+ name: 'skill_save', label: '結晶技能', readOnly: true,
53
+ description: '把一套你摸出來、會重複用到的流程「結晶」成可複用技能。政策:每個技能必須附 (1) goal 明確目標 (2) verify 一條可驗證它有效的指令——verify 會被實際執行,通過(exit 0)才會新增,否則拒絕並回傳輸出讓你修正。確保結晶的是「已驗證的成功」。給 name、goal、body(步驟)、verify(驗證指令)。與 playbook 的差別:playbook 是專案事實性 know-how,skill 是可複用且已驗證的操作流程/SOP。',
54
+ parameters: {
55
+ type: 'object',
56
+ properties: {
57
+ name: { type: 'string', description: '技能短名(會 slug 化為檔名)' },
58
+ goal: { type: 'string', description: '這個技能要達成的明確目標(一句話)' },
59
+ body: { type: 'string', description: '完整步驟內容(markdown)' },
60
+ verify: { type: 'string', description: '一條能驗證此技能/成果有效的 shell 指令(須 exit 0;如 `npm test`、`test -f dist/app.js`)。會被實際執行。' },
61
+ description: { type: 'string', description: '可選;省略則用 goal 當簡述' },
62
+ },
63
+ required: ['name', 'goal', 'body', 'verify'],
64
+ },
65
+ execute: async (_id, { name, goal, body, verify, description }) => {
66
+ const nm = slug(name);
67
+ if (!nm) return txt({ error: 'name 不合法(需含中英數)' });
68
+ if (!goal || !String(goal).trim()) return txt({ error: '缺 goal:每個技能必須有明確目標' });
69
+ if (!body || !String(body).trim()) return txt({ error: 'body 不可為空' });
70
+ if (!verify || !String(verify).trim()) return txt({ error: '缺 verify:必須提供可驗證有效的指令(測試完成才能新增)' });
71
+ if (typeof verifyRunner !== 'function') return txt({ error: '此環境不支援技能驗證,無法新增(須在 kernel 內執行)' });
72
+
73
+ // 政策閘門:先實跑驗證,通過才落地
74
+ const vr = await verifyRunner(String(verify).trim());
75
+ if (vr.blocked) return txt({ error: '驗證被安全策略擋下,未新增', reason: vr.reason, verify });
76
+ if (!vr.ok) return txt({ error: '驗證未通過,未新增技能。請修正步驟或指令後重試。', exitCode: vr.code, output: vr.output, verify });
77
+
78
+ const now = new Date().toISOString();
79
+ const desc = (String(description || goal)).replace(/\s+/g, ' ').trim().slice(0, 120);
80
+ const content =
81
+ `---\ndescription: ${desc}\ngoal: ${String(goal).replace(/\s+/g, ' ').trim()}\nverified: true\nverifiedAt: ${now}\n---\n\n` +
82
+ `## 目標\n${String(goal).trim()}\n\n${String(body).trim()}\n\n## 驗證(已通過 exit 0)\n\`\`\`sh\n${String(verify).trim()}\n\`\`\`\n`;
83
+ try {
84
+ mkdirSync(dir, { recursive: true });
85
+ const existed = existsSync(join(dir, `${nm}.md`));
86
+ writeFileSync(join(dir, `${nm}.md`), content);
87
+ skills = readAll();
88
+ return txt({ [existed ? 'updated' : 'saved']: nm, verified: true, verifyOutput: vr.output, hint: '驗證通過,已結晶為技能;本 session 可用 skill 按名載入,下次 session 自動列入。' });
89
+ } catch (e) { return txt({ error: e.message }); }
90
+ },
91
+ };
92
+
93
+ const remove = (name) => {
94
+ const nm = slug(name);
95
+ const file = join(dir, `${nm}.md`);
96
+ if (!existsSync(file)) return { error: '找不到技能', name };
97
+ try { unlinkSync(file); skills = readAll(); return { removed: nm }; } catch (e) { return { error: e.message }; }
98
+ };
35
99
 
36
- return { skills, promptSection, tool };
100
+ return {
101
+ skills, promptSection,
102
+ tool: loadTool, // 向後相容(舊呼叫點)
103
+ tools: [loadTool, saveTool], // kernel 注入用
104
+ list: () => readAll().map(({ name, desc }) => ({ name, desc })),
105
+ remove,
106
+ reload: () => { skills = readAll(); return skills; },
107
+ };
37
108
  }