diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index aa0c720..498fd90 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "llmdoc", "source": "./", "description": "Persistent engineering context powered by the llmdoc CLI and progressive MDX knowledge retrieval", - "version": "3.5.2" + "version": "3.5.3" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d34bbb6..b3ce8db 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "llmdoc", "description": "Persistent engineering context powered by the llmdoc CLI and progressive MDX knowledge retrieval", - "version": "3.5.2", + "version": "3.5.3", "author": { "name": "DJJ & Danniel" } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index dcae0bc..2f6c1fe 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "llmdoc", - "version": "3.5.2", + "version": "3.5.3", "description": "Persistent engineering context powered by the llmdoc CLI and progressive MDX knowledge retrieval", "skills": "./.agents/skills/" } diff --git a/README.md b/README.md index f8cc49c..e7d37f1 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,10 @@ counts, and Git state. `context --files` evaluates every input independently and reports `unmappedFiles`; a non-empty impacted set never hides unmapped siblings. +Plugin lifecycle hooks invoke the same scoped CLI through an npm package alias. +This prevents a same-name local or `file:` dependency without a built bin from +shadowing the hook runtime; normal interactive commands keep the shorter form. + ```bash # Map the knowledge surface npx -y @tokenroll/llmdoc tree --docs diff --git a/README.zh-CN.md b/README.zh-CN.md index ba1e0a7..5d68af8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -108,6 +108,9 @@ flowchart LR `context --files` 会逐个判断输入并报告 `unmappedFiles`;非空的 impacted 结果不会再掩盖同批查询中未映射的其他路径。 +插件生命周期 hook 会通过 npm package alias 调用同一个 scoped CLI,避免宿主仓库中 +同名但尚未构建 bin 的本地或 `file:` 依赖遮蔽 hook runtime;日常交互命令仍使用上面的短写法。 + ```bash # 展开知识地图 npx -y @tokenroll/llmdoc tree --docs diff --git a/cli/package-lock.json b/cli/package-lock.json index 5cd71e8..c8d9e68 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tokenroll/llmdoc", - "version": "3.5.2", + "version": "3.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tokenroll/llmdoc", - "version": "3.5.2", + "version": "3.5.3", "dependencies": { "ajv": "^8.17.1", "commander": "^13.1.0", diff --git a/cli/package.json b/cli/package.json index 5b0103f..f0799ca 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@tokenroll/llmdoc", - "version": "3.5.2", + "version": "3.5.3", "description": "V3 runtime CLI for llmdoc repositories", "homepage": "https://github.com/TokenRollAI/llmdoc", "repository": { diff --git a/docs/v3-design/05-packaging.md b/docs/v3-design/05-packaging.md index 0d60930..057ff76 100644 --- a/docs/v3-design/05-packaging.md +++ b/docs/v3-design/05-packaging.md @@ -17,7 +17,7 @@ llmdoc-repository/ # 根 = 标准 Claude Code plugin │ └── upgrade/SKILL.md # disable-model-invocation: true,仅显式调用 ├── agents/ # investigator.md / reflector.md / recorder.md(角色契约) ├── hooks/ -│ └── hooks.json # SessionStart/Stop/PreCompact → npx @tokenroll/llmdoc hook * +│ └── hooks.json # SessionStart/Stop/PreCompact → npm alias → scoped llmdoc hook * ├── cli/ # npm 包 @tokenroll/llmdoc,暴露 bin llmdoc(Runtime 实体) │ ├── src/ │ ├── schemas/ # front matter / meta.json 的 JSON Schema(事实源) @@ -53,6 +53,7 @@ llmdoc-repository/ # 根 = 标准 Claude Code plugin - upgrade 入口禁止隐式调用,正文惰性加载; - hook 只注入短状态信号,失败不阻塞开发; +- hook 的 `npx --package` 使用 npm alias 指向 `@tokenroll/llmdoc`,避免宿主仓库中同名但缺失 bin 的 local/file dependency 抢先满足解析; - 插件不复制 CLI 已承担的机械逻辑。 ## 3. CI diff --git a/hooks/hooks.json b/hooks/hooks.json index 4108b86..bb8c9f1 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,12 +1,12 @@ { - "description": "V3 llmdoc hooks delegated to the scoped llmdoc CLI. Missing packages are installed non-interactively; SessionStart may return plain text, while Stop and PreCompact return JSON on success.", + "description": "V3 llmdoc hooks delegated to the scoped llmdoc CLI through an npm alias that cannot be shadowed by a same-name local dependency. Missing packages are installed non-interactively; SessionStart may return plain text, while Stop and PreCompact return JSON on success.", "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", - "command": "npx -y @tokenroll/llmdoc hook session-start", + "command": "npx -y --package=@tokenroll/llmdoc-hook-runtime@npm:@tokenroll/llmdoc -- llmdoc hook session-start", "additionalContextLimit": 1200 } ] @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "npx -y @tokenroll/llmdoc hook stop" + "command": "npx -y --package=@tokenroll/llmdoc-hook-runtime@npm:@tokenroll/llmdoc -- llmdoc hook stop" } ] } @@ -27,7 +27,7 @@ "hooks": [ { "type": "command", - "command": "npx -y @tokenroll/llmdoc hook compact" + "command": "npx -y --package=@tokenroll/llmdoc-hook-runtime@npm:@tokenroll/llmdoc -- llmdoc hook compact" } ] } diff --git a/llmdoc/meta.json b/llmdoc/meta.json index 63965ea..7c5669f 100644 --- a/llmdoc/meta.json +++ b/llmdoc/meta.json @@ -6,19 +6,19 @@ }, "documents": { "architecture.mdx": { - "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" + "validatedRevision": "255d8672b5c4de14149324c15a017392b0865488" }, "cli-runtime/state-and-validation.mdx": { - "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" + "validatedRevision": "255d8672b5c4de14149324c15a017392b0865488" }, "cli-runtime/retrieval-and-mutation.mdx": { "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" }, "plugin-packaging/claude-and-codex.mdx": { - "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" + "validatedRevision": "255d8672b5c4de14149324c15a017392b0865488" }, "plugin-packaging/development-and-release.mdx": { - "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" + "validatedRevision": "255d8672b5c4de14149324c15a017392b0865488" }, "workflows/init-and-update.mdx": { "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" @@ -27,7 +27,7 @@ "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" }, "website/product-and-deployment.mdx": { - "validatedRevision": "2b114c1e18652ea1ef337848f5837a67157bf057" + "validatedRevision": "255d8672b5c4de14149324c15a017392b0865488" } }, "convergence": { diff --git a/llmdoc/plugin-packaging/claude-and-codex.mdx b/llmdoc/plugin-packaging/claude-and-codex.mdx index 2a9ebd9..976d51a 100644 --- a/llmdoc/plugin-packaging/claude-and-codex.mdx +++ b/llmdoc/plugin-packaging/claude-and-codex.mdx @@ -1,5 +1,5 @@ --- -description: Claude canonical surface、Codex 生成面、共同 CLI runtime 与跨宿主一致性不变量。 +description: Claude canonical surface、Codex 生成面,以及防同名依赖遮蔽的 lifecycle hook runtime 启动边界。 kind: architecture code: paths: @@ -22,16 +22,18 @@ code: # Claude 与 Codex 表面 -## 真相源与共同 runtime +## 真相源与 runtime 启动边界 Claude 根插件是手工维护的 canonical surface:skills 定义 Retrieval/Reflection/Stable Knowledge Gate 与显式工作流,三个 agent 定义调查、候选捕获和正式写入边界。Codex 的 skills、agents、manifest 与 marketplace 是转换产物,不得成为独立设计源。 -两种宿主都把确定性行为收敛到外部 `@tokenroll/llmdoc` CLI。统一调用形式是 `npx -y @tokenroll/llmdoc `:不向消费仓库写依赖;需要复现时在包名中固定版本;不得退回会解析错误包名或交互挂起的裸调用。 +两种宿主都把确定性行为收敛到外部 `@tokenroll/llmdoc` CLI。普通交互命令使用 `npx -y @tokenroll/llmdoc `:不向消费仓库写依赖;需要复现时在包名中固定版本;不得退回会解析错误包名或交互挂起的裸调用。 + +生命周期 hook 属于不同的启动边界:宿主从消费仓库的 cwd 调用 npm,而该仓库可能暴露同名的 local/`file:` 依赖。npm 会让这个依赖先满足普通 package identity;即使其 bin 尚未构建或不是应运行的发布 runtime,进程也不会自动回退到 registry CLI。hook 因此必须通过一个不同 identity 的 scoped npm alias 请求 `@tokenroll/llmdoc`,再调用它暴露的 `llmdoc hook *` bin。alias 只隔离 package 解析,不改变共同 runtime、参数或输出契约,也不应用于日常交互命令。 ## 生成与一致性不变量 转换必须在临时副本生成后替换式同步,因为生成器不保证清除陈旧输出。Codex 角色文本不手工维护;宿主特有 front matter/TOML 可以不同,实际 skill 与 agent 指令正文必须与 Claude canonical 一致。被 skill 按需加载的 reference 同样属于可执行 prompt surface:Codex 镜像必须包含相同正文,相关 skill/agent 也必须保留可发现的加载路径。 -`scripts/check-codex-surface.mjs` 在 CI 中机械校验版本/marketplace 身份、五个 skill 与三个 agent 的正文一致性、按需 reference 的正文 parity 与加载路径、Reflection Gate 约束和 hook 调用。它还分别遍历 `skills/` 与 `.agents/skills/`,用 `gray-matter` 解析每份 `SKILL.md` 的 YAML front matter,并要求 `name`、`description` 是非空字符串;这是两侧各自的语法门槛,不替代跨宿主正文 parity。人工 checklist 只补宿主 UI policy、知识拓扑质量与信任模型等无法从正文等价判断的部分。 +`scripts/check-codex-surface.mjs` 在 CI 中机械校验版本/marketplace 身份、五个 skill 与三个 agent 的正文一致性、按需 reference 的正文 parity 与加载路径、Reflection Gate 约束和三种 lifecycle hook 的 alias 调用。它还分别遍历 `skills/` 与 `.agents/skills/`,用 `gray-matter` 解析每份 `SKILL.md` 的 YAML front matter,并要求 `name`、`description` 是非空字符串;这是两侧各自的语法门槛,不替代跨宿主正文 parity。人工 checklist 只补宿主 UI policy、知识拓扑质量与信任模型等无法从正文等价判断的部分。 -hooks 位于共享插件根,一份配置供两种宿主使用;它们保持 fail-open、只读,且由安装宿主按自己的信任模型启用。 +hooks 位于共享插件根,一份配置供两种宿主使用;它们保持只读,且由安装宿主按自己的信任模型启用。CLI 成功启动后 hook 逻辑保持 fail-open,但进程启动前的 npm 解析或 registry 失败不受这层保护;保留 alias 启动边界是避免本地同名依赖把错误暴露在宿主生命周期中的必要条件。 diff --git a/llmdoc/plugin-packaging/development-and-release.mdx b/llmdoc/plugin-packaging/development-and-release.mdx index b6c6882..98db742 100644 --- a/llmdoc/plugin-packaging/development-and-release.mdx +++ b/llmdoc/plugin-packaging/development-and-release.mdx @@ -1,5 +1,5 @@ --- -description: 开发前分支同步、消费边界、验证与发布表面的同步不变量。 +description: 开发前分支同步、消费边界、生命周期 hook 发布验证与发布表面的同步不变量。 kind: reference code: paths: @@ -8,6 +8,7 @@ code: - .github/workflows/ci.yml - .github/workflows/codex-plugin-scanner.yml - .github/workflows/release.yml + - scripts/check-prompt-budget.mjs - README.md - README.zh-CN.md - docs/agent-integration.md @@ -29,6 +30,10 @@ CLI 命令或输出语义变化必须在同一 change-set 同步:canonical ski `scripts/check-codex-surface.mjs` 负责版本、身份与生成正文 parity;CI 还运行 lint、typecheck、test、build、prompt budget、dogfood validate 和消费者安装 smoke。自动化通过后仍需审查语义是否在所有公开表面一致,特别是宿主 UI policy 与授权边界。 +## Lifecycle hook 发布验证 + +hook 的发布边界包含宿主 cwd 中的 npm package 解析、发布包的 bin 链接和 CLI 启动,而不只是 `hook` 命令进入后的业务逻辑。发布验证必须从 `hooks/hooks.json` 读取并执行真实 lifecycle 命令,在可见同名 local/`file:` 依赖的消费方环境中确认 alias 仍选择发布 runtime,并验证 SessionStart 的文本与 Stop/PreCompact 的 JSON 契约。静态命令前缀检查、直接运行仓库 `dist` 或只调用内部 hook handler 都不能替代这项验证,因为它们绕过了发生遮蔽的启动层。 + ## 外部宿主指引复核不变量 公开的外部宿主安装或启用指导在发布前必须依据宿主当时的官方用户文档复核。仓库中存在或本地可调用的命令,不足以单独成为主快速入门路径的依据;可选、恢复性或其他条件性步骤必须保留其条件语义。具体命令、UI 文案与步骤序列属于易变事实,不在稳定知识中固定。 diff --git a/package-lock.json b/package-lock.json index e6659d5..f15731c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tokenroll/llmdoc-repository", - "version": "3.5.2", + "version": "3.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tokenroll/llmdoc-repository", - "version": "3.5.2", + "version": "3.5.3", "devDependencies": { "@tokenroll/llmdoc": "file:cli", "gray-matter": "^4.0.3" @@ -17,7 +17,7 @@ }, "cli": { "name": "@tokenroll/llmdoc", - "version": "3.5.2", + "version": "3.5.3", "dev": true, "dependencies": { "ajv": "^8.17.1", diff --git a/package.json b/package.json index 2f899a3..f6ce62b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tokenroll/llmdoc-repository", - "version": "3.5.2", + "version": "3.5.3", "private": true, "description": "Development workspace for the llmdoc V3 CLI and plugin surfaces", "scripts": { diff --git a/scripts/check-codex-surface.mjs b/scripts/check-codex-surface.mjs index 7e56f62..5da381b 100644 --- a/scripts/check-codex-surface.mjs +++ b/scripts/check-codex-surface.mjs @@ -221,16 +221,25 @@ for (const name of ["investigator", "reflector", "recorder"]) { } } -// 6) hooks.json:合法 JSON 且所有命令使用 scoped 包名 + 非交互安装确认 +// 6) hooks.json:合法 JSON;npm alias 强制从 scoped registry package 解析 runtime, +// 避免消费仓库中同名但缺少 bin 的本地/file dependency 遮蔽 hook CLI。 const hooks = readJson("hooks/hooks.json"); if (hooks) { - const commands = JSON.stringify(hooks).match(/"command":"([^"]+)"/g) ?? []; - for (const raw of commands) { - const command = raw.slice(11, -1); - if (!command.startsWith("npx -y @tokenroll/llmdoc")) { - errors.push(`hooks.json: 命令必须以 'npx -y @tokenroll/llmdoc' 开头: ${command}`); + const runtime = "npx -y --package=@tokenroll/llmdoc-hook-runtime@npm:@tokenroll/llmdoc -- llmdoc hook"; + const expectedCommands = new Set([ + `${runtime} session-start`, + `${runtime} stop`, + `${runtime} compact` + ]); + const commands = (JSON.stringify(hooks).match(/"command":"([^"]+)"/g) ?? []).map((raw) => raw.slice(11, -1)); + for (const command of commands) { + if (!expectedCommands.delete(command)) { + errors.push(`hooks.json: 非法或重复 hook runtime 命令: ${command}`); } } + for (const missing of expectedCommands) { + errors.push(`hooks.json: 缺少防本地遮蔽的 hook runtime 命令: ${missing}`); + } } if (errors.length > 0) { diff --git a/scripts/check-prompt-budget.mjs b/scripts/check-prompt-budget.mjs index 639decb..563265e 100644 --- a/scripts/check-prompt-budget.mjs +++ b/scripts/check-prompt-budget.mjs @@ -59,6 +59,51 @@ for (const hookCase of hookCases) { } } +// The direct dist checks above prove current hook semantics. Execute the actual +// plugin commands too: npm package resolution is a separate launcher boundary, +// and a same-name local/file dependency can otherwise shadow the scoped CLI. +const hookConfig = JSON.parse(fs.readFileSync(path.join(root, "hooks/hooks.json"), "utf8")); +const launcherCases = [ + { event: "SessionStart", stdin: JSON.stringify({ source: "startup" }), json: false }, + { event: "Stop", stdin: "{}", json: true }, + { event: "PreCompact", stdin: "{}", json: true } +]; + +for (const launcherCase of launcherCases) { + const configured = hookConfig.hooks?.[launcherCase.event]?.flatMap((group) => group.hooks ?? []) ?? []; + if (configured.length !== 1 || typeof configured[0]?.command !== "string") { + failures.push(`hook launcher ${launcherCase.event}: expected exactly one command`); + continue; + } + const result = spawnSync(configured[0].command, { + cwd: root, + encoding: "utf8", + input: launcherCase.stdin, + shell: true, + timeout: 120_000 + }); + if (result.error) { + failures.push(`hook launcher ${launcherCase.event}: ${result.error.message}`); + continue; + } + if (result.status !== 0) { + failures.push( + `hook launcher ${launcherCase.event}: exited ${result.status}: ${(result.stderr ?? "").trim()}` + ); + continue; + } + const output = result.stdout.trim(); + if (launcherCase.json) { + try { + JSON.parse(output); + } catch { + failures.push(`hook launcher ${launcherCase.event}: output is not valid JSON`); + continue; + } + } + process.stdout.write(`hook launcher ${launcherCase.event}: ok\n`); +} + if (failures.length > 0) { process.stderr.write(`${failures.join("\n")}\n`); process.exit(1); diff --git a/tests/parity-checklist.md b/tests/parity-checklist.md index 91115ee..2f3d602 100644 --- a/tests/parity-checklist.md +++ b/tests/parity-checklist.md @@ -8,7 +8,7 @@ Claude Code 根插件是唯一手工维护的准源。Codex 表面由 ACPlugin - [ ] 两个平台暴露 `investigator`、受限 `reflector` 与 `recorder` 三个角色契约;Reflector 只能写 `.llmdoc-tmp/reflections/pending/`。 - [ ] `init/update/prune/upgrade` 的宿主专属 front matter / UI policy 保持正确;五个 skill 和三个 agent 的正文一致性已由脚本机械校验。 - [ ] `upgrade` 在两个平台都保持仅显式调用(Claude 侧 `disable-model-invocation: true`;Codex 侧 `policy.allow_implicit_invocation: false`),未被 operating skill 或 hook 隐式触发。 -- [ ] Claude 的 `SessionStart`、`Stop`、`PreCompact` 都通过 `npx -y @tokenroll/llmdoc` 调用 scoped CLI;Codex 保留仓库根 `hooks/hooks.json`,并按官方信任模型启用。 +- [ ] Claude 的 `SessionStart`、`Stop`、`PreCompact` 都通过 npm alias `@tokenroll/llmdoc-hook-runtime@npm:@tokenroll/llmdoc` 调用 scoped CLI,避免消费仓库的同名本地依赖遮蔽 runtime;Codex 保留仓库根 `hooks/hooks.json`,并按官方信任模型启用。 - [ ] hooks fail-open、永不写 `llmdoc/`;SessionStart 不超过 200 token,Stop/PreCompact 成功时输出合法 JSON;pending 反思候选能在无代码 delta 时触发 update 提醒。 - [ ] 生成目录中没有 V2 `worker`、tracked reflection/memory 树、startup pack、watermark 或旧命令残留;恢复后的 Reflector 不保存 transcript。 - [ ] `.agents/skills/upgrade/agents/openai.yaml` 设置 `policy.allow_implicit_invocation: false`,确保 upgrade 只能显式调用。