Skip to content

Commit e39351e

Browse files
committed
fix: eliminate Grok workspace plugin collision
1 parent 25d44a2 commit e39351e

14 files changed

Lines changed: 336 additions & 60 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ AGENTS.md ← 由 instructions.md 确定性生成的共享精简
206206

207207
`init/update` 还会在当前 active runtime root 写入 `managed/deployment-manifest.json`。命令先预览 `add/update/unchanged/stale/unowned``stale` 只报警,不会自动删除;`unowned` 表示目标受管子树中从未进入 manifest 的文件(例如用户自有 `.codex/config.toml`),不会被接管。workspace-namespace 下 manifest 写入 `.devcodex/<project-or-workspace>/managed/`,不会另建平行运行态根。
208208

209-
workspace-namespace 的共享真相与宿主 adapter 都由工作区拥有。Grok 使用工作区 `.grok/plugins/devcodex-workspace` 薄插件:工作区根由 Grok 原生发现,CLI 再以 Grok 官方本地插件命令登记该 workspace source owner,确保子 Git 项目 cwd 同样可发现 resolver Skill、Hook 与 MCP;用户级 `~/.grok/config.toml` 只精确维护受管 `plugins.enabled` 项,旧的受管 `plugins.paths` 项会被移除以避免同名插件碰撞。从子项目执行 `update --host grok` 时实际 owner/manifest 仍是工作区根,子项目不会生成 `AGENTS.md``.grok/``.codex/``.claude/``.gemini/`。由于 Grok passive Hook stdout 不进入提示上下文,子 Git 项目的完整 kernel 保证通过 `devcodex grok` 调用官方 `--rules` 提供;plain `grok` 只承诺插件发现与 best-effort resolver Skill。项目本地 adapter 仅用于显式 `--project-portable` 模式。
209+
workspace-namespace 的共享真相与宿主 adapter 都由工作区拥有。Grok 使用工作区非自动发现 source `.grok/devcodex/plugins/devcodex-workspace`:CLI 只通过 Grok 官方本地插件命令登记这个 source,root 与子 Git 项目因此都只看到一个 user-installed plugin identity;root kernel 仍由工作区 `AGENTS.md` 原生加载。旧 `.grok/plugins/devcodex-workspace` 会在新安装验证成功后可逆移动到 active-root backup,用户级 `~/.grok/config.toml` 只精确维护受管 `plugins.enabled`,并移除新旧 owner 的受管 `plugins.paths` 。从子项目执行 `update --host grok` owner/manifest 仍是工作区根,子项目不会生成 `AGENTS.md`、`.grok/`、`.codex/`、`.claude/` 或 `.gemini/`。由于 Grok passive Hook stdout 不进入提示上下文,子 Git 项目的完整 kernel 保证通过 `devcodex grok` 调用官方 `--rules` 提供;plain child 只承诺插件发现与 best-effort resolver Skill。项目本地 adapter 仅用于显式 `--project-portable` 模式。
210210

