Skip to content

Commit 26cbe06

Browse files
committed
feat: close unified skill routing acceptance
1 parent 88b69e2 commit 26cbe06

10 files changed

Lines changed: 1085 additions & 53 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Codex/Claude 等宿主自己的 `AGENTS.md`、`CLAUDE.md`、原生个人/项目
150150
| 宿主 | 大致能力 |
151151
|------|----------|
152152
| Claude Code / Codex / Copilot CLI 等 | Hook 较完整时可硬拦危险命令、Stop 收口 |
153-
| **Grok** | **Partial**:UserPromptSubmit **不能**可靠注入;`devcodex grok` 的 exact global-launcher + local-stdio 变体通过新鲜 S15 时可使用渐进式 Skill 路由,其他入口回退 legacy |
153+
| **Grok** | **Partial enforcement**:UserPromptSubmit 不能可靠注入完整入口块;`devcodex grok` 仍使用统一 local-stdio Skill 路由,但当前精确 variant direct evidence 为 UNVERIFIED,不据此声称与 Codex 的宿主观察能力等价 |
154154
| 仅 Instruction 的 surface | 语义约束,不保证硬拦 |
155155

156156
「adapter 已安装」≠「五宿主能力完全一致」。

‎hooks/_runtime/host-skill-route-capabilities.v1.json‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"status": "PASS",
1414
"testedVersion": "codex-cli 0.145.0",
1515
"protocol": "MCP 2024-11-05",
16-
"runtimeContractDigest": "78b04aeb54897ea111fb20697b7031695eafd11fc089708734ab8fee132c43ac",
17-
"hostAdapterDigest": "7db6ee0f7b46dee235ec7bd38c1930f44f55b1cb7b31c6b31f280d3c786ee131",
18-
"evidenceDigest": "c8a57c368a2d7d464ed13ca47f51fe704f413cecba76445e3d10025e555eb734",
19-
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-codex-20260729-candidate-r5.json",
16+
"runtimeContractDigest": "e1f1f244e0fc7cb6bb789abc18f11fe4e0e762ff611488de341277866b2142bb",
17+
"hostAdapterDigest": "b2693a7aad1643fd7d21be90b7f73ba42b6c79f1eac405d3919bbb28a316ad20",
18+
"evidenceDigest": "203c109eb4b9208035bc67c08ca632896428de795752c150773daa4f959b2b86",
19+
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-codex-20260729-full-closure-production.json",
2020
"entrySurface": "codex exec --ephemeral",
2121
"bootstrapDelivery": "user-global hooks.json UserPromptSubmit",
2222
"defaultEligible": true
@@ -37,16 +37,13 @@
3737
},
3838
{
3939
"hostVariant": "grok-cli-single/global-launcher-local-stdio",
40-
"status": "PASS",
40+
"status": "UNVERIFIED",
4141
"testedVersion": "grok 0.2.112 (9bbd559437)",
4242
"protocol": "MCP 2024-11-05",
43-
"runtimeContractDigest": "2eb23345d426d870b2f45f0c585bc341c87c110612d357efaa79d39c14af3489",
44-
"hostAdapterDigest": "b2ab490df9c881a1a8b67c8391f72c615d05a64aab1db9e4bd6ec8a5f0a99385",
45-
"evidenceDigest": "771a886ffc0606ac08429fa4e27675422083b864df76e2d1c43819fac7333c95",
46-
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-grok-20260729-current-candidate.json",
43+
"evidenceRef": null,
4744
"entrySurface": "devcodex grok -p/--single",
4845
"bootstrapDelivery": "global launcher --rules",
49-
"defaultEligible": true
46+
"defaultEligible": false
5047
}
5148
]
5249
}

