@yottameta/yotta-skills-plugin 0.7.2 → 0.8.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.
@@ -12,7 +12,7 @@
12
12
  "authentication": "ON_INSTALL"
13
13
  },
14
14
  "category": "Developer Tools",
15
- "version": "0.7.2",
15
+ "version": "0.8.0",
16
16
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
17
17
  "interface": {
18
18
  "displayName": "元阁 yotta-skills",
@@ -12,7 +12,7 @@
12
12
  "authentication": "ON_INSTALL"
13
13
  },
14
14
  "category": "Developer Tools",
15
- "version": "0.7.2",
15
+ "version": "0.8.0",
16
16
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
17
17
  "interface": {
18
18
  "displayName": "元阁 yotta-skills",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-skills-plugin",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "YuanGe (元阁) — orchestration and routing for the YottaMeta skill family, packaged as a YottaMeta Agent Plugin.",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "yotta-skills",
4
- "version": "0.7.2",
4
+ "version": "0.8.0",
5
5
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
6
6
  "author": {
7
7
  "name": "YottaMeta",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-skills
3
- version: 0.7.2
4
- description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么自动装+自动用」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录;盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工可先跑 --reindex 与 update --check(联网只读检查更新);自包含零依赖,不依赖任何元技能);MCP 按需加载(可选:list_installed_skills/describe_skill/reindex/route_request,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / install / update / update --check(只读检查)/ update --auto(家族自动更新)/ --inventory / --reindex / --dry-run 预览 / --pin 锁版本。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/该用哪个技能/路由技能/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能;装前摘要仅供参考,安装决策由用户确认。
3
+ version: 0.8.0
4
+ description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么自动装+自动用」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录;盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工可先跑 --reindex 与 update --check(联网只读检查更新);自包含零依赖,不依赖任何元技能);MCP 按需加载(可选:list_installed_skills/describe_skill/reindex/route_request,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / install / update / update --check(只读检查)/ update --auto(家族自动更新)/ --inventory / --reindex / --dry-run 预览 / --pin 锁版本。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/该用哪个技能/路由技能/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
5
5
  license: MIT
6
6
  metadata:
7
7
  zh_name: 元阁
@@ -132,9 +132,9 @@ npx -y @yottameta/yotta-skills --reindex
132
132
  | `--dry-run` | 预览将执行的安装 / 更新清单;不联网、不改动 |
133
133
  | `--pin` | 锁死清单精确版本(默认 range:跟随同 major 最新 patch) |
134
134
  | `--force` | 已是最新也重新安装 |
135
- | `--skip-scan` | 跳过元信装前摘要(装了 yotta-verify 时默认自动启用) |
135
+ | `--skip-scan` | 人工应急路径:跳过元信门禁并标记 `explicit-unverified`;不得用于 `update --auto` |
136
136
  | `--npm <path>` | 指定 npm 可执行文件 |
137
- | `--python <path>` | 指定 python 可执行文件(元信摘要用) |
137
+ | `--python <path>` | 指定 python 可执行文件(元信 scan 用) |
138
138
  | `--verify <path>` | 指定 yotta_verify.py 路径 |
139
139
  | `-h, --help` / `-v, --version` | 帮助 / 版本 |
140
140
 
@@ -235,12 +235,19 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
235
235
  - 是否「已是最新」由目标目录 `<slug>/SKILL.md` 的 frontmatter `version` 与清单比对,
236
236
  一致即跳过(幂等)。
237
237
 
238
- ## 元信装前摘要
238
+ ## 元信装前门禁
239
+
240
+ 家族安装默认执行装前扫描。若本机没有元信(yotta-verify),元阁先按 `skills.json`
241
+ 安装元信自身,再扫描待装技能。安装前会先读取包内 manifest、校验身份,再进入元信门禁:
242
+
243
+ - `SAFE TO INSTALL`:继续安装;
244
+ - `INSTALL WITH CAUTION` / `REVIEW REQUIRED`:继续安装,但显示风险并写入证据;
245
+ - `DO NOT INSTALL` 或扫描失败:阻断,不替换旧版本。
239
246
 
240
- 安装时若检测到元信(yotta-verify)引擎,会对每个待装技能先跑一次装前摘要并打印 verdict
241
- 与计数——仅提示、不拦截;verdict 为 DO NOT INSTALL 时会额外提示人工复核。
242
247
  引擎查找顺序:`--verify` 指定路径 → 环境变量 `YOTTA_SKILLS_VERIFY` → 目标目录下已装的
243
- `yotta-verify/scripts/yotta_verify.py`。可用 `--skip-scan` 关闭。
248
+ `yotta-verify/scripts/yotta_verify.py`。`--skip-scan` 只保留为人工应急路径,使用时输出
249
+ `explicit-unverified` 并写入 `~/.yottaskills/install-log.jsonl`;`update --auto` 不会使用
250
+ 该开关。
244
251
 
245
252
  ## 支持智能体
246
253
 
@@ -254,7 +261,7 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
254
261
  |---|---|
255
262
  | `YOTTA_SKILLS_NPM` | 指定 npm 可执行文件(同 `--npm`) |
256
263
  | `YOTTA_SKILLS_NPM_FLAGS` | 追加传给 `npm pack` 的参数(按空白拆分,如 `--registry=...`) |
257
- | `YOTTA_SKILLS_PYTHON` | 指定 python 可执行文件(元信摘要用,同 `--python`) |
264
+ | `YOTTA_SKILLS_PYTHON` | 指定 python 可执行文件(元信 scan 用,同 `--python`) |
258
265
  | `YOTTA_SKILLS_VERIFY` | 指定 yotta_verify.py 路径(同 `--verify`) |
259
266
  | `YOTTA_SKILLS_MANIFEST` | 指定技能清单 JSON 路径(默认随包 skills.json) |
260
267
 
@@ -24,6 +24,9 @@ const os = require('os');
24
24
  const { spawnSync } = require('child_process');
25
25
  const http = require('http');
26
26
  const https = require('https');
27
+ const evidenceLib = require('../lib/install-evidence');
28
+ const gateLib = require('../lib/verify-gate');
29
+ const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
27
30
 
28
31
  const PKG_ROOT = path.join(__dirname, '..');
29
32
  let VERSION = '0.2.1';
@@ -343,19 +346,27 @@ async function runUpdateAuto(opts, dest) {
343
346
  }
344
347
  out('');
345
348
  out('检测到 ' + r.updates + ' 个家族技能可更新,开始自动更新(安装管线,含装前安全扫描):');
346
- var ok = 0, failed2 = 0;
349
+ var ok = 0, failed2 = 0, autoExitCode = 0;
347
350
  for (var i = 0; i < r.rows.length; i++) {
348
351
  var row = r.rows[i];
349
352
  if (!row.hasUpdate) continue;
350
- var res = installOne(Object.assign({}, row.family, { version: row.latest }), dest, Object.assign({}, opts, { force: true, pin: true }));
353
+ var res = installOne(
354
+ Object.assign({}, row.family, { version: row.latest }),
355
+ dest,
356
+ Object.assign({}, opts, { force: true, pin: true, skipScan: false }),
357
+ );
351
358
  if (res.status === 'ok') ok++;
352
359
  else if (res.status === 'skip') ok++;
353
- else failed2++;
360
+ else {
361
+ failed2++;
362
+ autoExitCode = Math.max(autoExitCode, res.exitCode || 1);
363
+ out(' ✘ ' + row.slug + ' 失败: ' + res.note);
364
+ }
354
365
  }
355
366
  out('');
356
367
  out('自动更新汇总: 成功 ' + ok + ' / 失败 ' + failed2);
357
- maybeAutoReindex(opts, dest);
358
- return { code: failed2 > 0 ? 1 : 0 };
368
+ if (failed2 === 0) maybeAutoReindex(opts, dest);
369
+ return { code: failed2 > 0 ? autoExitCode : 0 };
359
370
  }
360
371
 
361
372
  function shouldSkip(name, isFile) {
@@ -399,17 +410,12 @@ function detectProjectDir() {
399
410
  return null;
400
411
  }
401
412
 
402
- // ── 元信 scan(若可用)─────────────────────────────────────────────────────
413
+ // ── 元信 scan 与安装门禁 ──────────────────────────────────────────────────
403
414
  function findVerifyEngine(dest, opts) {
404
- const cands = [];
405
- if (opts.verify) cands.push(path.resolve(opts.verify));
406
- if (process.env.YOTTA_SKILLS_VERIFY) cands.push(path.resolve(process.env.YOTTA_SKILLS_VERIFY));
407
- if (dest) cands.push(path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py'));
408
- for (const c of cands) {
409
- try { if (fs.statSync(c).isFile()) return c; } catch (_) { /* next */ }
410
- }
411
- return null;
415
+ const registry = require('../lib/skills-scan').readRegistry();
416
+ return gateLib.findVerifier({ dest, opts, registry });
412
417
  }
418
+
413
419
  function findPython(opts) {
414
420
  const cands = [];
415
421
  if (opts.python) cands.push(opts.python);
@@ -424,19 +430,29 @@ function findPython(opts) {
424
430
  }
425
431
  return null;
426
432
  }
433
+
427
434
  function runScan(engine, skillDir) {
428
435
  const python = findPython({});
429
- if (!python) return { ok: false, note: '未找到 python(元信 scan 需要 Python 3.8+)' };
430
- // -B:禁止 Python 写 __pycache__(否则会在引擎所在目录(可能是已装技能)留下 .pyc 污染)
431
- const r = spawnSync(python, ['-B', engine, 'scan', skillDir, '--json'], { encoding: 'utf8', timeout: 60000 });
432
- if (r.status === null) return { ok: false, note: '元信 scan 执行失败' };
433
- try {
434
- const j = JSON.parse(r.stdout);
435
- const c = j.counts || {};
436
- return { ok: true, verdict: j.verdict, counts: c };
437
- } catch (_) {
438
- return { ok: false, note: '元信 scan 输出无法解析' };
436
+ if (!python) return { ok: false, error: '未找到 Python(元信 scan 需要 Python 3.8+)' };
437
+ return gateLib.runVerifier(engine, skillDir, { python, spawnSync });
438
+ }
439
+
440
+ function scanTarget(engine, skillDir) {
441
+ const scan = runScan(engine, skillDir);
442
+ if (scan.ok) {
443
+ const counts = scan.counts || {};
444
+ const line = 'critical ' + (counts.critical || 0) + ' / high ' + (counts.high || 0) +
445
+ ' / medium ' + (counts.medium || 0) + ' / low ' + (counts.low || 0) +
446
+ ' / info ' + (counts.info || 0);
447
+ out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
448
+ if (scan.verdict === gateLib.BLOCK) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,已阻断安装。');
449
+ else if (scan.verdict === gateLib.CAUTION || scan.verdict === gateLib.REVIEW) {
450
+ out(' ⚠ 元信 verdict 为 ' + scan.verdict + ',继续安装并保留风险证据。');
451
+ }
452
+ } else {
453
+ out(' 元信 scan: ' + scan.error);
439
454
  }
455
+ return scan;
440
456
  }
441
457
 
442
458
  // ── 安装 ───────────────────────────────────────────────────────────────────
@@ -468,6 +484,13 @@ function runNpmPack(skill, opts, packDir) {
468
484
  }
469
485
 
470
486
  function extractTarball(tarball, extractDir) {
487
+ const listed = spawnSync(tarBin(), ['-tzf', tarball], { encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 });
488
+ if (listed.status !== 0) {
489
+ return { error: (listed.stderr || listed.stdout || 'tar 列表读取失败').trim().split('\n').pop() };
490
+ }
491
+ const entries = String(listed.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
492
+ const unsafe = entries.find((entry) => !isSafeTarEntry(entry));
493
+ if (unsafe) return { error: '压缩包包含不安全路径: ' + unsafe };
471
494
  const r = spawnSync(tarBin(), ['-xzf', tarball, '-C', extractDir], { encoding: 'utf8', timeout: 120000 });
472
495
  if (r.status !== 0) return { error: (r.stderr || r.stdout || 'tar 解压失败').trim().split('\n').pop() };
473
496
  const pkgDir = path.join(extractDir, 'package');
@@ -477,72 +500,94 @@ function extractTarball(tarball, extractDir) {
477
500
 
478
501
  const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '__pycache__']);
479
502
 
480
- function installOne(skill, dest, opts) {
481
- const target = path.join(dest, skill.slug);
482
- const existing = readInstalledVersion(target);
483
- if (!opts.force && existing === skill.version) {
484
- return { skill, status: 'skip', version: existing, note: '已是最新' };
503
+ function ensureGate(context) {
504
+ const { skill, extracted, dest, opts } = context;
505
+ const current = findVerifyEngine(dest, opts);
506
+ if (current) {
507
+ return {
508
+ ok: true,
509
+ engine: current,
510
+ mode: opts.bootstrap ? 'trusted-bootstrap' : 'installed',
511
+ };
485
512
  }
513
+ if (skill.slug === 'yotta-verify') {
514
+ const engine = path.join(extracted.pkgDir, 'scripts', 'yotta_verify.py');
515
+ if (!fs.existsSync(engine)) return { ok: false, error: '元信包内缺少 scripts/yotta_verify.py' };
516
+ return { ok: true, engine, mode: 'trusted-bootstrap' };
517
+ }
518
+
519
+ const verifier = findSkill('yotta-verify');
520
+ if (!verifier) return { ok: false, error: 'skills.json 缺少 yotta-verify' };
486
521
  let tmp;
487
522
  try {
488
- tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-skills-'));
523
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-verify-bootstrap-'));
489
524
  const packDir = path.join(tmp, 'pack');
490
525
  fs.mkdirSync(packDir, { recursive: true });
491
- const packed = runNpmPack(skill, opts, packDir);
492
- if (packed.error) {
493
- if (packed.detail) out(' ' + packed.detail.split(/\r?\n/).filter(Boolean).slice(-12).join('\n '));
494
- return { skill, status: 'fail', version: null, note: packed.error };
495
- }
526
+ const packed = runNpmPack(verifier, opts, packDir);
527
+ if (packed.error) return { ok: false, error: '元信自举下载失败: ' + packed.error };
496
528
  const extractDir = path.join(tmp, 'extract');
497
529
  fs.mkdirSync(extractDir, { recursive: true });
498
- const extracted = extractTarball(packed.tarball, extractDir);
499
- if (extracted.error) return { skill, status: 'fail', version: packed.resolved, note: extracted.error };
500
-
501
- // 元信 scan(若可用):装前摘要,不拦截
502
- if (!opts.skipScan) {
503
- const engine = findVerifyEngine(dest, opts);
504
- if (engine) {
505
- const scan = runScan(engine, extracted.pkgDir);
506
- if (scan.ok) {
507
- const c = scan.counts;
508
- const line = 'critical ' + (c.critical || 0) + ' / high ' + (c.high || 0) + ' / medium ' + (c.medium || 0) +
509
- ' / low ' + (c.low || 0) + ' / info ' + (c.info || 0);
510
- out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
511
- if (scan.verdict && /DO NOT INSTALL/.test(scan.verdict)) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,请人工复核后再使用。');
512
- } else {
513
- out(' 元信 scan: ' + scan.note);
514
- }
515
- }
516
- }
517
- fs.rmSync(target, { recursive: true, force: true });
518
- fs.mkdirSync(target, { recursive: true });
519
- copyDir(extracted.pkgDir, target, COPY_SKIP);
520
- return { skill, status: 'ok', version: packed.resolved || skill.version, note: null };
521
- } catch (e) {
522
- return { skill, status: 'fail', version: null, note: e.message };
530
+ const extractedVerifier = extractTarball(packed.tarball, extractDir);
531
+ if (extractedVerifier.error) return { ok: false, error: '元信自举解包失败: ' + extractedVerifier.error };
532
+ const engine = path.join(extractedVerifier.pkgDir, 'scripts', 'yotta_verify.py');
533
+ if (!fs.existsSync(engine)) return { ok: false, error: '元信包内缺少 scripts/yotta_verify.py' };
534
+
535
+ out(' 元信未安装,使用可信源包自举并先自扫。');
536
+ const result = installOne(verifier, dest, {
537
+ ...opts,
538
+ force: true,
539
+ skipScan: false,
540
+ bootstrap: true,
541
+ verify: engine,
542
+ });
543
+ if (result.status !== 'ok') return { ok: false, error: '元信自举失败: ' + result.note };
544
+ const installedEngine = path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py');
545
+ if (!fs.existsSync(installedEngine)) return { ok: false, error: '元信自举后未找到安装引擎' };
546
+ return { ok: true, engine: installedEngine, mode: 'trusted-bootstrap' };
523
547
  } finally {
524
548
  if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
525
549
  }
526
550
  }
527
551
 
552
+ const installOne = createInstaller({
553
+ runNpmPack,
554
+ extractTarball,
555
+ copyDir: (src, dst) => copyDir(src, dst, COPY_SKIP),
556
+ readInstalledVersion,
557
+ ensureGate,
558
+ scanTarget,
559
+ appendEvidence: evidenceLib.appendEvidence,
560
+ });
561
+
528
562
  function runInstall(opts, dest) {
529
563
  const skills = selectSkills(opts);
530
564
  out('yotta-skills(元阁)v' + VERSION + ' —— 安装 ' + skills.length + ' 个技能 -> ' + dest);
531
565
  out('版本策略: ' + (opts.pin ? 'pin(锁死清单精确版本)' : 'range(' + skillRange(skills[0]) + ',跟随最新 patch;--pin 锁死)'));
532
566
  const results = [];
533
567
  let failed = 0;
568
+ let exitCode = 0;
534
569
  for (const s of skills) {
535
570
  const r = installOne(s, dest, opts);
536
571
  results.push(r);
537
- if (r.status === 'ok') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?'));
572
+ if (r.status === 'ok') {
573
+ if (r.gate && r.gate.mode === 'explicit-unverified') {
574
+ out(' ⚠ ' + s.slug.padEnd(22) + '未执行装前扫描(explicit-unverified)');
575
+ }
576
+ out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?'));
577
+ }
538
578
  else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ',v' + r.version + ')');
539
- else { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
579
+ else {
580
+ failed++;
581
+ exitCode = Math.max(exitCode, r.exitCode || 1);
582
+ out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note);
583
+ }
540
584
  }
541
585
  const ok = results.filter(r => r.status === 'ok').length;
542
586
  const skip = results.filter(r => r.status === 'skip').length;
543
587
  out('');
544
588
  out('汇总: 成功 ' + ok + ' / 跳过(已是最新) ' + skip + ' / 失败 ' + failed + '(共 ' + results.length + ')');
545
- if (failed > 0) process.exitCode = 1;
589
+ if (failed > 0) process.exitCode = exitCode || 1;
590
+ return { failed, exitCode };
546
591
  }
547
592
 
548
593
  function runUpdate(opts, dest) {
@@ -550,6 +595,7 @@ function runUpdate(opts, dest) {
550
595
  out('yotta-skills(元阁)v' + VERSION + ' —— 增量更新 -> ' + dest);
551
596
  const results = [];
552
597
  let failed = 0;
598
+ let exitCode = 0;
553
599
  for (const s of skills) {
554
600
  const existing = readInstalledVersion(path.join(dest, s.slug));
555
601
  if (existing === s.version) {
@@ -559,15 +605,25 @@ function runUpdate(opts, dest) {
559
605
  }
560
606
  const r = installOne(s, dest, opts);
561
607
  results.push(r);
562
- if (r.status === 'ok') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?') + (existing ? '(原 v' + existing + ')' : '(新装)'));
608
+ if (r.status === 'ok') {
609
+ if (r.gate && r.gate.mode === 'explicit-unverified') {
610
+ out(' ⚠ ' + s.slug.padEnd(22) + '未执行装前扫描(explicit-unverified)');
611
+ }
612
+ out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?') + (existing ? '(原 v' + existing + ')' : '(新装)'));
613
+ }
563
614
  else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ')');
564
- else { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
615
+ else {
616
+ failed++;
617
+ exitCode = Math.max(exitCode, r.exitCode || 1);
618
+ out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note);
619
+ }
565
620
  }
566
621
  const ok = results.filter(r => r.status === 'ok').length;
567
622
  const skip = results.filter(r => r.status === 'skip').length;
568
623
  out('');
569
624
  out('汇总: 更新 ' + ok + ' / 已是最新 ' + skip + ' / 失败 ' + failed);
570
- if (failed > 0) process.exitCode = 1;
625
+ if (failed > 0) process.exitCode = exitCode || 1;
626
+ return { failed, exitCode };
571
627
  }
572
628
 
573
629
  // ── 展示 ───────────────────────────────────────────────────────────────────
@@ -777,12 +833,12 @@ function main() {
777
833
  process.exitCode = r.code;
778
834
  });
779
835
  } else {
780
- runUpdate(opts, dest);
781
- maybeAutoReindex(opts, dest);
836
+ const result = runUpdate(opts, dest);
837
+ if (!result.failed) maybeAutoReindex(opts, dest);
782
838
  }
783
839
  } else {
784
- runInstall(opts, dest);
785
- maybeAutoReindex(opts, dest);
840
+ const result = runInstall(opts, dest);
841
+ if (!result.failed) maybeAutoReindex(opts, dest);
786
842
  }
787
843
  }
788
844
 
@@ -69,11 +69,15 @@ npx -y @yottameta/yotta-skills install yotta-memory yotta-workflow --agent codex
69
69
 
70
70
  ## 10. 安装前安全扫描怎么用?
71
71
 
72
- 默认会查找可用的元信引擎并输出摘要;可用 `--verify <path>` 指定引擎,或 `--skip-scan` 关闭。扫描结果仅提示,不替代人工判断。
72
+ 家族安装默认执行元信门禁:没有元信时先自动自举,再扫描待装包。`DO NOT INSTALL`
73
+ 和扫描失败会阻断;`INSTALL WITH CAUTION` / `REVIEW REQUIRED` 会继续但显示风险。
74
+ 可用 `--verify <path>` 指定引擎。`--skip-scan` 只是人工应急路径,会标记
75
+ `explicit-unverified`,`update --auto` 不使用它。
73
76
 
74
77
  ## 11. 安装失败后如何恢复?
75
78
 
76
- 查看汇总中的失败原因,先修复目录权限或网络,再单独安装失败技能:
79
+ 查看汇总中的失败原因。旧版本快照保存在 `~/.yottaskills/snapshots/`,当前版本失败时
80
+ 新版本不会替换旧版本。先修复目录权限或网络,再单独安装失败技能:
77
81
 
78
82
  ```bash
79
83
  npx -y @yottameta/yotta-skills install <slug> --dir /path/to/skills
@@ -10,10 +10,13 @@
10
10
  1. 读清单(`skills.json`,默认随包;`YOTTA_SKILLS_MANIFEST` 可覆盖);
11
11
  2. `npm pack <pkg>@<spec> --pack-destination <临时目录>`;
12
12
  3. 系统 `tar -xzf` 解压到临时目录(产物应有 `SKILL.md`);
13
- 4. 元信装前摘要(若引擎可用;`--skip-scan` 关闭);
14
- 5. 删除目标 `<dest>/<slug>` 后重新复制(跳过规则见下);
15
- 6. 汇总报告:✔ 成功 / - 跳过(已是最新)/ ✘ 失败;有失败项时退出码 1。
16
- 7. 全部完成后自动 re-index 本地技能注册表(`~/.yottaskills/registry.json`):重扫技能根目录并增量合并,
13
+ 4. 读取包内 `skill-manifest.json`;没有 manifest 时使用 `skills.json` 的家族默认契约;
14
+ 5. 校验 slug / package / version / 权限 / 生命周期脚本路径;
15
+ 6. 元信装前扫描:已有元信则直接扫描;没有则自动安装元信自身,再扫描;
16
+ 7. 通过后创建旧版本快照,把新版本复制到同盘暂存目录,再原子切换到目标目录;
17
+ 8. setup / doctor 阶段执行内置检查;P0-2.1 不执行自定义生命周期脚本;
18
+ 9. 汇总报告:✔ 成功 / - 跳过(已是最新)/ ✘ 失败;
19
+ 10. 全部成功后自动 re-index 本地技能注册表(`~/.yottaskills/registry.json`):重扫技能根目录并增量合并,
17
20
  新装 / 更新的技能随即进入注册表(`--no-reindex` 可关闭;`--dry-run` 不触发)。
18
21
 
19
22
  ## 复制跳过规则
@@ -24,6 +27,17 @@
24
27
  因此装进技能目录的是「技能本体」(SKILL.md / references / scripts 等),不含 npm
25
28
  安装器自身、测试夹具与 Python 字节码缓存。
26
29
 
30
+ ## manifest 与家族默认契约
31
+
32
+ 每个技能包可以自带根目录 `skill-manifest.json`。有 manifest 时:
33
+
34
+ - 校验 `slug` / `package` / `version` 与包目录、`package.json`、`SKILL.md` 一致;
35
+ - 校验 `install.idempotent` 为 `true`;
36
+ - 校验 setup / doctor / rollback 路径为包内相对路径,不能包含绝对路径或 `..`。
37
+
38
+ 没有 manifest 时使用家族默认契约:来源 `yottameta`、幂等安装、自动应用模式 `route`,
39
+ 不声明 MCP、hook 或自定义生命周期脚本。
40
+
27
41
  ## re-index(装技能后自动重扫注册表)
28
42
 
29
43
  - `install` / `update` 完成后,CLI 自动调用 `--reindex`(同一套 `lib/skills-scan.js` 扫描核心):
@@ -51,14 +65,37 @@
51
65
  其它 → 直接执行;
52
66
  - `YOTTA_SKILLS_NPM_FLAGS` 追加到 `npm pack` 参数。
53
67
 
54
- ## 元信装前摘要
68
+ ## 元信装前门禁
55
69
 
56
70
  - 引擎查找:`--verify` → `YOTTA_SKILLS_VERIFY` → `<dest>/yotta-verify/scripts/yotta_verify.py`;
57
71
  - python 查找:`--python` → `YOTTA_SKILLS_PYTHON` → `python3` / `python` / `py`(win32);
58
72
  - 执行:`python -B <engine> scan <解压目录> --json`(`-B` 禁止写 `__pycache__`,防止污染
59
73
  引擎所在目录);
60
- - 输出 verdict + counts(critical / high / medium / low / info);DO NOT INSTALL 额外提示
61
- 人工复核;仅摘要、不拦截。
74
+ - 元信缺失时:先按 `skills.json` 安装 `yotta-verify`,并记录 `gate_mode=trusted-bootstrap`
75
+ 与 `bootstrap_scan=self`,随后用元信扫描其余家族包;
76
+ - verdict 处置:`SAFE TO INSTALL` 继续;`INSTALL WITH CAUTION` / `REVIEW REQUIRED`
77
+ 继续但显示风险并留证;`DO NOT INSTALL` 和扫描失败阻断,不替换目标;
78
+ - `--skip-scan` 只保留为人工应急路径,使用时输出 `explicit-unverified` 并写入证据;
79
+ - `update --auto` 始终执行装前门禁,不接受 `--skip-scan`。
80
+
81
+ ## 快照与回滚
82
+
83
+ - 旧版本快照:`~/.yottaskills/snapshots/<slug>/<timestamp>-<version>-<随机后缀>/`;
84
+ - 新版本先落在 `<dest>/.yottaskills-staging/<slug>-<随机后缀>/`;
85
+ - 旧目标先重命名为同盘备份,再把暂存目录切到目标;切换失败时恢复备份;
86
+ - 证据写入失败会触发回滚,不把安装标记为成功;
87
+ - Windows 上对 `EPERM` / `EACCES` / `EBUSY` 做短重试,避免杀毒或索引服务造成的瞬时锁。
88
+
89
+ ## 安装证据
90
+
91
+ 每次安装决策写入:
92
+
93
+ ```text
94
+ ~/.yottaskills/install-log.jsonl
95
+ ```
96
+
97
+ 记录包含时间、事件、技能、包名、版本、`gate_mode`、verdict、decision
98
+ 和快照路径;不记录技能内容或用户数据,只写本机。
62
99
 
63
100
  ## 退出码
64
101
 
@@ -68,6 +105,8 @@
68
105
  | 1 | 安装 / 更新存在失败项 |
69
106
  | 2 | 用法错误(未知参数 / 未知技能 / 未收录智能体) |
70
107
  | 4 | 未指定目标且当前目录未检测到项目级技能目录 |
108
+ | 5 | 装前 gate 阻断或元信不可用 |
109
+ | 6 | manifest / 身份校验失败 |
71
110
 
72
111
  ## 目标目录解析
73
112
 
@@ -8,7 +8,7 @@
8
8
  | `yotta-anti-shallow` | 元谨 | `@yottameta/yotta-anti-shallow` | 1.4.0 | 防 AI 敷衍规则引擎(深入分析/根因追溯时激活) |
9
9
  | `yotta-code-quality` | 元质 | `@yottameta/yotta-code-quality` | 0.4.0 | 结对式代码质量评审(十二类腐化风险 + 0-100 健康分) |
10
10
  | `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.4.0 | 跨会话/跨项目通用工作流协议(状态就近存) |
11
- | `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.11.0 | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
11
+ | `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.12.0 | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
12
12
  | `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.2.0 | 学习沉淀 CLI:错误/纠正/洞见沉淀为 .learnings/ 条目 |
13
13
  | `yotta-security-audit` | 元安 | `@yottameta/yotta-security-audit` | 0.2.2 | 安全扫描引擎:13 类检测器 + 系统安全基线 |
14
14
  | `yotta-vetter` | 元审 | `@yottameta/yotta-vetter` | 0.2.3 | 安全审查协议:四阶段 review + SAFE TO INSTALL 判定 |
@@ -8,7 +8,7 @@
8
8
 
9
9
  - 学会 `--list` / `install` / `update` / `--dry-run` / `--pin` / `--force` 的用法;
10
10
  - 理解版本策略(range 与 --pin)与幂等行为;
11
- - 掌握元信装前摘要的开关与配置;
11
+ - 了解元信装前门禁、元信自举与 `--skip-scan` 的人工应急边界;
12
12
  - 前置:Node.js 18+、npm、系统 tar(Windows 10+ 自带);网络可访问 npm 注册表
13
13
  (国内可配置镜像或代理)。
14
14
 
@@ -76,22 +76,26 @@ dry-run 只打印将要执行的清单(含目标与版本策略),不联网
76
76
  npx -y @yottameta/yotta-skills install --agent codex --pin
77
77
  ```
78
78
 
79
- ## 8. 强制重装与跳过元信摘要
79
+ ## 8. 强制重装与元信门禁
80
80
 
81
81
  ```bash
82
82
  npx -y @yottameta/yotta-skills install --agent codex --force # 已是最新也重装
83
- npx -y @yottameta/yotta-skills install --agent codex --skip-scan # 跳过装前摘要
83
+ npx -y @yottameta/yotta-skills install --agent codex --skip-scan # 人工应急:显式未验证
84
84
  ```
85
85
 
86
- 元信(yotta-verify)已安装时默认会对每个待装技能输出装前摘要(verdict + 计数),仅提示
87
- 不拦截;verdict 为 DO NOT INSTALL 时会提示人工复核。
86
+ 家族安装默认执行元信门禁:没有元信时,元阁先按可信源安装元信自身,再扫描待装技能。
87
+ `DO NOT INSTALL` 会阻断安装;`INSTALL WITH CAUTION` / `REVIEW REQUIRED` 会继续但显示风险。
88
+ 旧版本会先快照到 `~/.yottaskills/snapshots/`,安装决策写入
89
+ `~/.yottaskills/install-log.jsonl`。`--skip-scan` 只用于人工应急,会输出
90
+ `explicit-unverified` 并留证,`update --auto` 不会使用它。
88
91
 
89
92
  ## 9. 验证安装结果
90
93
 
91
94
  - 看安装汇总:成功 N / 跳过 N / 失败 N;
92
95
  - 抽查技能目录:`ls ~/.codex/skills/yotta-memory/SKILL.md`;
93
96
  - 复核版本:每个技能 SKILL.md 的 frontmatter `version` 应与 `--list` 一致(这也是幂等
94
- 判断的依据)。
97
+ 判断的依据);
98
+ - 需要安全复核时,查看 `~/.yottaskills/install-log.jsonl` 中的 verdict 与 `gate_mode`。
95
99
 
96
100
  ## 10. 常见问题
97
101
 
@@ -24,7 +24,7 @@ import subprocess
24
24
  import sys
25
25
  from pathlib import Path
26
26
 
27
- VERSION = "0.7.2"
27
+ VERSION = "0.8.0"
28
28
  TOOL_NAME = "yotta-skills"
29
29
  CN_NAME = "元阁"
30
30
  MCP_PROTOCOL_MODERN = "2026-07-28"