211211
> ⚠️ 请确保 IDE 的 "Use Instruction Files" 设置已开启(默认开启)。
212212
>
@@ -512,14 +512,14 @@ README / 用户使用文档当前补充四类专项 Skill:`user-manual-authori
512512
| **GitHub Copilot (JetBrains)** | `.github/instructions/*.md` + `copilot-instructions.md`(instruction-fallback) | ⚠️ 官方自定义指令路径,无本地 Hook 硬拦承诺 | ⚠️ 仅文本 | ❌ 未内置 MCP | 🟡 Beta |
513513
| **Claude Code (CLI/桌面端)** | `CLAUDE.md` + `.claude/{instructions,skills,prompts,hooks/_runtime,mcp}/` + `settings.json` hooks + `.mcp.json` | ✅ Hook 事件支持硬拦;默认 `safety-only` 下流程项提醒放行 | ✅ Hook + 文本确认 | ✅ MCP | 🟢 Full |
514514
| **Gemini CLI** | `GEMINI.md` 薄入口 + `AGENTS.md` kernel + `.agents/skills/` + `.gemini/settings.json` | ⚠️ Before/After adapter 已实现;本机无 CLI direct replay | ⚠️ Hook + portable fallback | ⚠️ 按宿主配置 | 🟡 Beta / UNVERIFIED |
515-
| **Grok Build** | 独立显式 portable:项目 `AGENTS.md + .agents + .grok/hooks`;workspace-namespace:工作区 `.grok/plugins/devcodex-workspace` + 官方用户级本地插件登记,子项目零 generated host artifacts`devcodex grok` 提供 full kernel launcher | ⚠️ plugin validate、两 cwd inspect、passive/PreToolUse 分级已验证;plain child 为 partial,launcher 为 direct | ⚠️ plain child partial;launcher 完整文本门控 |plugin MCP doctor direct | 🟡 Plain Partial / Launcher Full |
515+
| **Grok Build** | 独立显式 portable:项目 `AGENTS.md + .agents + .grok/hooks`;workspace-namespace:工作区非自动发现 source `.grok/devcodex/plugins/devcodex-workspace` + 单一官方用户级本地插件登记,子项目零 generated host artifacts | ✅ root native kernel + single user plugin;child plain plugin partial;`devcodex grok` launcher full | ⚠️ 仅 PreToolUse 可阻断;passive stdout ignored |root/child 双 MCP doctor direct | 🟢 Root Native / 🟡 Child Plain Partial / 🟢 Launcher Full |
516516
| **Cursor IDE** | 需手工配置 `.cursor/rules` 或 root `AGENTS.md`(instruction-fallback;DevCodex ****自动分发 Cursor 规则;HOST best-effort only) | ⚠️ 无 DevCodex 本地 Hook 硬拦承诺 | ⚠️ 仅文本 || 🟡 Best-effort |
517517
| **OpenAI Codex app/CLI** | `AGENTS.md` + `.agents/skills/` + `.codex/hooks.json`(含 `PreCompact` compaction guardrail) | ⚠️ Codex hook guardrail;阻断输出按事件契约分为顶层 `decision``continue:false` 与工具级 `permissionDecision` | ⚠️ Hook + 文本确认 | ⚠️ 可手工配置 MCP;DevCodex 未自动写入 | 🟡 Beta |
518518
| **ChatGPT 普通对话** | 不读取本地工作区 `AGENTS.md` / `.agents/` / `.codex/`;可手工粘贴规则 || ⚠️ 文本 || 🔴 Unsupported |
519519

520520
> **安装命令**:已发布 v1.15.1 默认三宿主部署 → `npx @vextjs/devcodex init`;仅 Claude Code adapter → `npx @vextjs/devcodex init --claude`;仅 Codex adapter → `npx @vextjs/devcodex init --codex`。当前未发布源码可用 `node index.js init --host <gemini|grok|all>` 显式增加 Gemini / Grok,默认面仍保持三宿主兼容行为。
521521
>
522-
> **Grok workspace 插件**:在 `workspace-namespace` 的工作区或任一子项目执行 `update --host grok`,CLI 都把 kernel、Skills、薄插件和 managed manifest 写到同一工作区 owner,再通过 Grok 官方本地插件命令同步用户安装登记;用户 Grok 配置只增量维护 DevCodex plugin enabled 项,并移除同一 owner 的旧 path 注册,官方命令执行后恢复配置原始字节,因此其他设置、注释和插件保持不变,重复执行幂等。`uninstall --host grok` 复用相同 scope identity,只解除官方用户安装与受管配置,保留 workspace source,重复卸载幂等。插件用安装登记中的 source owner 校验 cwd,工作区外 no-op。工作区根可直接运行 `grok`;子 Git 项目要获得完整 kernel 保证时运行 `devcodex grok [原 Grok 参数]`。launcher 先消费官方 `--cwd` 决定真实 owner,校验 root kernel,且只在子 Git 边界追加官方 `--rules`;用户额外 rules 会合并,system prompt override 与重复 cwd 会因破坏保证而拒绝。plain child 与 launcher 证据严格分开,不能恢复 ignored 项目 bridge 冒充成功
522+
> **Grok workspace 插件**:在 `workspace-namespace` 的工作区或任一子项目执行 `update --host grok`,CLI 都把 kernel、Skills、薄插件和 managed manifest 写到同一工作区 owner。薄插件 source 位于 `.grok/devcodex/plugins/devcodex-workspace`,不会被 project auto-discovery 再发现,只由 Grok 官方本地插件登记形成一个 user identity。旧 `.grok/plugins/devcodex-workspace` 登记会先通过官方 CLI 迁移,新安装 digest 验证后旧 source 才可逆移动到 `.tmp/backups`;失败走旧 source/registration/config 回滚。用户 Grok 配置只维护 DevCodex enabled 项和新旧受管 path 清理,其他设置、注释和插件保持不变,重复执行幂等。`uninstall --host grok` 只解除当前官方登记与受管配置,保留 canonical workspace source工作区根可直接运行 `grok`;子 Git 项目要获得完整 kernel 保证时运行 `devcodex grok [原 Grok 参数]`。launcher 先消费官方 `--cwd` 决定真实 owner,校验 root kernel,且只在子 Git 边界追加官方 `--rules`;用户额外 rules 会合并,system prompt override 与重复 cwd 会因破坏保证而拒绝。root native、plain child 与 launcher 证据严格分开。
523523
>
524524
> **能力差异**:🟢 Full = 已验证 Hook 事件 + MCP + 自动同步;🟡 Beta/Best-effort = 尚未达到 Full,具体能力以矩阵各列为准;🔴 Unsupported = 不在当前本地 adapter 发布范围。默认 `safety-only` 下,bootstrap / CP / auto 白名单等流程问题为提醒并继续,仅危险命令硬拦;设置 `DEVCODEX_HOOK_ENFORCEMENT=strict` 后,支持硬拦的事件才会停止流程。
525525
>

changelogs/unreleased.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ WorkspaceDataAbsorptionScopeGate · DocsSiteVisualAcceptanceGate · OmissionOnly
1515
- bridge 在读取 Profile / memory / full fallback 前先形成语义意图种子,再绑定父级 kernel、项目 active-root 与意图相关 Skills;缺失或歧义时 fail-closed。
1616
- 独立项目仍保留本地 kernel + Skills + 完整 fallback;部署 manifest、碰撞检测、host inspection、source-root 精确例外与正负向 fixture 已同步。
1717
- 修正 Grok lifecycle 事件误带 tool matcher 导致 `UserPromptSubmit` / `Stop` 被拒载的问题;workspace 模式改由工作区薄插件承载 Hook 与 MCP bridge,通过官方本地插件安装登记确保 Git 项目 cwd 可达,只维护 `plugins.enabled` 并迁出旧的同 owner path 注册,避免插件碰撞;安装过程恢复用户配置原始字节。
18-
- 新增 `HostAdapterScopeV1`:workspace-namespace 的子项目默认零 generated host artifacts;Grok adapter owner 固定在工作区 `.grok/plugins/devcodex-workspace`,工作区外 no-op,用户配置保真/幂等,project legacy manifest claims 自动退休。旧项目 bridge 方案已 superseded
18+
- 修复 Grok workspace plugin 双身份碰撞:`HostAdapterScopeV1` 将 canonical source 迁移到非自动发现的 `.grok/devcodex/plugins/devcodex-workspace`只保留官方 user installation identity;旧 source/registration 采用 install-before-move 的可逆备份迁移,配置保真、工作区外 no-op、子项目零 generated host artifacts 与 legacy manifest 退休保持不变
1919
- 新增 `devcodex grok` full-evidence launcher:只在 workspace 子 Git 项目用官方 `--rules` 绑定共享 kernel,并明确把 plain child 的 Skill-discovery/partial 与 launcher direct evidence 分开;修正把 passive Hook stdout 误当上下文注入的错误口径。
2020
- 拆分包开发与 Claude 安装态 MCP 契约:源码根 `.mcp.json` 改为指向实际存在的包内 `mcp/*`,CLI 仍为业务项目生成 `.claude/mcp/*`,避免 Grok 在源码仓发现通用 manifest 时启动不存在路径。
2121
- 补齐 `uninstall --host grok` 生命周期:复用 `HostAdapterScopeV1`,官方卸载用户插件并只移除 DevCodex 受管配置,保留 workspace source、未知键/注释与重复卸载幂等性。

grok/plugins/devcodex-workspace/hooks/devcodex-workspace.cjs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ function findWorkspaceRoot(start) {
3131

3232
function workspaceFromPluginRoot(pluginRoot) {
3333
const absolute = path.resolve(pluginRoot)
34-
if (
35-
path.basename(path.dirname(absolute)).toLowerCase() === 'plugins' &&
36-
path.basename(path.dirname(path.dirname(absolute))).toLowerCase() === '.grok'
37-
) {
38-
return path.dirname(path.dirname(path.dirname(absolute)))
34+
const sourceWorkspace = findWorkspaceRoot(absolute)
35+
if (sourceWorkspace) {
36+
const allowedSources = [
37+
path.join(sourceWorkspace, '.grok', 'devcodex', 'plugins', 'devcodex-workspace'),
38+
path.join(sourceWorkspace, '.grok', 'plugins', 'devcodex-workspace')
39+
]
40+
if (allowedSources.some(candidate => samePath(candidate, absolute))) return sourceWorkspace
3941
}
4042
const installedRoot = path.dirname(absolute)
4143
if (path.basename(installedRoot).toLowerCase() !== 'installed-plugins') return null

grok/plugins/devcodex-workspace/mcp/workspace-bridge.cjs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,29 @@ function samePath(left, right) {
1717
const b = path.resolve(right)
1818
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
1919
}
20+
function findWorkspaceRoot(start) {
21+
let current = path.resolve(start)
22+
while (true) {
23+
const marker = path.join(current, '.devcodex', 'layout.json')
24+
try {
25+
const layout = JSON.parse(fs.readFileSync(marker, 'utf8'))
26+
if (String(layout.mode || '').trim() === 'workspace-namespace') return current
27+
} catch { }
28+
const parent = path.dirname(current)
29+
if (parent === current) return null
30+
current = parent
31+
}
32+
}
2033
function workspaceFromPluginRoot(root) {
2134
const absolute = path.resolve(root)
22-
if (
23-
path.basename(path.dirname(absolute)).toLowerCase() === 'plugins' &&
24-
path.basename(path.dirname(path.dirname(absolute))).toLowerCase() === '.grok'
25-
) return path.dirname(path.dirname(path.dirname(absolute)))
35+
const sourceWorkspace = findWorkspaceRoot(absolute)
36+
if (sourceWorkspace) {
37+
const allowedSources = [
38+
path.join(sourceWorkspace, '.grok', 'devcodex', 'plugins', 'devcodex-workspace'),
39+
path.join(sourceWorkspace, '.grok', 'plugins', 'devcodex-workspace')
40+
]
41+
if (allowedSources.some(candidate => samePath(candidate, absolute))) return sourceWorkspace
42+
}
2643
const installedRoot = path.dirname(absolute)
2744
if (path.basename(installedRoot).toLowerCase() !== 'installed-plugins') return null
2845
try {

index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const {
2424
grokUserConfigPath,
2525
retireWorkspaceProjectHostManifest,
2626
resolveHostAdapterScope,
27-
syncGrokPluginInstallation, uninstallGrokPluginInstallation,
27+
syncGrokPluginInstallation, syncGrokWorkspacePluginInstallation, uninstallGrokPluginInstallation,
2828
writeGrokPluginRegistration
2929
} = require('./scripts/lib/host-adapter-scope.js')
3030
const { runCli: runMigrateLayout } = require('./scripts/migrate-layout.js')
@@ -356,7 +356,7 @@ const { cmdInit, cmdInitHost, cmdInitClaude, cmdInitCodex, cmdInitGemini, cmdIni
356356
ensureRuntimeDirs, ensureDevCodexGitignore, walkDir,
357357
resolveActiveRuntimeRoot, resolveGitignoreRoot, getLegacyCounts, isPlainObject,
358358
resolveHostAdapterScope, writeGrokPluginRegistration,
359-
syncGrokPluginInstallation,
359+
syncGrokPluginInstallation, syncGrokWorkspacePluginInstallation,
360360
uninstallGrokPluginInstallation,
361361
retireWorkspaceProjectHostManifest,
362362
resolveTenantSelection, shouldIncludeInstructionFile

scripts/lib/cli-host-utils.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ function buildCliHostUtils({ fs, path, isPlainObject, claudeMcpJson }) {
173173
let registrationError = null
174174
try {
175175
const config = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : ''
176-
registrationCurrent = !mergeGrokPluginRegistration(config, hostScope.pluginRoot).changed
176+
registrationCurrent = !mergeGrokPluginRegistration(config, hostScope.pluginRoot, {
177+
legacyPluginPaths: hostScope.legacyPluginRoots
178+
}).changed
177179
} catch (error) { registrationError = error.message }
178180
grokPlugin = {
179181
root: hostScope.pluginRoot,
@@ -183,6 +185,7 @@ function buildCliHostUtils({ fs, path, isPlainObject, claudeMcpJson }) {
183185
sourcePresent: fs.existsSync(manifest) && fs.existsSync(hook),
184186
installed: fs.existsSync(manifest) && fs.existsSync(hook) && installation.current,
185187
installation,
188+
legacySourcesPresent: (hostScope.legacyPluginRoots || []).filter(item => fs.existsSync(item)),
186189
registrationCurrent,
187190
registrationError
188191
}
@@ -196,6 +199,9 @@ function buildCliHostUtils({ fs, path, isPlainObject, claudeMcpJson }) {
196199
...(registrationError ? { detail: registrationError } : {})
197200
})
198201
}
202+
if (grokPlugin.legacySourcesPresent.length) {
203+
issues.push({ code: 'HOST_GROK_LEGACY_PLUGIN_SOURCE_PRESENT', paths: grokPlugin.legacySourcesPresent })
204+
}
199205
}
200206
if (fallback.installed && sourceDigests.size === 1 && fallback.digest !== [...sourceDigests][0]) {
201207
issues.push({

scripts/lib/cli-install-commands.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function buildCliInstallCommands(ctx) {
1313
mergeClaudeHooks, mergeClaudeMcpConfig,
1414
ensureRuntimeDirs, ensureDevCodexGitignore, getLegacyCounts, isPlainObject,
1515
resolveHostAdapterScope, writeGrokPluginRegistration, syncGrokPluginInstallation,
16+
syncGrokWorkspacePluginInstallation,
1617
uninstallGrokPluginInstallation, retireWorkspaceProjectHostManifest,
1718
resolveTenantSelection, shouldIncludeInstructionFile
1819
} = ctx
@@ -794,6 +795,7 @@ function buildCliInstallCommands(ctx) {
794795
if (grokWorkspaceScope) {
795796
writeGrokPluginRegistration({
796797
pluginPath: hostScope.pluginRoot,
798+
legacyPluginPaths: hostScope.legacyPluginRoots,
797799
activeRoot,
798800
dryRun: true,
799801
env: process.env
@@ -818,30 +820,27 @@ function buildCliInstallCommands(ctx) {
818820
addCounts(counts, copyProjectedTree({
819821
cwd: targetRoot,
820822
source: 'grok/plugins/devcodex-workspace',
821-
destination: path.join('.grok', 'plugins', 'devcodex-workspace'),
823+
destination: path.join('.grok', 'devcodex', 'plugins', 'devcodex-workspace'),
822824
force, dryRun, backupDir, log, inlineLog, tenantId
823825
}))
824-
const registration = writeGrokPluginRegistration({
826+
const installation = syncGrokWorkspacePluginInstallation({
825827
pluginPath: hostScope.pluginRoot,
828+
legacyPluginPaths: hostScope.legacyPluginRoots,
826829
activeRoot,
830+
backupDir,
827831
dryRun,
828832
env: process.env
829833
})
830-
if (registration.changed) {
834+
if (installation.status === 'migrated') {
831835
counts.updated++
832-
log(c.green(' ✓ Grok user config (managed workspace plugin registration)'))
836+
log(c.green(' ✓ Grok workspace plugin migrated to the canonical source'))
833837
} else {
834838
counts.skipped++
835-
log(c.dim(' ~ Grok user config (workspace plugin already registered)'))
839+
log(c.dim(` ~ Grok workspace plugin source (${installation.status})`))
836840
}
837-
const installation = syncGrokPluginInstallation({
838-
pluginPath: hostScope.pluginRoot,
839-
dryRun,
840-
env: process.env
841-
})
842841
if (installation.status === 'unavailable') {
843-
inlineLog(c.yellow(' ⚠ Grok CLI not found; plugin path is registered but runtime activation remains unverified'))
844-
} else if (installation.status === 'verified') {
842+
inlineLog(c.yellow(' ⚠ Grok CLI not found; legacy source was retained and canonical activation remains unverified'))
843+
} else if (['verified', 'migrated'].includes(installation.status)) {
845844
log(c.green(' ✓ Grok user plugin installation synchronized'))
846845
} else {
847846
log(c.dim(` ~ Grok user plugin installation (${installation.status})`))

0 commit comments

Comments
 (0)