‎package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@
8282
"test:skill-route-contracts": "node scripts/test-skill-route-contracts.js",
8383
"test:skill-route-state": "node scripts/test-skill-route-state.js",
8484
"test:skill-route-lifecycle": "node scripts/test-skill-route-lifecycle.js",
85+
"test:skill-route-closure": "node scripts/test-skill-route-closure.js",
8586
"test:skill-intents": "node scripts/test-skill-intents.js",
8687
"generate:skill-intents": "node scripts/generate-skill-intents.js --write",
8788
"check:skill-intents": "node scripts/generate-skill-intents.js",
88-
"test:skill-route": "npm run test:skill-intents && npm run test:skill-route-contracts && npm run test:skill-route-state && npm run test:skill-route-lifecycle",
89+
"test:skill-route": "npm run test:skill-intents && npm run test:skill-route-contracts && npm run test:skill-route-state && npm run test:skill-route-lifecycle && npm run test:skill-route-closure",
8990
"probe:skill-route:s15:grok": "node scripts/probe-skill-route-s15-grok.js",
9091
"probe:skill-route:s15:host": "node scripts/probe-skill-route-s15-host.js",
9192
"test:global-host-config": "node scripts/test-global-host-config.js && node scripts/test-global-host-runtime-verifier.js && npm run test:skill-native-coexistence && npm run test:global-skill-runtime",
@@ -202,6 +203,8 @@
202203
"scripts/test-skill-route-contracts.js",
203204
"scripts/test-skill-route-state.js",
204205
"scripts/test-skill-route-lifecycle.js",
206+
"scripts/test-skill-route-closure.js",
207+
"scripts/fixtures/skill-route-closure-trace.v1.json",
205208
"scripts/probe-skill-route-s15-grok.js",
206209
"scripts/probe-skill-route-s15-host.js",
207210
"scripts/lib/skill-route-test-fixture.js",
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"schemaVersion": "SkillRouteClosureTraceV1",
3+
"requirementLinks": {
4+
"H1": ["S2", "S3", "S5"],
5+
"H2": ["S4", "S9", "S14"],
6+
"H3": ["S5", "S14"],
7+
"H4": ["S7"],
8+
"H5": ["S6", "S12"],
9+
"H6": ["S1", "S10"],
10+
"H7": ["S14"],
11+
"H8": ["S8"],
12+
"H9": ["S9", "S12"],
13+
"H10": ["S1", "S10", "S12"],
14+
"H11": ["S13", "S15"],
15+
"H12": ["S7", "S12"],
16+
"H13": ["S2", "S11"],
17+
"H14": ["S8"],
18+
"H15": ["S15"],
19+
"H16": ["S5", "S14", "S15"],
20+
"H17": ["S6", "S11", "S13"],
21+
"H18": ["S18"],
22+
"H19": ["S17", "S18"]
23+
},
24+
"acceptanceLinks": {
25+
"S1": ["C06"],
26+
"S2": ["R01", "M05"],
27+
"S3": ["R01", "M03"],
28+
"S4": ["C02", "C06"],
29+
"S5": ["M01", "M03"],
30+
"S6": ["R02", "P10"],
31+
"S7": ["P09"],
32+
"S8": ["C08"],
33+
"S9": ["C02"],
34+
"S10": ["C06"],
35+
"S11": ["P02"],
36+
"S12": ["C07", "P10", "M05"],
37+
"S13": ["M02", "M05"],
38+
"S14": ["M01", "M03"],
39+
"S15": ["M05"],
40+
"S16": ["C01", "C03"],
41+
"S17": ["T01a", "M10"],
42+
"S18": ["M09"]
43+
},
44+
"testCases": {
45+
"C01": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "Acceptance C01"},
46+
"C02": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "Acceptance C02"},
47+
"C03": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "gray-lifecycle"},
48+
"C04": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "indexDigest"},
49+
"C05": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "PAGE_LIMIT_BYTES"},
50+
"C06": {"owner": "scripts/test-skill-intents.js", "anchor": "activeCount, 83"},
51+
"C07": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "Object.keys(card).sort()"},
52+
"C08": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "Acceptance C08"},
53+
"C09": {"owner": "scripts/test-skill-route-state.js", "anchor": "serializedBytes <= 48 * 1024"},
54+
"R01": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance R01 / R05"},
55+
"R02": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance R02 / T01"},
56+
"R03": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance R03"},
57+
"R04": {"owner": "scripts/test-skill-route-state.js", "anchor": "CONTEXT_BINDING_PENDING"},
58+
"R05": {"owner": "scripts/test-skill-route-state.js", "anchor": "REQUEST_FIELD_UNSUPPORTED"},
59+
"R06": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "validateWorkflowRootRegistry"},
60+
"R07": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "availableConditionRules"},
61+
"R08": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance R08"},
62+
"P01": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "item.sources.length > 0"},
63+
"P02": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "Acceptance P02-P05"},
64+
"P03": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "MANDATORY_CONFLICT"},
65+
"P04": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "MISSING_ROOT"},
66+
"P05": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "DEPENDENCY_CYCLE"},
67+
"P06": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "coexistenceScenarios"},
68+
"P07": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance P07"},
69+
"P08": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "SINGLE_SKILL_BODY_BUDGET"},
70+
"P09": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance P09"},
71+
"P10": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance P10"},
72+
"P11": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance P11"},
73+
"T01": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance R02 / T01"},
74+
"T01a": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance T01a / T07 / M10"},
75+
"T02": {"owner": "scripts/test-skill-route-state.js", "anchor": "injected after-commit interruption"},
76+
"T03": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance T03 / T06"},
77+
"T04": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance T04"},
78+
"T05": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance T05"},
79+
"T06": {"owner": "scripts/test-skill-route-state.js", "anchor": "staleRecovered"},
80+
"T07": {"owner": "scripts/test-skill-route-state.js", "anchor": "ctx-snapshot-second"},
81+
"T08": {"owner": "scripts/test-skill-route-state.js", "anchor": "collectExpiredTurns"},
82+
"T09": {"owner": "scripts/test-skill-route-state.js", "anchor": "RESPONSE_CACHE_BUDGET_BLOCKED"},
83+
"T10": {"owner": "scripts/test-skill-route-state.js", "anchor": "RUNTIME_STATE_CAPACITY_BLOCKED"},
84+
"M01": {"owner": "scripts/test-skill-route-lifecycle.js", "anchor": "five-host-default"},
85+
"M02": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "DEVCODEX_SKILL_ROUTE_MODE"},
86+
"M02a": {"owner": "scripts/test-skill-route-contracts.js", "anchor": "Acceptance M02a"},
87+
"M03": {"owner": "scripts/test-skill-route-lifecycle.js", "anchor": "WorkspaceSkillIntent"},
88+
"M04": {"owner": "scripts/test-global-host-config.js", "anchor": "sourceSkillRouteMode"},
89+
"M05": {"owner": "scripts/probe-skill-route-s15-host.js", "anchor": "authorizationSource"},
90+
"M06": {"owner": "scripts/test-global-install-smoke.js", "anchor": "real global install"},
91+
"M07": {"owner": "README.md", "anchor": "global-adapters apply"},
92+
"M08": {"owner": "scripts/test-skill-route-state.js", "anchor": "MODE_CAPABILITY_STALE"},
93+
"M09": {"owner": "scripts/test-global-install-smoke.js", "anchor": "host-native .agents/skills root"},
94+
"M10": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance T01a / T07 / M10"}
95+
}
96+
}

