@yottameta/yotta-skills 0.20.1 → 0.21.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v0.21.0 (2026-09-30)
2
+ - 新增 M1 记忆裁决器开源调用口:`yotta-skills decide-memory` 与 MCP `decide_memory`,调用本地扩展提供方 `m1.adjudicate`,输出每个技能的 `promote / hold / demote` 只读建议、分数与信号明细。
3
+ - 新增本地使用记录:`usage status|enable|disable|mark|reset`;默认关闭,`usage enable` 后 `--route` 才记录结构化 route_hits 与组合对,显式 `usage mark` 记录 used / named / accepted。记录只含 slug、时间、信号类型、playbook / confidence 与组合对,不含需求原文、记忆正文、路径或身份信息。
4
+ - `decide-memory` 默认只读;`--dry-run` 显式只读;`--promote` 只写 `~/.yottaskills/memory-adjudication.json` 建议文件,生成私密 `PREF` 记忆候选,不自动写元忆、不删除任何内容。`--explain` 输出每个信号的得分明细。
5
+ - provider 子进程环境透传授权库相关变量(`YOTTA_LICENSE_HOME` / `YOTTA_LICENSE_KEYS_DIR` / `YOTTA_LICENSE_BASE_URL` / `YOTTA_LICENSE_SERVER_ID`),私有 M1 provider 可用 `LicenseGate.assertCan('m1.adjudicate')` 做授权门;未授权 / 超时 / 非法输出一律 fail-open,退出码保持 0。
6
+ - 评分算法位于本地私有目录 `license/m1/`,不进任何发布件;开源侧只做特征快照、白名单校验与展示。协议见 `references/provider-protocol.md`。
7
+ - 元忆 0.19.0 不变;静态 playbook / 安装 / hook / scanPolicy 契约零变更。
8
+
9
+ ## v0.20.2 (2026-09-29)
10
+ - 家族安装的 npm 拉包新增镜像回退:默认源返回 404(国内镜像未同步该版本 tarball)时自动改用官方源 `https://registry.npmjs.org/` 重试一次;安装输出显示回退行,安装证据新增 `npm_registry_fallback` 字段。
11
+ - 两次都失败时给出可直接复制的修复提示(`npm_config_registry` / `YOTTA_SKILLS_NPM_FLAGS`);已显式指定 registry 或设置 `YOTTA_SKILLS_NO_FALLBACK=1` 时不回退。
12
+ - 非 404 错误(网络超时等)不触发回退,保持原失败信息。
13
+ - 静态 playbook / 路由 / hook / scanPolicy 契约零变更。
14
+
1
15
  ## v0.20.1 (2026-09-29)
2
16
  - 家族安装接入 scanPolicy 例外复核:元信判 `DO NOT INSTALL` 时,按包内 `scan-policy.json` 的已审查例外(逐条绑定技能 + 版本 + treeHash + 规则 + 路径)复核,豁免检测规则表 / 攻防样例 / 文档说明类命中;复核后无阻断级发现则继续安装。
3
17
  - 修复元忆 / 元察 / 元鉴从元阁安装时被元信自指误报阻断的问题(检测类技能自带规则表字面量);例外表与 handoff / OpenCode 的 scanPolicy 同源,由 `tools/build_scan_policy.py` 生成。
package/README.md CHANGED
@@ -27,6 +27,7 @@ Installing the whole YottaMeta family used to mean running <code>npx</code> for
27
27
 
28
28
  - **List** - see the whole family: slug, Chinese name, package, version and a one-line description.
29
29
  - **Orchestration routing** - <code>--route</code> turns a task summary into a candidate combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install command; it only suggests installation and never installs automatically. An optional local provider may add or re-order installed skills within the allow-list (no behavior change when unconfigured; see <code>references/provider-protocol.md</code>).
30
+ - **M1 memory adjudication** - <code>decide-memory</code> returns read-only <code>promote / hold / demote</code> suggestions, scores and signal details. Scoring is performed by a user-configured local provider (capability <code>m1.adjudicate</code>). It is read-only by default; <code>--promote</code> writes only a local recommendation file and never writes memory or deletes anything. <code>usage</code> is off by default and records only structured signals after the user enables it.
30
31
  - **Install** — install everything (or a subset) into an agent's default user-level directory or any directory.
31
32
  - **Update** — incremental update: add missing skills, upgrade version-skewed ones.
32
33
  - **Update check / auto-update** — <code>update --check</code> compares installed skill versions against the npm registry and reports; <code>update --check --scheduled</code> is the weekly background entry with a local cache and jitter, so it stays offline until due; <code>update --auto</code> upgrades the installed YottaMeta family to the latest (non-family skills are never auto-updated).
@@ -34,7 +35,7 @@ Installing the whole YottaMeta family used to mean running <code>npx</code> for
34
35
  - **Runtime hook adapter** — manifests declare six lifecycle requirements; <code>hook capabilities / evaluate / bind / unbind</code> probe host support, apply deterministic decisions, record evidence, and never overstate audit-only hosts as enforced.
35
36
  - **Idempotent** — a skill already at the manifest version is skipped; re-running is safe.
36
37
  - **Pre-install gate** — family installs read the package manifest, bootstrap yotta-verify (元信) when it is missing, then scan each package. <code>DO NOT INSTALL</code> blocks; caution/review continue with visible risk. Old versions are snapshotted before replacement.
37
- - **Inventory / re-index** - scan the skill directories on this machine and keep a local registry (<code>~/.yottaskills/registry.json</code>); set <code>YOTTA_SKILLS_REGISTRY_FILE</code> to isolate the registry per agent; self-contained, no other skills required. Newly installed skills are discovered automatically: <code>install</code> / <code>update</code> re-index the registry afterwards, and <code>--reindex</code> re-scans on demand. An optional <code>yotta-skills</code> MCP (on-demand, not resident) exposes <code>list_installed_skills</code> / <code>describe_skill</code> / <code>reindex</code> / <code>route_request</code>; see <code>SKILL.md</code> for the config.
38
+ - **Inventory / re-index** - scan the skill directories on this machine and keep a local registry (<code>~/.yottaskills/registry.json</code>); set <code>YOTTA_SKILLS_REGISTRY_FILE</code> to isolate the registry per agent; self-contained, no other skills required. Newly installed skills are discovered automatically: <code>install</code> / <code>update</code> re-index the registry afterwards, and <code>--reindex</code> re-scans on demand. An optional <code>yotta-skills</code> MCP (on-demand, not resident) exposes <code>list_installed_skills</code> / <code>describe_skill</code> / <code>reindex</code> / <code>route_request</code> / <code>decide_memory</code>; see <code>SKILL.md</code> for the config.
38
39
  - **No silent writes** - 元阁 only suggests. Installing skills, adding a client <code>mcpServers</code> entry, and writing an agent's global memory each require explicit user confirmation beforehand; declining never blocks the plain CLI workflow.
39
40
 
40
41
  Boundaries: it only downloads, places, gates and summarizes — it does **not** develop skill content, does **not** bundle any skill body, does **not** use <code>-g</code> global installs, and does **not** silently write host configuration or agent global memory. Besides the target directory, it keeps its registry, snapshots, install evidence and update-check cache under <code>~/.yottaskills</code>.
@@ -83,6 +84,14 @@ npx -y @yottameta/yotta-skills --dry-run
83
84
  # Route a task to a combination, call order, and missing-skill install suggestion
84
85
  npx -y @yottameta/yotta-skills --route "Review this code carefully before release"
85
86
 
87
+ # Inspect / enable local structured usage signals (off by default)
88
+ npx -y @yottameta/yotta-skills usage status
89
+ npx -y @yottameta/yotta-skills usage enable
90
+
91
+ # Read-only M1 memory adjudication; --promote writes only a local recommendation file
92
+ npx -y @yottameta/yotta-skills decide-memory --explain
93
+ npx -y @yottameta/yotta-skills decide-memory --promote --json
94
+
86
95
  # Inventory installed skills on this machine (self-contained scan, no other skills needed)
87
96
  npx -y @yottameta/yotta-skills --inventory
88
97
 