‎scripts/test-global-host-config.js‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,12 +542,17 @@ const installedSkillRouteCapabilities = JSON.parse(fs.readFileSync(
542542
path.join(codexRuntime, 'hooks', '_runtime', 'host-skill-route-capabilities.v1.json'),
543543
'utf8'
544544
))
545-
const installedGrokCapability = installedSkillRouteCapabilities.capabilities.find(item =>
546-
item.hostVariant === 'grok-cli-single/global-launcher-local-stdio'
545+
const installedCodexCapability = installedSkillRouteCapabilities.capabilities.find(item =>
546+
item.hostVariant === 'codex-cli/exec-user-global-local-stdio'
547547
)
548-
assert.ok(installedGrokCapability)
549-
assert.strictEqual(installedGrokCapability.status, 'PASS')
550-
assert.match(installedGrokCapability.runtimeContractDigest, /^[a-f0-9]{64}$/)
548+
assert.ok(installedCodexCapability)
549+
assert.strictEqual(installedCodexCapability.status, 'PASS')
550+
assert.strictEqual(
551+
installedCodexCapability.runtimeContractDigest,
552+
installedRuntimeContractDigest
553+
)
554+
assert.match(installedCodexCapability.hostAdapterDigest, /^[a-f0-9]{64}$/)
555+
assert.match(installedCodexCapability.evidenceDigest, /^[a-f0-9]{64}$/)
551556

552557
const forgedReceiptFile = path.join(codexTarget.root, 'devcodex', 'global-host-receipt.json')
553558
const forgedReceipt = JSON.parse(fs.readFileSync(forgedReceiptFile, 'utf8'))
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
'use strict'
2+
3+
const assert = require('assert')
4+
const fs = require('fs')
5+
const path = require('path')
6+
7+
const {
8+
getRuntimeContractDigest
9+
} = require('../hooks/_runtime/skill-route-mode.cjs')
10+
const {
11+
getLifecycleHostAdapterDigest
12+
} = require('../hooks/_runtime/host-adapter-identity.cjs')
13+
14+
const ROOT = path.resolve(__dirname, '..')
15+
const REQUIREMENTS_ROOT = process.env.DEVCODEX_SKILL_ROUTE_REQUIREMENTS_ROOT
16+
? path.resolve(process.env.DEVCODEX_SKILL_ROUTE_REQUIREMENTS_ROOT)
17+
: path.resolve(
18+
ROOT,
19+
'..',
20+
'.devcodex',
21+
'devcodex',
22+
'requirements',
23+
'工作区Skill意图结构化与路由增强'
24+
)
25+
const TRACE_FILE = path.join(
26+
__dirname,
27+
'fixtures',
28+
'skill-route-closure-trace.v1.json'
29+
)
30+
31+
function read (file) {
32+
return fs.readFileSync(file, 'utf8')
33+
}
34+
35+
function range (prefix, start, end) {
36+
return Array.from({ length: end - start + 1 }, (_, index) =>
37+
`${prefix}${start + index}`
38+
)
39+
}
40+
41+
const trace = JSON.parse(read(TRACE_FILE))
42+
assert.strictEqual(trace.schemaVersion, 'SkillRouteClosureTraceV1')
43+
44+
const expectedRequirements = range('H', 1, 19)
45+
const expectedAcceptance = range('S', 1, 18)
46+
const expectedTests = [
47+
...range('C0', 1, 9),
48+
...range('R0', 1, 8),
49+
...range('P0', 1, 9),
50+
'P10',
51+
'P11',
52+
'T01',
53+
'T01a',
54+
...range('T0', 2, 9),
55+
'T10',
56+
'M01',
57+
'M02',
58+
'M02a',
59+
...range('M0', 3, 9),
60+
'M10'
61+
]
62+
63+
assert.deepStrictEqual(
64+
Object.keys(trace.requirementLinks).sort(),
65+
expectedRequirements.sort()
66+
)
67+
assert.deepStrictEqual(
68+
Object.keys(trace.acceptanceLinks).sort(),
69+
expectedAcceptance.sort()
70+
)
71+
assert.deepStrictEqual(
72+
Object.keys(trace.testCases).sort(),
73+
expectedTests.sort()
74+
)
75+
76+
for (const [requirementId, acceptanceIds] of Object.entries(trace.requirementLinks)) {
77+
assert(acceptanceIds.length > 0, `${requirementId} has no acceptance link`)
78+
for (const acceptanceId of acceptanceIds) {
79+
assert(trace.acceptanceLinks[acceptanceId], `${requirementId} -> ${acceptanceId} missing`)
80+
}
81+
}
82+
for (const [acceptanceId, testIds] of Object.entries(trace.acceptanceLinks)) {
83+
assert(testIds.length > 0, `${acceptanceId} has no executable test link`)
84+
for (const testId of testIds) {
85+
assert(trace.testCases[testId], `${acceptanceId} -> ${testId} missing`)
86+
}
87+
}
88+
for (const [testId, evidence] of Object.entries(trace.testCases)) {
89+
const owner = path.join(ROOT, evidence.owner)
90+
assert(fs.existsSync(owner), `${testId} owner missing: ${evidence.owner}`)
91+
assert(
92+
read(owner).includes(evidence.anchor),
93+
`${testId} anchor missing from ${evidence.owner}: ${evidence.anchor}`
94+
)
95+
}
96+
97+
const requirementFile = path.join(REQUIREMENTS_ROOT, '00-需求概况.md')
98+
const designFile = path.join(REQUIREMENTS_ROOT, '02-技术方案.md')
99+
const sourceDocsAvailable = fs.existsSync(requirementFile) && fs.existsSync(designFile)
100+
if (sourceDocsAvailable) {
101+
const requirement = read(requirementFile)
102+
const design = read(designFile)
103+
for (const id of expectedRequirements) {
104+
assert(new RegExp(`\\| ${id} \\|`).test(requirement), `00 missing ${id}`)
105+
}
106+
for (const id of expectedAcceptance) {
107+
assert(new RegExp(`\\| \\*\\*?${id}\\*\\*? \\||\\| ${id} \\|`).test(requirement), `00 missing ${id}`)
108+
}
109+
for (const id of expectedTests) {
110+
assert(new RegExp(`\\| ${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\|`).test(design), `02 missing ${id}`)
111+
}
112+
113+
const terminalSection = design.slice(design.indexOf('## 8. SkillRouteModeV2'))
114+
for (const forbidden of [
115+
'当前 BLOCK',
116+
'默认仍 legacy',
117+
'SkillRouteModeV1',
118+
'sample:skill-route:retirement'
119+
]) {
120+
assert.strictEqual(
121+
terminalSection.includes(forbidden),
122+
false,
123+
`terminal design retains superseded contract: ${forbidden}`
124+
)
125+
}
126+
}
127+
128+
const packageJson = JSON.parse(read(path.join(ROOT, 'package.json')))
129+
assert.match(packageJson.scripts['test:skill-route'], /test:skill-route-closure/)
130+
const packagedFiles = JSON.stringify(packageJson.files || [])
131+
assert.doesNotMatch(packagedFiles, /retired-workspace-skill-route/)
132+
133+
const activeFiles = [
134+
...fs.readdirSync(path.join(ROOT, 'hooks', '_runtime'))
135+
.map(name => path.join('hooks', '_runtime', name)),
136+
...fs.readdirSync(path.join(ROOT, 'scripts'))
137+
.filter(name => fs.statSync(path.join(ROOT, 'scripts', name)).isFile())
138+
.map(name => path.join('scripts', name))
139+
]
140+
for (const retired of [
141+
'workspace-skill-auto-match.cjs',
142+
'workspace-skill-intent.cjs',
143+
'workspace-skill-catalog.cjs',
144+
'skill-route-retirement-gate.cjs',
145+
'skill-route-retirement-policy.v1.json'
146+
]) {
147+
assert.strictEqual(
148+
activeFiles.some(file => path.basename(file) === retired),
149+
false,
150+
`retired runtime remains active: ${retired}`
151+
)
152+
}
153+
154+
const capabilities = JSON.parse(read(path.join(
155+
ROOT,
156+
'hooks',
157+
'_runtime',
158+
'host-skill-route-capabilities.v1.json'
159+
)))
160+
const pass = capabilities.capabilities.filter(item => item.status === 'PASS')
161+
assert.strictEqual(pass.length, 1)
162+
assert.strictEqual(
163+
pass[0].hostVariant,
164+
'codex-cli/exec-user-global-local-stdio'
165+
)
166+
assert.strictEqual(
167+
pass[0].runtimeContractDigest,
168+
getRuntimeContractDigest({
169+
globalRuntime: {
170+
status: 'resolved',
171+
root: path.join(ROOT, 'skills')
172+
}
173+
})
174+
)
175+
assert.strictEqual(
176+
pass[0].hostAdapterDigest,
177+
getLifecycleHostAdapterDigest('codex')
178+
)
179+
assert.match(pass[0].evidenceDigest, /^[a-f0-9]{64}$/)
180+
assert.strictEqual(pass[0].defaultEligible, true)
181+
182+
console.log(
183+
`test-skill-route-closure: ok requirements=${expectedRequirements.length} ` +
184+
`acceptance=${expectedAcceptance.length} executable=${expectedTests.length} ` +
185+
`sourceDocs=${sourceDocsAvailable ? 'checked' : 'not-packaged'}`
186+
)

0 commit comments

Comments
 (0)