@@ -111,6 +120,10 @@ Requirements: Node.js 18+, npm, and system <code>tar</code> (built into Windows
111
120
  | `--inventory` | Inventory installed skills: scan skill directories and update the local registry (self-contained); `--json` for JSON, `--project` adds project-level dirs |
112
121
  | `--reindex` | Re-index the registry: re-scan skill directories and merge changes incrementally (on demand; `install` / `update` re-index automatically; `--rescan` is a synonym); `--json` for JSON |
113
122
  | `--route <task-summary>` | Static orchestration routing: return a combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install suggestion; also lists other installed (non-YottaMeta) skills as candidates matched mechanically against their frontmatter description, tagged "not scanned", read-only and never auto-invoked; an optional local provider may add or re-order installed skills within the allow-list (see <code>references/provider-protocol.md</code>); `--json` for JSON plus a `dynamic` status block, `--project` adds project-level dirs |
123
+ | `usage status` / `usage enable` / `usage disable` | Inspect / enable / disable local structured usage signals; off by default and limited to slug, time, signal type, playbook / confidence and skill pairs |
124
+ | `usage mark --skill <slug> --signal used\|named\|accepted` | Record one explicit usage signal; no prior enable required |
125
+ | `usage reset --yes` | Clear the local usage record |
126
+ | `decide-memory [--dry-run\|--promote] [--explain]` | Read-only M1 memory adjudication; `--promote` writes only a local recommendation file and never writes memory or deletes anything; `--json` for stable JSON |
114
127
  | `--no-reindex` | Do not re-index the registry automatically after `install` / `update` |
115
128
  | `--dry-run` | Preview the install / update list; no network, no changes |
116
129
  | `--pin` | Lock the exact manifest versions (default) |
package/README.zh-CN.md CHANGED
@@ -30,6 +30,7 @@
30
30
 
31
31
  - **看清单**--全家技能一览:slug / 中文名 / 包名 / 版本 / 一句话说明。
32
32
  - **编排路由**--`--route` 按需求摘要输出候选组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。可选本地扩展提供方可在已装白名单内增补 / 重排(未配置时行为不变),协议见 `references/provider-protocol.md`。
33
+ - **M1 记忆裁决**--`decide-memory` 输出技能 `promote / hold / demote` 只读建议、分数与信号明细;评分由用户显式配置的本地 provider(capability `m1.adjudicate`)完成。默认只读,`--promote` 只写本地建议文件,不写元忆、不删除内容。`usage` 默认关闭,显式开启后才记录结构化使用信号(不含需求原文)。
33
34
  - **安装**——装全家(或指定技能)到智能体默认用户级目录或任意目录。
34
35
  - **更新**——增量更新:补齐缺失技能、升级版本不一致的技能。
35
36
  - **更新检查 / 自动更新**——<code>update --check</code> 只读比对已装技能版本与 npm 注册表并报告;<code>update --check --scheduled</code> 是后台周检入口,用本地缓存和随机抖动做到未到期不联网;<code>update --auto</code> 检查后自动把已装元阁家族升到最新(非元阁家族技能绝不自动更新)。
@@ -37,7 +38,7 @@
37
38
  - **运行时 hook 适配层**——技能 manifest 只声明六个生命周期要求;<code>hook capabilities / evaluate / bind / unbind</code> 负责宿主能力探测、确定性决策、结构化证据和降级标注,不把 audit 能力夸大为强制。
38
39
  - **幂等**——已在清单版本的技能跳过;重复运行安全。
39
40
  - **装前门禁**——家族安装先读取包内 manifest,元信(yotta-verify)缺失时自动自举,再逐个扫描。`DO NOT INSTALL` 阻断;`CAUTION` / `REVIEW` 继续但显示风险。旧版本会先做快照再替换。
40
- - **盘点 / re-index**--扫描本机各智能体技能目录,维护本地注册表(<code>~/.yottaskills/registry.json</code>);可用 <code>YOTTA_SKILLS_REGISTRY_FILE</code> 为不同 agent 指定独立注册表;自包含,不需要任何其他技能。新装技能自动被发现:<code>install</code> / <code>update</code> 完成后自动重扫注册表,<code>--reindex</code> 可随时手动重扫(如会话开工)。可选 <code>yotta-skills</code> MCP(按需加载、不常驻)提供 <code>list_installed_skills</code> / <code>describe_skill</code> / <code>reindex</code> / <code>route_request</code> 四工具,配置见 <code>SKILL.md</code>。
41
+ - **盘点 / re-index**--扫描本机各智能体技能目录,维护本地注册表(<code>~/.yottaskills/registry.json</code>);可用 <code>YOTTA_SKILLS_REGISTRY_FILE</code> 为不同 agent 指定独立注册表;自包含,不需要任何其他技能。新装技能自动被发现:<code>install</code> / <code>update</code> 完成后自动重扫注册表,<code>--reindex</code> 可随时手动重扫(如会话开工)。可选 <code>yotta-skills</code> MCP(按需加载、不常驻)提供 <code>list_installed_skills</code> / <code>describe_skill</code> / <code>reindex</code> / <code>route_request</code> / <code>decide_memory</code> 五工具,配置见 <code>SKILL.md</code>。
41
42
 
42
43
  边界:只做「下载 + 落位 + 门禁 + 汇总」——**不**开发技能内容、**不**内置任何技能本体、**不**用 <code>-g</code>
43
44
  全局安装;除目标目录外,会在 <code>~/.yottaskills</code> 下保留注册表、快照、安装证据与更新检查缓存。
@@ -86,6 +87,14 @@ npx -y @yottameta/yotta-skills --dry-run
86
87
  # 按需求摘要给出组合、顺序与缺失技能安装建议
87
88
  npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄"
88
89
 
90
+ # 查看 / 开启本地使用记录(默认关闭)
91
+ npx -y @yottameta/yotta-skills usage status
92
+ npx -y @yottameta/yotta-skills usage enable
93
+
94
+ # M1 记忆裁决只读建议;--promote 只写本地建议文件
95
+ npx -y @yottameta/yotta-skills decide-memory --explain
96
+ npx -y @yottameta/yotta-skills decide-memory --promote --json
97
+
89
98
  # 盘点本机已装技能(自包含扫描,不依赖任何元技能)
90
99
  npx -y @yottameta/yotta-skills --inventory
91
100
 
@@ -114,6 +123,10 @@ npx -y @yottameta/yotta-skills --reindex
114
123
  | `--inventory` | 盘点已装技能:扫描技能目录并更新本地注册表(自包含);`--json` 输出 JSON、`--project` 附扫项目级目录 |
115
124
  | `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(install / update 完成后自动重扫,也可随时手动运行;`--rescan` 同义);`--json` 输出 JSON |
116
125
  | `--route <需求摘要>` | 静态编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;可选本地扩展提供方在已装白名单内增补 / 重排(未配置时行为不变,协议见 `references/provider-protocol.md`);`--json` 输出 JSON 与 `dynamic` 状态块、`--project` 附扫项目级目录 |
126
+ | `usage status` / `usage enable` / `usage disable` | 查看 / 开启 / 关闭本地结构化使用记录;默认关闭,只记录 slug、时间、信号类型、playbook / confidence 与组合对 |
127
+ | `usage mark --skill <slug> --signal used\|named\|accepted` | 记录一次显式使用信号;不要求先 enable |
128
+ | `usage reset --yes` | 清空本地使用记录 |
129
+ | `decide-memory [--dry-run\|--promote] [--explain]` | M1 记忆裁决只读建议;`--promote` 只写本地建议文件,不写元忆、不删除内容;`--json` 输出稳定 JSON |
117
130
  | `--no-reindex` | 安装 / 更新后不自动重扫注册表 |
118
131
  | `--dry-run` | 预览将执行的安装 / 更新清单;不联网、不改动 |
119
132
  | `--pin` | 锁死清单精确版本(默认) |
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-skills
3
- version: 0.20.1
4
- description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 运行时 hook 适配。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录(默认 --pin 锁死清单精确版本);盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工只建议跑本地 --reindex;更新检查走手动 --check 或后台 --check --scheduled);运行时适配层:hook capabilities / evaluate / bind / unbind 按宿主能力矩阵执行六个统一事件并留证降级,元信 before_install 已接入安装管线;自包含零依赖,不依赖任何元技能;MCP 按需加载且需用户确认后写入配置(可选:list_installed_skills/describe_skill/reindex/route_request,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
3
+ version: 0.21.0
4
+ description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 运行时 hook 适配。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);M1 记忆裁决层:usage enable/mark 本地结构化记录 + decide-memory / MCP decide_memory 输出 promote / hold / demote 只读建议(需授权 provider;只建议不删除、不自动写元忆);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录(默认 --pin 锁死清单精确版本);盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工只建议跑本地 --reindex;更新检查走手动 --check 或后台 --check --scheduled);运行时适配层:hook capabilities / evaluate / bind / unbind 按宿主能力矩阵执行六个统一事件并留证降级,元信 before_install 已接入安装管线;自包含零依赖,不依赖任何元技能;MCP 按需加载且需用户确认后写入配置(可选:list_installed_skills/describe_skill/reindex/route_request/decide_memory,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / usage 使用记录 / decide-memory 记忆裁决 / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、判断哪些技能值得长期记忆、查看或记录技能使用信号、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/记忆裁决/技能该不该记住/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + M1 记忆裁决只读建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆、不自动删除技能或记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
5
5
  license: MIT
6
6
  metadata:
7
7
  zh_name: 元阁
@@ -44,6 +44,7 @@ metadata:
44
44
 
45
45
  - **路由层(该用哪几个)**:`--route "<需求摘要>"` / MCP `route_request` 查本地注册表与静态编排 playbook,输出候选组合、调用顺序、每个技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。
46
46
  - **动态扩展口(v0.20.0,capability `o1.route`)**:可选调用用户显式配置的本地扩展提供方(provider),在已装注册表白名单内对路由结果增补 / 重排;静态 playbook 结果先算必算、永不缺席,未配置或调用失败时文本输出与历史一致,`--json` 仅多一个 `dynamic` 状态块。协议与配置见 `references/provider-protocol.md`。
47
+ - **M1 记忆裁决层(哪些值得长期记住)**:`decide-memory` / MCP `decide_memory` 调用可选本地 provider(capability `m1.adjudicate`),输出 `promote / hold / demote` 只读建议、分数与信号明细;默认不写元忆、不删除技能。`usage` 默认关闭,用户显式开启后才记录结构化使用信号。协议与边界见 `references/provider-protocol.md`。
47
48
  - **策划层(怎么用)**:接到需求,先按「编排策划」定位命中哪个组合——哪些元技能搭配起来最强、适合什么场景、AI 该给出什么安装与调用建议(**安装与调用均由用户确认后执行**)。
48
49
  - **安装层(怎么装)**:一条 `npx -y @yottameta/yotta-skills` 把组合/全家装进指定智能体或目录——`--list` 看清单、`install` 装、`update` 增量更新、`doctor` 只读自检、`rollback` 恢复快照、`--dry-run` 预览、`--pin` 锁版本。
49
50
 
@@ -173,6 +174,11 @@ openclaw skills update @yottameta/yotta-skills
173
174
  | `--inventory` | 盘点本机已装技能:扫描技能目录生成/更新注册表(自包含,不依赖元技能);`--json` 输出 JSON、`--project` 附扫项目级目录 |
174
175
  | `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(install / update 完成后 CLI 自动重扫;也可在会话开工等时机手动运行);`--json` 输出 JSON、`--project` 附扫项目级目录;`--rescan` 同义 |
175
176
  | `--route <需求摘要>` | 静态编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;`--json` 输出 JSON、`--project` 附扫项目级目录 |
177
+ | `usage status` | 查看本地使用记录开关与计数;默认关闭,不创建文件 |
178
+ | `usage enable` / `usage disable` | 开启 / 关闭 `--route` 的结构化使用记录;不记录需求原文 |
179
+ | `usage mark --skill <slug> --signal used\|named\|accepted` | 记录一次显式使用信号;不要求先 enable |
180
+ | `usage reset --yes` | 清空本地使用记录 |
181
+ | `decide-memory [--dry-run\|--promote] [--explain] [--json]` | M1 记忆裁决只读建议;`--promote` 只写本地建议文件,不写元忆、不删除内容 |
176
182
  | `--no-reindex` | 安装 / 更新后不自动重扫注册表 |
177
183
  | `--dry-run` | 预览将执行的安装 / 更新清单;不联网、不改动 |
178
184
  | `--pin` | 锁死清单精确版本(默认 range:跟随同 major 最新 patch) |
@@ -298,11 +304,41 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
298
304
  - **无明确匹配**:低置信度回退到「入口 + 安装」组合,先澄清需求再继续。
299
305
  - **边界**:路由是建议,不保证完全正确;关键动作由用户确认,数据不出本机。
300
306
 
307
+ ## M1 记忆裁决与使用记录
308
+
309
+ `decide-memory` 判断哪些已装技能值得进入长期记忆,输出 `promote / hold / demote` 建议、分数与信号明细。
310
+ 评分由用户显式配置的本地 provider(capability `m1.adjudicate`)完成;未配置 / 未授权 / 超时 / 非法输出时
311
+ 只返回状态,不阻断其他能力,退出码保持 0。
312
+
313
+ ```bash
314
+ # 默认只读:只看建议,不写任何文件
315
+ npx -y @yottameta/yotta-skills decide-memory
316
+
317
+ # 显式只读 + 信号明细
318
+ npx -y @yottameta/yotta-skills decide-memory --dry-run --explain
319
+
320
+ # 只写本地建议文件(不写元忆、不删除内容)
321
+ npx -y @yottameta/yotta-skills decide-memory --promote --json
322
+ ```
323
+
324
+ 使用记录默认关闭:
325
+
326
+ ```bash
327
+ npx -y @yottameta/yotta-skills usage status
328
+ npx -y @yottameta/yotta-skills usage enable
329
+ npx -y @yottameta/yotta-skills usage mark --skill yotta-memory --signal used
330
+ npx -y @yottameta/yotta-skills usage disable
331
+ npx -y @yottameta/yotta-skills usage reset --yes
332
+ ```
333
+
334
+ 边界:记录只含技能 slug、时间、信号类型、playbook / confidence 与组合对;不含需求原文、记忆正文、路径或身份信息。
335
+ `--promote` 只写 `~/.yottaskills/memory-adjudication.json`,生成私密 `PREF` 记忆候选,由用户或 AI 再显式调用元忆写入。
336
+
301
337
  ### MCP:按需加载(可选)
302
338
 
303
339
  本技能自带一个 MCP server:`yotta-skills`(`scripts/yotta-skills-mcp.py`,零依赖、数据不出本机),
304
340
  基于 MCP 最新协议 2026-07-28(无状态时代;向后兼容 2025-11-25 及更早握手客户端)。
305
- 提供 `list_installed_skills`(盘点)/ `describe_skill`(单技能详情)/ `reindex`(强制重扫)/ `route_request`(静态编排路由)四个工具。
341
+ 提供 `list_installed_skills`(盘点)/ `describe_skill`(单技能详情)/ `reindex`(强制重扫)/ `route_request`(静态编排路由)/ `decide_memory`(M1 记忆裁决只读建议)五个工具。
306
342
 
307
343
  **按需加载,不走常驻**:本技能与 MCP 均为按需触发。默认以 CLI 为主
308
344
  (`npx -y @yottameta/yotta-skills --inventory`);需要让 AI 通过工具直接调用时,再按下面配置启用。
@@ -327,9 +363,9 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
327
363
  > `<技能目录>` = 本技能实际安装目录,**不要写死盘符路径**;Windows 用 `python`,Linux/macOS 用 `python3`。
328
364
  > 单 agent 环境可不设 `YOTTA_SKILLS_REGISTRY_FILE`;同一宿主内多 agent 共用 MCP 时,应分别配置独立路径,避免注册表状态互相覆盖。
329
365
  3. **提醒用户**:改 `mcpServers` 后多数客户端需**重启 / 重载一次** MCP server 才生效;加载后应看到
330
- `list_installed_skills` / `describe_skill` / `reindex` / `route_request` 四个工具。
366
+ `list_installed_skills` / `describe_skill` / `reindex` / `route_request` / `decide_memory` 五个工具。
331
367
  4. **降级兜底(重要)**:若客户端未暴露 MCP 工具 / 用户拒绝改配置 / 无法改配置 / server 未加载,**自动降级 CLI**
332
- (同一套扫描与路由核心、结果一致):`npx -y @yottameta/yotta-skills --inventory / --reindex / --route ...`。
368
+ (同一套扫描、路由与 M1 核心、结果一致):`npx -y @yottameta/yotta-skills --inventory / --reindex / --route ... / decide-memory ...`。
333
369
 
334
370
  ## 版本策略
335
371
 
@@ -375,14 +411,16 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
375
411
  |---|---|
376
412
  | `YOTTA_SKILLS_NPM` | 指定 npm 可执行文件(同 `--npm`) |
377
413
  | `YOTTA_SKILLS_NPM_FLAGS` | 追加传给 `npm pack` 的参数(按空白拆分,如 `--registry=...`) |
414
+ | `YOTTA_SKILLS_NO_FALLBACK` | 设为 `1` 时禁用「默认源 404 → 官方源重试」的自动回退 |
378
415
  | `YOTTA_SKILLS_PYTHON` | 指定 python 可执行文件(元信 scan 用,同 `--python`) |
379
416
  | `YOTTA_SKILLS_VERIFY` | 指定 yotta_verify.py 路径(同 `--verify`) |
380
417
  | `YOTTA_SKILLS_MANIFEST` | 指定技能清单 JSON 路径(默认随包 skills.json) |
381
418
 
382
419
  ## 常见问题
383
420
 
384
- - **npmmirror 全新包 404**:镜像同步有延迟,通过 `YOTTA_SKILLS_NPM_FLAGS` 追加
385
- `--registry=https://registry.npmjs.org/`(国内需代理)或等待镜像缓存后重试。
421
+ - **npmmirror 全新包 404**:默认源返回 404 时安装器会**自动**改用官方源
422
+ `https://registry.npmjs.org/` 重试一次(输出显示回退行,安装证据记 `npm_registry_fallback`);
423
+ 若已显式指定 registry 或设置 `YOTTA_SKILLS_NO_FALLBACK=1`,则保持在指定源上,失败时给出可复制的修复提示。
386
424
  - **未收录智能体**:`--agent <name>` 报未收录时,改用 `--dir` 指到它的技能目录。
387
425
  - **Windows 下 npm 报错**:CLI 已内置 npm-cli.js 解析,无需额外处理;如需覆盖用 `--npm`。
388
426
  - **某技能安装失败**:汇总报告会列出失败原因,可单装该技能排查。
@@ -36,10 +36,13 @@ const snapshotLib = require('../lib/install-snapshot');
36
36
  const updateCheckLib = require('../lib/update-check');
37
37
  const hookAdapterLib = require('../lib/hook-adapter');
38
38
  const scanPolicyLib = require('../lib/scan-policy');
39
+ const npmPackLib = require('../lib/npm-pack');
40
+ const usageJournalLib = require('../lib/usage-journal');
41
+ const m1FeaturesLib = require('../lib/m1-features');
39
42
  const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
40
43
 
41
44
  const PKG_ROOT = path.join(__dirname, '..');
42
- let VERSION = '0.20.1';
45
+ let VERSION = '0.21.0';
43
46
  try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
44
47
 
45
48
  function loadManifest() {
@@ -152,6 +155,7 @@ function parseArgs(argv) {
152
155
  inventory: false, reindex: false, noReindex: false, json: false, project: false, route: null,
153
156
  check: false, auto: false, scheduled: false, registry: null, slug: null,
154
157
  host: null, event: null, manifest: null, context: null,
158
+ signal: null, yes: false, explain: false, promote: false,
155
159
  };
156
160
  const positionals = [];
157
161
  for (let i = 0; i < argv.length; i++) {
@@ -196,12 +200,18 @@ function parseArgs(argv) {
196
200
  else if (a === '--event') opts.event = take('--event').toLowerCase();
197
201
  else if (a === '--manifest') opts.manifest = take('--manifest');
198
202
  else if (a === '--context') opts.context = take('--context');
203
+ else if (a === '--skill') opts.skill = take('--skill').toLowerCase();
204
+ else if (a === '--signal') opts.signal = take('--signal').toLowerCase();
205
+ else if (a === '--yes') opts.yes = true;
206
+ else if (a === '--explain') opts.explain = true;
207
+ else if (a === '--promote') opts.promote = true;
199
208
  else if (a.startsWith('-')) die('未知参数: ' + a, 2, '可用 --help 查看支持的选项。');
200
209
  else positionals.push(a);
201
210
  }
202
211
  // 命令解析:install / update / doctor / rollback,其余位置参数 = 技能 slug(可多个)
203
212
  for (const p of positionals) {
204
- if (p === 'install' || p === 'update' || p === 'doctor' || p === 'rollback' || p === 'hook') {
213
+ if (p === 'install' || p === 'update' || p === 'doctor' || p === 'rollback' || p === 'hook'
214
+ || p === 'usage' || p === 'decide-memory') {
205
215
  if (opts.command && opts.command !== p) die('命令冲突:' + opts.command + ' 与 ' + p);
206
216
  opts.command = p;
207
217
  } else {
@@ -617,31 +627,14 @@ function scanTarget(engine, skillDir, context) {
617
627
  }
618
628
 
619
629
  // ── 安装 ───────────────────────────────────────────────────────────────────
630
+ const runNpmPackBase = npmPackLib.createPackRunner({ spawnSync, specOf, resolveNpm });
631
+
620
632
  function runNpmPack(skill, opts, packDir) {
621
- const spec = specOf(skill, opts.pin);
622
- const args = ['pack', spec, '--pack-destination', packDir];
623
- const flags = (process.env.YOTTA_SKILLS_NPM_FLAGS || '').trim();
624
- if (flags) args.push(...flags.split(/\s+/));
625
- const npm = resolveNpm(opts);
626
- const r = spawnSync(npm.bin, [...npm.prefix, ...args], { encoding: 'utf8', timeout: 180000, maxBuffer: 64 * 1024 * 1024, shell: npm.shell });
627
- if (r.status !== 0) {
628
- const raw = (r.stderr || r.stdout || 'npm pack 失败').trim();
629
- const ll = raw.split(/\r?\n/).filter(Boolean);
630
- const brief = ll.slice(-4).join(' | ');
631
- return { error: brief || 'npm pack 失败', detail: raw };
632
- }
633
- const lines = (r.stdout || '').split(/\r?\n/).map(l => l.trim()).filter(Boolean);
634
- let tarball = null;
635
- for (const l of lines) if (/\.tgz$/.test(l)) tarball = l;
636
- if (!tarball) {
637
- try {
638
- const found = fs.readdirSync(packDir).filter(f => f.endsWith('.tgz'));
639
- if (found.length === 1) tarball = found[0];
640
- } catch (_) { /* ignore */ }
633
+ const packed = runNpmPackBase(skill, opts, packDir);
634
+ if (packed.registryFallback) {
635
+ out(' ↳ npm 默认源 404,已用官方源重试成功(' + npmPackLib.OFFICIAL_REGISTRY + ')');
641
636
  }
642
- if (!tarball) return { error: '未找到 npm pack 产物(' + spec + ')' };
643
- const vm = String(tarball).match(/-([0-9]+\.[0-9]+\.[0-9]+)\.tgz$/);
644
- return { tarball: path.join(packDir, tarball), resolved: vm ? vm[1] : null, spec };
637
+ return packed;
645
638
  }
646
639
 
647
640
  function extractTarball(tarball, extractDir) {
@@ -1117,6 +1110,9 @@ function printHelp() {
1117
1110
  out(' yotta-skills --inventory 盘点本机已装技能(自研扫描,不依赖任何元技能)');
1118
1111
  out(' yotta-skills --reindex 重扫注册表(手动触发;install / update 完成后 CLI 自动重扫)');
1119
1112
  out(' yotta-skills --route "<需求摘要>" 给出场景组合、调用顺序、缺失技能安装建议');
1113
+ out(' yotta-skills usage status 查看本地使用记录开关与计数');
1114
+ out(' yotta-skills usage enable|disable 开启 / 关闭 --route 的结构化使用记录');
1115
+ out(' yotta-skills usage mark --skill <slug> --signal used|named|accepted');
1120
1116
  out(' yotta-skills hook capabilities 查看宿主六事件能力矩阵');
1121
1117
  out(' yotta-skills hook evaluate --event <event> --manifest <file> 评估 hook 声明并留证');
1122
1118
  out(' yotta-skills hook bind --manifest <file> 注册 hook 声明(幂等)');
@@ -1143,6 +1139,11 @@ function printHelp() {
1143
1139
  out(' --event <event> hook 六事件之一(before_start / before_tool / before_install / before_publish / after_milestone / before_send)');
1144
1140
  out(' --manifest <file> hook evaluate / bind 使用的 skill-manifest.json 路径');
1145
1141
  out(' --context <json> hook evaluate 的检查结果 JSON(checks / wrapperRegistered / evidence)');
1142
+ out(' --skill <slug> usage mark 的技能 slug');
1143
+ out(' --signal <name> usage mark 的信号:used / named / accepted');
1144
+ out(' --yes usage reset 确认清空');
1145
+ out(' --explain decide-memory 文本报告追加信号明细');
1146
+ out(' --promote decide-memory 只写本地建议文件(不写元忆)');
1146
1147
  out(' --project inventory / reindex 时附加扫描当前项目 .agents/skills / .codex/skills');
1147
1148
  out(' --no-reindex 安装 / 更新后不自动重扫注册表');
1148
1149
  out(' -h, --help 帮助');
@@ -1318,6 +1319,15 @@ function runRoute(opts) {
1318
1319
  const yottaSlugs = new Set([...defaultYottaSlugs(), ...MANIFEST.map((s) => s.slug)]);
1319
1320
  const result = routeRequest(opts.route, { registry, yottaSlugs });
1320
1321
  result.dynamic = applyDynamicRoute(result, registry, opts);
1322
+ try {
1323
+ usageJournalLib.recordRoute({
1324
+ playbook: result.playbook.id,
1325
+ confidence: result.confidence,
1326
+ skills: result.skills.map((skill) => skill.slug),
1327
+ });
1328
+ } catch (_) {
1329
+ // 使用记录失败不阻断路由
1330
+ }
1321
1331
  if (opts.json) {
1322
1332
  out(JSON.stringify(result, null, 2));
1323
1333
  return;
@@ -1363,6 +1373,184 @@ function runRoute(opts) {
1363
1373
  out('说明: ' + result.disclaimer);
1364
1374
  }
1365
1375
 
1376
+ function runUsage(opts) {
1377
+ const action = opts.rest[0] || 'status';
1378
+ if (action === 'status') {
1379
+ const state = usageJournalLib.readUsage();
1380
+ if (opts.json) {
1381
+ out(JSON.stringify(state, null, 2));
1382
+ return;
1383
+ }
1384
+ out('元阁本地使用记录:' + (state.enabled ? '已开启' : '已关闭'));
1385
+ out('文件: ' + usageJournalLib.usageFilePath());
1386
+ const skills = Object.keys(state.skills || {}).sort();
1387
+ if (!skills.length) {
1388
+ out('暂无记录。');
1389
+ return;
1390
+ }
1391
+ for (const slug of skills) {
1392
+ const item = state.skills[slug];
1393
+ out(' ' + slug + ' used=' + item.used + ' named=' + item.named
1394
+ + ' accepted=' + item.accepted + ' route_hits=' + item.route_hits
1395
+ + ' pairs=' + Object.keys(item.pairs || {}).length);
1396
+ }
1397
+ return;
1398
+ }
1399
+ if (action === 'enable' || action === 'disable') {
1400
+ const state = usageJournalLib.setEnabled(action === 'enable');
1401
+ if (opts.json) {
1402
+ out(JSON.stringify(state, null, 2));
1403
+ return;
1404
+ }
1405
+ out('元阁本地使用记录已' + (state.enabled ? '开启' : '关闭') + '。');
1406
+ out('文件: ' + usageJournalLib.usageFilePath());
1407
+ return;
1408
+ }
1409
+ if (action === 'mark') {
1410
+ if (!opts.skill) die('usage mark 缺少 --skill', 2, '例如 --skill yotta-memory。');
1411
+ if (!opts.signal) die('usage mark 缺少 --signal', 2, '可用 used / named / accepted。');
1412
+ let state;
1413
+ try {
1414
+ state = usageJournalLib.markUsage(opts.skill, opts.signal);
1415
+ } catch (error) {
1416
+ die(error.message, 2, '请检查 --skill 与 --signal。');
1417
+ }
1418
+ if (opts.json) {
1419
+ out(JSON.stringify(state, null, 2));
1420
+ return;
1421
+ }
1422
+ out('已记录:' + opts.skill + ' / ' + opts.signal);
1423
+ return;
1424
+ }
1425
+ if (action === 'reset') {
1426
+ if (!opts.yes) die('usage reset 需要 --yes 确认', 2, '该操作会清空本地使用记录。');
1427
+ const file = usageJournalLib.resetUsage();
1428
+ if (opts.json) {
1429
+ out(JSON.stringify({ reset: true, file }, null, 2));
1430
+ return;
1431
+ }
1432
+ out('已清空本地使用记录:' + file);
1433
+ return;
1434
+ }
1435
+ die('未知 usage 子命令: ' + action, 2, '支持 status / enable / disable / mark / reset。');
1436
+ }
1437
+
1438
+ function m1StatusText(block) {
1439
+ if (!block) return '';
1440
+ if (block.status === 'active') {
1441
+ return '已应用(提供方 ' + (block.provider_id || '-') + ';建议 '
1442
+ + block.decisions.length + ' 条)';
1443
+ }
1444
+ if (block.status === 'license_required') return '需授权(该能力需要授权后使用)';
1445
+ if (block.status === 'timeout') return '未生效(提供方超时)';
1446
+ if (block.status === 'invalid_output') return '未生效(提供方输出无效)';
1447
+ if (block.status === 'error') return '未生效(提供方异常)';
1448
+ return block.status;
1449
+ }
1450
+
1451
+ function runDecideMemory(opts) {
1452
+ if (opts.dryRun && opts.promote) {
1453
+ die('--dry-run 与 --promote 不能同时使用', 2, '默认就是只读;需要写建议文件时只加 --promote。');
1454
+ }
1455
+ const { registry } = reindexRegistry(opts);
1456
+ const usage = usageJournalLib.readUsage();
1457
+ const snapshot = m1FeaturesLib.buildFeatureSnapshot(registry, usage);
1458
+ const provider = require('../lib/provider');
1459
+ let run;
1460
+ try {
1461
+ run = provider.runCapability('m1.adjudicate', snapshot);
1462
+ } catch (error) {
1463
+ run = { status: 'error', provider_id: '', note: 'M1 装载失败:' + error.message };
1464
+ }
1465
+ const block = {
1466
+ status: run.status || 'error',
1467
+ provider_id: run.provider_id || '',
1468
+ applied: false,
1469
+ mode: opts.promote ? 'recommendation' : 'dry-run',
1470
+ decisions: [],
1471
+ summary: { promote: 0, hold: 0, demote: 0 },
1472
+ dropped: [],
1473
+ note: run.note || run.message || '',
1474
+ };
1475
+ if (run.status === 'active' && run.data && typeof run.data === 'object') {
1476
+ const allowed = new Set(Object.keys(registry.skills || {}));
1477
+ const verdicts = new Set(['promote', 'hold', 'demote']);
1478
+ const requested = Array.isArray(run.data.decisions) ? run.data.decisions : [];
1479
+ for (const raw of requested) {
1480
+ const slug = raw && typeof raw === 'object' ? String(raw.slug || '') : '';
1481
+ const score = raw && Number(raw.score);
1482
+ const verdict = raw && String(raw.verdict || '');
1483
+ if (!slug || !allowed.has(slug) || !verdicts.has(verdict) || !Number.isFinite(score) || score < 0 || score > 100) {
1484
+ if (slug) block.dropped.push(slug);
1485
+ continue;
1486
+ }
1487
+ const reasons = Array.isArray(raw.reasons)
1488
+ ? raw.reasons.filter((item) => typeof item === 'string').slice(0, 20)
1489
+ : [];
1490
+ block.decisions.push({
1491
+ slug,
1492
+ verdict,
1493
+ score: Math.round(score),
1494
+ reasons,
1495
+ signals: raw.signals && typeof raw.signals === 'object' && !Array.isArray(raw.signals)
1496
+ ? raw.signals
1497
+ : {},
1498
+ });
1499
+ }
1500
+ block.decisions.sort((left, right) => right.score - left.score || left.slug.localeCompare(right.slug));
1501
+ for (const decision of block.decisions) {
1502
+ if (block.summary[decision.verdict] !== undefined) block.summary[decision.verdict] += 1;
1503
+ }
1504
+ block.applied = block.decisions.length > 0;
1505
+ }
1506
+
1507
+ let reportFile = null;
1508
+ if (opts.promote && block.status === 'active') {
1509
+ const report = {
1510
+ schema: 1,
1511
+ generated_at: snapshot.generated_at,
1512
+ provider_id: block.provider_id,
1513
+ mode: 'recommendation',
1514
+ decisions: block.decisions,
1515
+ memory_candidates: m1FeaturesLib.buildMemoryCandidates(block.decisions, registry),
1516
+ note: '只写本地建议文件;不写元忆、不删除任何内容。',
1517
+ };
1518
+ try {
1519
+ reportFile = m1FeaturesLib.writeAdjudication(report);
1520
+ block.report_file = reportFile;
1521
+ } catch (error) {
1522
+ die('M1 建议文件写入失败: ' + error.message, 1, '请检查 ~/.yottaskills 目录权限。');
1523
+ }
1524
+ }
1525
+
1526
+ if (opts.json) {
1527
+ out(JSON.stringify({ schema: 1, generated_at: snapshot.generated_at, m1: block }, null, 2));
1528
+ return;
1529
+ }
1530
+ out('元阁记忆裁决(M1)');
1531
+ out('状态: ' + m1StatusText(block));
1532
+ if (block.status === 'active') {
1533
+ out('建议汇总: promote ' + block.summary.promote + ' / hold ' + block.summary.hold
1534
+ + ' / demote ' + block.summary.demote + ';丢弃 ' + block.dropped.length);
1535
+ for (const decision of block.decisions) {
1536
+ out(' [' + decision.verdict + '] ' + decision.slug + ' ' + decision.score
1537
+ + (decision.reasons.length ? ' - ' + decision.reasons.join(';') : ''));
1538
+ if (opts.explain) {
1539
+ const signals = decision.signals || {};
1540
+ out(' 信号: used=' + (signals.used || 0) + ' named=' + (signals.named || 0)
1541
+ + ' accepted=' + (signals.accepted || 0) + ' route_hits=' + (signals.route_hits || 0)
1542
+ + ' distinct_pairs=' + (signals.distinct_pairs || 0)
1543
+ + ' description_quality=' + (signals.description_quality || 0)
1544
+ + ' recency=' + (signals.recency || 0));
1545
+ }
1546
+ }
1547
+ if (reportFile) out('建议文件: ' + reportFile);
1548
+ } else if (block.note) {
1549
+ out('说明: ' + block.note);
1550
+ }
1551
+ out('边界: 只建议不删除;不自动写元忆;数据不出本机。');
1552
+ }
1553
+
1366
1554
  /** 装技能后自动 re-index(--no-reindex 关闭):把本次落位结果反映进注册表。best-effort:失败不阻断安装。 */
1367
1555
  function maybeAutoReindex(opts, dest) {
1368
1556
  if (opts.noReindex || opts.dryRun || !dest) return;
@@ -1481,6 +1669,14 @@ function main() {
1481
1669
  if (opts.route && !opts.command) { runRoute(opts); return; }
1482
1670
 
1483
1671
  const command = opts.command || 'install';
1672
+ if (command === 'usage') {
1673
+ runUsage(opts);
1674
+ return;
1675
+ }
1676
+ if (command === 'decide-memory') {
1677
+ runDecideMemory(opts);
1678
+ return;
1679
+ }
1484
1680
  if (command === 'hook') {
1485
1681
  runHook(opts);
1486
1682
  return;
@@ -338,6 +338,7 @@ function createInstaller(deps) {
338
338
  verdict: scan.verdict,
339
339
  decision: scan.decision,
340
340
  scan_policy: scan.policy || null,
341
+ npm_registry_fallback: Boolean(packed.registryFallback),
341
342
  snapshot,
342
343
  });
343
344
  } catch (error) {
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * M1 记忆裁决器:开源侧只做确定性特征快照与建议文件。
5
+ *
6
+ * 评分算法不在本文件,由私有 provider(capability m1.adjudicate)返回。
7
+ * 快照只含技能元数据与聚合计数,不含需求原文、记忆正文、路径、用户名或主机名。
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const usageJournal = require('./usage-journal');
13
+
14
+ const SCHEMA = 1;
15
+ const REPORT_FILE = 'memory-adjudication.json';
16
+
17
+ function nowIso(options) {
18
+ const value = options && options.now;
19
+ return typeof value === 'string' && value ? value : new Date().toISOString();
20
+ }
21
+
22
+ function skillSignal(usage, slug) {
23
+ const source = usage && usage.skills && usage.skills[slug] ? usage.skills[slug] : {};
24
+ const pairs = source.pairs && typeof source.pairs === 'object' && !Array.isArray(source.pairs)
25
+ ? source.pairs
26
+ : {};
27
+ return {
28
+ used: Number(source.used) || 0,
29
+ named: Number(source.named) || 0,
30
+ accepted: Number(source.accepted) || 0,
31
+ route_hits: Number(source.route_hits) || 0,
32
+ distinct_pairs: Object.keys(pairs).length,
33
+ };
34
+ }
35
+
36
+ function buildFeatureSnapshot(registry, usage, options) {
37
+ const skills = Object.values((registry && registry.skills) || {})
38
+ .filter((record) => record && record.slug)
39
+ .map((record) => {
40
+ const slug = String(record.slug);
41
+ return {
42
+ slug,
43
+ version: String(record.version || ''),
44
+ status: String(record.status || 'known'),
45
+ description: String(record.description || ''),
46
+ first_seen: String(record.first_seen || ''),
47
+ last_seen: String(record.last_seen || ''),
48
+ last_signal_at: String((usage && usage.skills && usage.skills[slug] && usage.skills[slug].last_signal_at) || ''),
49
+ pinned: record.pinned === true,
50
+ signals: skillSignal(usage, slug),
51
+ };
52
+ })
53
+ .sort((left, right) => left.slug.localeCompare(right.slug));
54
+ return {
55
+ schema: SCHEMA,
56
+ generated_at: nowIso(options),
57
+ skills,
58
+ };
59
+ }
60
+
61
+ function reportPath(options) {
62
+ const opts = options || {};
63
+ if (opts.file) return path.resolve(opts.file);
64
+ const fromEnv = String(process.env.YOTTA_SKILLS_M1_REPORT_FILE || '').trim();
65
+ if (fromEnv) return path.resolve(fromEnv);
66
+ return path.join(path.dirname(usageJournal.usageFilePath(opts)), REPORT_FILE);
67
+ }
68
+
69
+ function buildMemoryCandidates(decisions, registry) {
70
+ const records = (registry && registry.skills) || {};
71
+ const candidates = [];
72
+ for (const decision of Array.isArray(decisions) ? decisions : []) {
73
+ if (!decision || decision.verdict !== 'promote') continue;
74
+ const record = records[decision.slug] || {};
75
+ const description = String(record.description || '').trim();
76
+ const summary = description
77
+ ? description.slice(0, 180)
78
+ : '暂无描述';
79
+ candidates.push({
80
+ type: 'PREF',
81
+ subject: '技能索引:' + decision.slug,
82
+ statement: '本机长期保留技能 ' + decision.slug + ':' + summary
83
+ + ';M1 评分:' + decision.score + '。',
84
+ source: 'yotta-skills m1',
85
+ weight: 0.8,
86
+ });
87
+ }
88
+ return candidates;
89
+ }
90
+
91
+ function writeAdjudication(report, options) {
92
+ const file = reportPath(options);
93
+ fs.mkdirSync(path.dirname(file), { recursive: true });
94
+ const tmp = file + '.tmp-' + process.pid + '-' + Date.now();
95
+ fs.writeFileSync(tmp, JSON.stringify(report, null, 2) + '\n', 'utf8');
96
+ try {
97
+ fs.renameSync(tmp, file);
98
+ } catch (_) {
99
+ fs.rmSync(file, { force: true });
100
+ fs.renameSync(tmp, file);
101
+ }
102
+ return file;
103
+ }
104
+
105
+ module.exports = {
106
+ SCHEMA,
107
+ REPORT_FILE,
108
+ buildFeatureSnapshot,
109
+ reportPath,
110
+ buildMemoryCandidates,
111
+ writeAdjudication,
112
+ };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
6
+ const FALLBACK_HINT =
7
+ '提示:镜像同步延迟时可用官方源重试 —— 设置环境变量 npm_config_registry=' + OFFICIAL_REGISTRY +
8
+ ',或 YOTTA_SKILLS_NPM_FLAGS=--registry=' + OFFICIAL_REGISTRY;
9
+
10
+ function isNotFound(raw) {
11
+ const text = String(raw || '');
12
+ return /\bE404\b/.test(text) || /\b404 Not Found\b/i.test(text) || /could not be found/i.test(text);
13
+ }
14
+
15
+ function hasRegistryFlag(args) {
16
+ return (args || []).some((arg) => arg === '--registry' || String(arg).startsWith('--registry='));
17
+ }
18
+
19
+ function briefOf(raw) {
20
+ const lines = String(raw || '')
21
+ .split(/\r?\n/)
22
+ .map((line) => line.trim())
23
+ .filter(Boolean);
24
+ return lines.slice(-4).join(' | ');
25
+ }
26
+
27
+ function findTarball(stdout, packDir, readdirSync) {
28
+ const lines = String(stdout || '')
29
+ .split(/\r?\n/)
30
+ .map((line) => line.trim())
31
+ .filter(Boolean);
32
+ let tarball = null;
33
+ for (const line of lines) if (/\.tgz$/.test(line)) tarball = line;
34
+ if (tarball) return tarball;
35
+ try {
36
+ const found = readdirSync(packDir).filter((name) => name.endsWith('.tgz'));
37
+ if (found.length === 1) return found[0];
38
+ } catch (_) {
39
+ /* ignore */
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * 创建 npm pack 执行器。
46
+ *
47
+ * 默认源(国内镜像)拉不到 tarball 时会报 404 —— 此时自动改用官方源重试一次,
48
+ * 并把 registryFallback 标记回传给安装管线(写入证据)。用户已显式指定 registry、
49
+ * 或设置 YOTTA_SKILLS_NO_FALLBACK=1 时不重试。
50
+ */
51
+ function createPackRunner(deps) {
52
+ const spawnSync = deps.spawnSync;
53
+ const specOf = deps.specOf;
54
+ const resolveNpm = deps.resolveNpm;
55
+ const readdirSync = deps.readdirSync || fs.readdirSync;
56
+ const fallbackDisabled = deps.fallbackDisabled || (() => process.env.YOTTA_SKILLS_NO_FALLBACK === '1');
57
+
58
+ return function runNpmPack(skill, opts, packDir) {
59
+ const spec = specOf(skill, opts.pin);
60
+ const baseArgs = ['pack', spec, '--pack-destination', packDir];
61
+ const flags = (process.env.YOTTA_SKILLS_NPM_FLAGS || '').trim();
62
+ if (flags) baseArgs.push(...flags.split(/\s+/));
63
+ const npm = resolveNpm(opts);
64
+ const attempt = (args) => spawnSync(npm.bin, [...npm.prefix, ...args], {
65
+ encoding: 'utf8',
66
+ timeout: 180000,
67
+ maxBuffer: 64 * 1024 * 1024,
68
+ shell: npm.shell,
69
+ });
70
+
71
+ let result = attempt(baseArgs);
72
+ let registryFallback = false;
73
+ if (result.status !== 0) {
74
+ const raw = (result.stderr || result.stdout || 'npm pack 失败').trim();
75
+ if (!fallbackDisabled() && !hasRegistryFlag(baseArgs) && isNotFound(raw)) {
76
+ result = attempt([...baseArgs, '--registry', OFFICIAL_REGISTRY]);
77
+ registryFallback = result.status === 0;
78
+ }
79
+ }
80
+ if (result.status !== 0) {
81
+ const raw = (result.stderr || result.stdout || 'npm pack 失败').trim();
82
+ const brief = briefOf(raw) || 'npm pack 失败';
83
+ const hint = isNotFound(raw) ? '\n ' + FALLBACK_HINT : '';
84
+ return { error: brief + hint, detail: raw };
85
+ }
86
+ const tarball = findTarball(result.stdout, packDir, readdirSync);
87
+ if (!tarball) return { error: '未找到 npm pack 产物(' + spec + ')' };
88
+ const versionMatch = String(tarball).match(/-([0-9]+\.[0-9]+\.[0-9]+)\.tgz$/);
89
+ return {
90
+ tarball: path.join(packDir, tarball),
91
+ resolved: versionMatch ? versionMatch[1] : null,
92
+ spec,
93
+ registryFallback,
94
+ };
95
+ };
96
+ }
97
+
98
+ module.exports = {
99
+ OFFICIAL_REGISTRY,
100
+ FALLBACK_HINT,
101
+ isNotFound,
102
+ hasRegistryFlag,
103
+ createPackRunner,
104
+ };
package/lib/provider.js CHANGED
@@ -94,6 +94,7 @@ function minimalEnv() {
94
94
  const keep = [
95
95
  'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'COMSPEC', 'ComSpec',
96
96
  'TEMP', 'TMP', 'HOME', 'USERPROFILE', 'LANG', 'LC_ALL',
97
+ 'YOTTA_LICENSE_HOME', 'YOTTA_LICENSE_KEYS_DIR', 'YOTTA_LICENSE_BASE_URL', 'YOTTA_LICENSE_SERVER_ID',
97
98
  ];
98
99
  const env = {};
99
100
  for (const key of keep) {
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 元阁本地使用记录(M1 特征来源之一)。
5
+ *
6
+ * 边界:
7
+ * - 只记录技能 slug、信号类型、路由 playbook / confidence 与组合对。
8
+ * - 不记录需求原文、记忆正文、文件内容、用户名、主机名或路径。
9
+ * - 自动路由记录默认关闭;显式 mark 不要求先 enable。
10
+ * - 文件默认 ~/.yottaskills/usage.json,YOTTA_SKILLS_USAGE_FILE 可覆盖。
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+
17
+ const SCHEMA = 1;
18
+ const SIGNALS = ['used', 'named', 'accepted'];
19
+ const MAX_COUNT = 999999;
20
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
21
+
22
+ function defaultState() {
23
+ return {
24
+ schema: SCHEMA,
25
+ enabled: false,
26
+ updated_at: '',
27
+ skills: {},
28
+ last_route: null,
29
+ };
30
+ }
31
+
32
+ function usageFilePath(options) {
33
+ const opts = options || {};
34
+ if (opts.file) return path.resolve(opts.file);
35
+ const fromEnv = String(process.env.YOTTA_SKILLS_USAGE_FILE || '').trim();
36
+ if (fromEnv) return path.resolve(fromEnv);
37
+ return path.join(opts.homeDir || os.homedir(), '.yottaskills', 'usage.json');
38
+ }
39
+
40
+ function bump(value) {
41
+ const n = Number.isFinite(value) ? Math.floor(value) : 0;
42
+ return Math.min(MAX_COUNT, Math.max(0, n) + 1);
43
+ }
44
+
45
+ function normalizeSkill(raw) {
46
+ const skill = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
47
+ const pairs = {};
48
+ if (skill.pairs && typeof skill.pairs === 'object' && !Array.isArray(skill.pairs)) {
49
+ for (const key of Object.keys(skill.pairs)) {
50
+ if (!SLUG_RE.test(key)) continue;
51
+ const n = Number(skill.pairs[key]);
52
+ if (Number.isFinite(n) && n > 0) pairs[key] = Math.min(MAX_COUNT, Math.floor(n));
53
+ }
54
+ }
55
+ return {
56
+ used: Math.min(MAX_COUNT, Math.max(0, Math.floor(Number(skill.used) || 0))),
57
+ named: Math.min(MAX_COUNT, Math.max(0, Math.floor(Number(skill.named) || 0))),
58
+ accepted: Math.min(MAX_COUNT, Math.max(0, Math.floor(Number(skill.accepted) || 0))),
59
+ route_hits: Math.min(MAX_COUNT, Math.max(0, Math.floor(Number(skill.route_hits) || 0))),
60
+ last_signal_at: typeof skill.last_signal_at === 'string' ? skill.last_signal_at : '',
61
+ pairs,
62
+ };
63
+ }
64
+
65
+ function normalizeState(raw) {
66
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw) || raw.schema !== SCHEMA) {
67
+ return defaultState();
68
+ }
69
+ const state = defaultState();
70
+ state.enabled = raw.enabled === true;
71
+ state.updated_at = typeof raw.updated_at === 'string' ? raw.updated_at : '';
72
+ if (raw.skills && typeof raw.skills === 'object' && !Array.isArray(raw.skills)) {
73
+ for (const slug of Object.keys(raw.skills)) {
74
+ if (!SLUG_RE.test(slug)) continue;
75
+ state.skills[slug] = normalizeSkill(raw.skills[slug]);
76
+ }
77
+ }
78
+ if (raw.last_route && typeof raw.last_route === 'object' && !Array.isArray(raw.last_route)) {
79
+ const skills = Array.isArray(raw.last_route.skills)
80
+ ? raw.last_route.skills.filter((slug) => typeof slug === 'string' && SLUG_RE.test(slug))
81
+ : [];
82
+ state.last_route = {
83
+ at: typeof raw.last_route.at === 'string' ? raw.last_route.at : '',
84
+ playbook: typeof raw.last_route.playbook === 'string' ? raw.last_route.playbook : '',
85
+ confidence: typeof raw.last_route.confidence === 'string' ? raw.last_route.confidence : '',
86
+ skills,
87
+ };
88
+ }
89
+ return state;
90
+ }
91
+
92
+ function readUsage(options) {
93
+ const file = usageFilePath(options);
94
+ try {
95
+ return normalizeState(JSON.parse(fs.readFileSync(file, 'utf8')));
96
+ } catch (_) {
97
+ return defaultState();
98
+ }
99
+ }
100
+
101
+ function writeUsage(state, options) {
102
+ const file = usageFilePath(options);
103
+ const next = normalizeState(Object.assign(defaultState(), state));
104
+ next.updated_at = new Date().toISOString();
105
+ fs.mkdirSync(path.dirname(file), { recursive: true });
106
+ const tmp = file + '.tmp-' + process.pid + '-' + Date.now();
107
+ fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', 'utf8');
108
+ try {
109
+ fs.renameSync(tmp, file);
110
+ } catch (_) {
111
+ fs.rmSync(file, { force: true });
112
+ fs.renameSync(tmp, file);
113
+ }
114
+ return file;
115
+ }
116
+
117
+ function setEnabled(enabled, options) {
118
+ const state = readUsage(options);
119
+ state.enabled = enabled === true;
120
+ writeUsage(state, options);
121
+ return state;
122
+ }
123
+
124
+ function ensureSkill(state, slug) {
125
+ if (!state.skills[slug]) {
126
+ state.skills[slug] = normalizeSkill({});
127
+ }
128
+ return state.skills[slug];
129
+ }
130
+
131
+ function markUsage(skill, signal, options) {
132
+ const slug = String(skill || '').trim().toLowerCase();
133
+ if (!SLUG_RE.test(slug)) throw new Error('非法 skill slug:' + String(skill || ''));
134
+ if (SIGNALS.indexOf(signal) === -1) throw new Error('非法 signal:' + String(signal || ''));
135
+ const state = readUsage(options);
136
+ const record = ensureSkill(state, slug);
137
+ record[signal] = bump(record[signal]);
138
+ record.last_signal_at = new Date().toISOString();
139
+ writeUsage(state, options);
140
+ return state;
141
+ }
142
+
143
+ function uniqueSkills(skills) {
144
+ const seen = new Set();
145
+ const out = [];
146
+ for (const item of Array.isArray(skills) ? skills : []) {
147
+ const slug = String(item || '').trim().toLowerCase();
148
+ if (!SLUG_RE.test(slug) || seen.has(slug)) continue;
149
+ seen.add(slug);
150
+ out.push(slug);
151
+ }
152
+ return out;
153
+ }
154
+
155
+ function recordRoute(input, options) {
156
+ const state = readUsage(options);
157
+ if (!state.enabled) return { recorded: false, reason: 'disabled' };
158
+ const data = input && typeof input === 'object' ? input : {};
159
+ const skills = uniqueSkills(data.skills);
160
+ if (!skills.length) return { recorded: false, reason: 'empty' };
161
+ for (const slug of skills) {
162
+ const record = ensureSkill(state, slug);
163
+ record.route_hits = bump(record.route_hits);
164
+ }
165
+ for (let i = 0; i < skills.length; i++) {
166
+ for (let j = i + 1; j < skills.length; j++) {
167
+ const left = ensureSkill(state, skills[i]);
168
+ const right = skills[j];
169
+ left.pairs[right] = bump(left.pairs[right]);
170
+ const rightRecord = ensureSkill(state, right);
171
+ rightRecord.pairs[skills[i]] = bump(rightRecord.pairs[skills[i]]);
172
+ }
173
+ }
174
+ state.last_route = {
175
+ at: new Date().toISOString(),
176
+ playbook: typeof data.playbook === 'string' ? data.playbook : '',
177
+ confidence: typeof data.confidence === 'string' ? data.confidence : '',
178
+ skills,
179
+ };
180
+ writeUsage(state, options);
181
+ return { recorded: true, skills };
182
+ }
183
+
184
+ function resetUsage(options) {
185
+ const file = usageFilePath(options);
186
+ fs.rmSync(file, { force: true });
187
+ return file;
188
+ }
189
+
190
+ module.exports = {
191
+ SCHEMA,
192
+ SIGNALS,
193
+ usageFilePath,
194
+ defaultState,
195
+ readUsage,
196
+ writeUsage,
197
+ setEnabled,
198
+ markUsage,
199
+ recordRoute,
200
+ resetUsage,
201
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-skills",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/references/faq.md CHANGED
@@ -113,3 +113,15 @@ npx -y @yottameta/yotta-skills install <slug> --dir /path/to/skills
113
113
  不会。元阁只给建议:`--route` 输出缺失技能与安装命令,安装动作由你执行或确认后才发生;
114
114
  写客户端 `mcpServers` 配置、把编排护栏写进全局记忆(如 `AGENTS.md` / `CLAUDE.md`)都需要**事先明确同意**,
115
115
  并会先展示目标文件与完整文本。你拒绝写入时,元阁不写任何文件,直接以 CLI 方式继续工作。
116
+
117
+ ## 15. `decide-memory` 会替我删除技能或写记忆吗?
118
+
119
+ 不会。`decide-memory` 默认只读,输出 `promote / hold / demote` 建议、分数与信号明细;
120
+ `--promote` 只写本地 `~/.yottaskills/memory-adjudication.json` 建议文件,并生成私密 `PREF` 记忆候选。
121
+ 元忆写入由你或 AI 再显式调用元忆完成;技能本体与记忆都不会被自动删除。
122
+
123
+ ## 16. `usage` 会记录我的需求原文吗?
124
+
125
+ 不会。使用记录默认关闭;执行 `usage enable` 后才记录 `--route` 的结构化 route_hits 与组合对。
126
+ 记录只含技能 slug、时间、信号类型、playbook / confidence 与组合对,不含需求原文、记忆正文、路径或身份信息。
127
+ `usage disable` 停止记录,`usage reset --yes` 清空记录。
@@ -8,7 +8,8 @@
8
8
  对每个待装技能:
9
9
 
10
10
  1. 读清单(`skills.json`,默认随包;`YOTTA_SKILLS_MANIFEST` 可覆盖);
11
- 2. `npm pack <pkg>@<spec> --pack-destination <临时目录>`;
11
+ 2. `npm pack <pkg>@<spec> --pack-destination <临时目录>`(默认源返回 404 时自动改用官方源重试一次,
12
+ 证据记 `npm_registry_fallback`);
12
13
  3. 系统 `tar -xzf` 解压到临时目录(产物应有 `SKILL.md`);
13
14
  4. 读取包内 `skill-manifest.json`;没有 manifest 时使用 `skills.json` 的家族默认契约;
14
15
  5. 校验 slug / package / version / 权限 / 生命周期脚本路径;
@@ -108,6 +109,13 @@
108
109
  - 评估证据写入 `~/.yottaskills/hook-log.jsonl`,绑定记录写入 `~/.yottaskills/hook-bindings.json`。
109
110
  - `native-audit` 不等于强制;缺少能力或证据时必须显示 `explicit-unverified`。
110
111
 
112
+ ## M1 记忆裁决
113
+
114
+ - `decide-memory` 默认只读,调用可选本地 provider(capability `m1.adjudicate`)输出 `promote / hold / demote` 建议。
115
+ - `--promote` 只写 `~/.yottaskills/memory-adjudication.json` 建议文件;不写元忆、不删除技能。
116
+ - `usage` 默认关闭;显式 `usage enable` 后才记录结构化使用信号,不记录需求原文。
117
+ - 未配置 / 未授权 / 超时 / 非法输出时 fail-open,退出码保持 0。
118
+
111
119
  ## 快照与回滚
112
120
 
113
121
  - 旧版本快照:`~/.yottaskills/snapshots/<slug>/<timestamp>-<version>-<随机后缀>/`;
@@ -1,7 +1,9 @@
1
- # 扩展提供方协议 v1(元阁 · capability `o1.route`)
1
+ # 扩展提供方协议 v1(元阁 · capability `o1.route` / `m1.adjudicate`)
2
2
 
3
- 元阁可以可选地调用一个由用户显式配置的**本地扩展提供方(provider)**,由它在已装技能白名单内
4
- 对静态路由结果做增补 / 重排。未配置提供方时,`--route` 的行为与输出与之前完全一致。
3
+ 元阁可以可选地调用一个由用户显式配置的**本地扩展提供方(provider)**:
4
+
5
+ - `o1.route`:在已装技能白名单内对静态路由结果做增补 / 重排;未配置时 `--route` 行为与输出与之前完全一致。
6
+ - `m1.adjudicate`:对已装技能做 M1 记忆裁决,返回 `promote / hold / demote` 只读建议;未配置时 `decide-memory` 返回 `not_installed`,不写任何文件。
5
7
 
6
8
  ## 1. 配置
7
9
 
@@ -99,3 +101,105 @@ CLI 文本输出只在状态非 `not_installed` 时多一行「动态路由: ...
99
101
  - 元阁不替 provider 联网;provider 自身行为由它自己的包声明。
100
102
  - 删除 `provider.json` 即回到纯静态路由,无残留依赖。
101
103
  - provider 输出只当数据使用:白名单外的 slug、非法结构一律丢弃,不作为指令执行。
104
+
105
+ ## 6. capability `m1.adjudicate`
106
+
107
+ ### 6.1 触发
108
+
109
+ 只在用户显式执行 `yotta-skills decide-memory`(CLI)或 MCP `decide_memory` 时触发。
110
+ 安装 / 更新 / 盘点 / re-index / doctor 等路径不执行 M1 provider。
111
+
112
+ ### 6.2 请求
113
+
114
+ 元阁先把注册表与本地使用记录转成确定性特征快照,再发给 provider:
115
+
116
+ ```json
117
+ {
118
+ "schema": 1,
119
+ "capability": "m1.adjudicate",
120
+ "request_id": "<uuid>",
121
+ "payload": {
122
+ "schema": 1,
123
+ "generated_at": "2026-09-30T00:00:00Z",
124
+ "skills": [
125
+ {
126
+ "slug": "yotta-memory",
127
+ "version": "0.19.0",
128
+ "status": "known",
129
+ "description": "文件式智能体记忆...",
130
+ "first_seen": "2026-08-23T00:00:00Z",
131
+ "last_seen": "2026-09-30T00:00:00Z",
132
+ "last_signal_at": "2026-09-30T00:00:00Z",
133
+ "pinned": false,
134
+ "signals": {
135
+ "used": 2,
136
+ "named": 0,
137
+ "accepted": 1,
138
+ "route_hits": 5,
139
+ "distinct_pairs": 3
140
+ }
141
+ }
142
+ ]
143
+ }
144
+ }
145
+ ```
146
+
147
+ payload 不含需求原文、记忆正文、凭据、主机名、用户名、来源目录或任意文件路径。
148
+
149
+ ### 6.3 响应
150
+
151
+ ```json
152
+ {
153
+ "ok": true,
154
+ "capability": "m1.adjudicate",
155
+ "data": {
156
+ "policy": {
157
+ "version": "m1-mvp-1",
158
+ "promote_threshold": 60,
159
+ "hold_threshold": 30,
160
+ "cooling_days": 7
161
+ },
162
+ "decisions": [
163
+ {
164
+ "slug": "yotta-memory",
165
+ "verdict": "promote",
166
+ "score": 78,
167
+ "reasons": ["used x2:+24", "组合出现 x3:+9"],
168
+ "signals": {
169
+ "used": 2,
170
+ "named": 0,
171
+ "accepted": 1,
172
+ "route_hits": 5,
173
+ "distinct_pairs": 3,
174
+ "description_quality": 12,
175
+ "recency": 10
176
+ }
177
+ }
178
+ ]
179
+ }
180
+ }
181
+ ```
182
+
183
+ 元阁只接受:slug 在本次注册表白名单内、verdict 为 `promote / hold / demote`、score 为 0-100。
184
+ 未知 slug / 非法 verdict / 非法 score 一律丢弃并记入 `dropped`。
185
+
186
+ ### 6.4 授权与回落
187
+
188
+ | 状态 | 触发 | 元阁行为 |
189
+ | --- | --- | --- |
190
+ | `not_installed` | 无配置 / 无匹配 capability | `decide-memory` 返回 `not_installed`,不写文件 |
191
+ | `active` | provider 返回合法 decisions | 展示建议;`--promote` 可写本地建议文件 |
192
+ | `license_required` | provider 明确返回该 code | 显示需授权;不阻断其他能力 |
193
+ | `timeout` / `invalid_output` / `error` | 超时 / 非法 JSON / 执行失败 | 显示状态;不写文件、不阻断 |
194
+
195
+ provider 内部应使用路线 B 授权门(capability `m1.adjudicate`)。未授权时返回:
196
+
197
+ ```json
198
+ { "ok": false, "code": "license_required", "message": "该能力需要授权后使用" }
199
+ ```
200
+
201
+ ### 6.5 建议文件与元忆边界
202
+
203
+ `--promote` 只写 `~/.yottaskills/memory-adjudication.json`,包含 decisions 与 `memory_candidates`。
204
+ `memory_candidates` 默认使用私密 `PREF`,不自动写元忆;用户或 AI 需要再显式调用元忆写入。
205
+ MCP `decide_memory` 始终只读,不写建议文件、不写元忆。
@@ -108,8 +108,8 @@ npx -y @yottameta/yotta-skills rollback --slug yotta-memory --agent codex
108
108
 
109
109
  ## 10. 常见问题
110
110
 
111
- - **npmmirror 全新包 404**:镜像同步有延迟。设置环境变量 `YOTTA_SKILLS_NPM_FLAGS` 为
112
- `--registry=https://registry.npmjs.org/`(国内需代理)再执行,或等待镜像缓存后重试。
111
+ - **npmmirror 全新包 404**:镜像同步有延迟时安装器会自动改用官方源重试一次(默认行为);
112
+ 如需固定在指定源,设置 `YOTTA_SKILLS_NPM_FLAGS=--registry=...` 或 `YOTTA_SKILLS_NO_FALLBACK=1`。
113
113
  - **`--agent` 报未收录**:改用 `--dir` 指到该智能体的技能目录(`.agents/skills` 不是
114
114
  通用目录)。
115
115
  - **某技能安装失败**:汇总报告给出失败原因;可单装该技能排查:
@@ -152,6 +152,27 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄"
152
152
  - `--json` 输出机器可读结果(含新增 / 更新 / 消失),适合脚本与钩子。
153
153
  - 如果本机还装了非元阁家族技能,`--route` 会额外列出「其他已装技能候选」:只按 frontmatter description 与需求文本做本地机械匹配,标注来源、得分、命中词项与扫描状态(默认未扫描),不读取全文指令、不自动调用;使用/安装前请先做装前安全扫描。
154
154
 
155
+ ## M1 记忆裁决(decide-memory)
156
+
157
+ 当你想判断「哪些已装技能值得长期记住」,可以运行:
158
+
159
+ ```bash
160
+ npx -y @yottameta/yotta-skills decide-memory --explain
161
+ npx -y @yottameta/yotta-skills decide-memory --promote --json
162
+ ```
163
+
164
+ `decide-memory` 默认只读,输出每个技能的 `promote / hold / demote` 建议、分数与信号明细;
165
+ `--promote` 只写 `~/.yottaskills/memory-adjudication.json` 建议文件,不写元忆、不删除内容。
166
+ 评分由用户显式配置的本地 provider(capability `m1.adjudicate`)完成;未配置 / 未授权 / 超时 / 非法输出时只返回状态,不阻断其他能力。
167
+
168
+ 使用记录默认关闭,只有显式开启后才记录结构化信号:
169
+
170
+ ```bash
171
+ npx -y @yottameta/yotta-skills usage status
172
+ npx -y @yottameta/yotta-skills usage enable
173
+ npx -y @yottameta/yotta-skills usage mark --skill yotta-memory --signal used
174
+ ```
175
+
155
176
  ## 运行时 hook 适配(hook)
156
177
 
157
178
  技能 manifest 只声明六个统一事件的要求;元阁负责能力探测、确定性评估、证据留痕和降级标注:
@@ -7,6 +7,7 @@ stdio MCP server(JSON-RPC 2.0,换行分隔),把元阁技能扫描核心
7
7
  describe_skill 查看单个技能详情(slug / 版本 / 功能 / 来源)
8
8
  reindex 强制重新扫描并更新注册表
9
9
  route_request 按需求摘要给出静态编排路由建议
10
+ decide_memory M1 记忆裁决只读建议(不写元忆、不删除)
10
11
 
11
12
  自包含原则:扫描由元阁自带核心(bin/yotta-skills.js --inventory / --reindex)完成,
12
13
  不依赖任何元技能;数据只写本机注册表,默认 ~/.yottaskills/registry.json,
@@ -26,7 +27,7 @@ import subprocess
26
27
  import sys
27
28
  from pathlib import Path
28
29
 
29
- VERSION = "0.20.1"
30
+ VERSION = "0.21.0"
30
31
  TOOL_NAME = "yotta-skills"
31
32
  CN_NAME = "元阁"
32
33
  MCP_PROTOCOL_MODERN = "2026-07-28"
@@ -163,11 +164,24 @@ def _tool_route(arguments):
163
164
  "isError": False}
164
165
 
165
166
 
167
+ def _tool_decide_memory(arguments):
168
+ if arguments.get("dry_run") is False:
169
+ return _tool_error("decide_memory 只读:MCP 不写建议文件,也不写元忆")
170
+ stdout = _run_cli(["decide-memory", "--json"])
171
+ try:
172
+ payload = json.loads(stdout)
173
+ except json.JSONDecodeError as e:
174
+ return _tool_error("decide_memory 输出解析失败:%s" % e)
175
+ return {"content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False, indent=2)}],
176
+ "isError": False}
177
+
178
+
166
179
  TOOL_HANDLERS = {
167
180
  "list_installed_skills": _tool_list,
168
181
  "describe_skill": _tool_describe,
169
182
  "reindex": _tool_reindex,
170
183
  "route_request": _tool_route,
184
+ "decide_memory": _tool_decide_memory,
171
185
  }
172
186
 
173
187
 
@@ -201,6 +215,16 @@ def mcp_tools():
201
215
  {"request": {"type": "string", "description": "用户需求摘要"}},
202
216
  ["request"],
203
217
  ),
218
+ _tool_spec(
219
+ "decide_memory",
220
+ "M1 记忆裁决只读建议:返回技能评分、promote / hold / demote 建议与信号明细。"
221
+ "只读,不写元忆、不删除技能、不写建议文件;需要写建议文件时使用 CLI --promote。数据不出本机。",
222
+ {
223
+ "dry_run": {"type": "boolean", "description": "固定为 true;MCP 只读"},
224
+ "explain": {"type": "boolean", "description": "是否要求解释信号明细(JSON 始终含 signals)"},
225
+ },
226
+ [],
227
+ ),
204
228
  ]
205
229
 
206
230
 
@@ -261,7 +285,7 @@ def handle_message(msg):
261
285
  "result": _modern_ok({
262
286
  "supportedVersions": [MCP_PROTOCOL_MODERN],
263
287
  "capabilities": {"tools": {}},
264
- "instructions": "元阁 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):本机技能盘点与静态编排路由 list_installed_skills/describe_skill/reindex/route_request;数据不出本机。",
288
+ "instructions": "元阁 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):本机技能盘点、静态编排路由与 M1 记忆裁决只读建议 list_installed_skills/describe_skill/reindex/route_request/decide_memory;数据不出本机。",
265
289
  }, (3600000, "public")),
266
290
  }
267
291
  if method == "tools/list":
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-skills",
4
4
  "name": "元阁",
5
5
  "package": "@yottameta/yotta-skills",
6
- "version": "0.20.1",
6
+ "version": "0.21.0",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true