Skip to content

Commit cf8e707

Browse files
committed
feat: gate progressive routing legacy retirement
1 parent 4d7a56c commit cf8e707

19 files changed

Lines changed: 1536 additions & 87 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict'
2+
3+
const crypto = require('crypto')
4+
const fs = require('fs')
5+
const path = require('path')
6+
7+
const HOST_VARIANTS = Object.freeze({
8+
claude: 'claude-code/print-user-global-local-stdio',
9+
codex: 'codex-cli/exec-user-global-local-stdio',
10+
copilot: 'copilot-cli/print-user-global-local-stdio',
11+
gemini: 'gemini-cli/headless-user-global-local-stdio',
12+
grok: 'grok-cli-single/global-launcher-local-stdio'
13+
})
14+
15+
const HOST_ALIASES = Object.freeze({
16+
'claude-code': 'claude',
17+
'github-copilot': 'copilot',
18+
'gemini-cli': 'gemini',
19+
'grok-cli-single': 'grok',
20+
'grok-single': 'grok'
21+
})
22+
23+
function sha256 (value) {
24+
return crypto.createHash('sha256').update(value).digest('hex')
25+
}
26+
27+
function normalizeHostId (host) {
28+
const value = String(host || '').trim().toLowerCase()
29+
if (Object.prototype.hasOwnProperty.call(HOST_VARIANTS, value)) return value
30+
return HOST_ALIASES[value] || ''
31+
}
32+
33+
function normalizeHostVariant (host) {
34+
const value = String(host || '').trim().toLowerCase()
35+
if (Object.values(HOST_VARIANTS).includes(value)) return value
36+
const hostId = normalizeHostId(value)
37+
return HOST_VARIANTS[hostId] || `${value || 'unknown'}/unsupported`
38+
}
39+
40+
function getLifecycleHostAdapterDigest (host, options = {}) {
41+
const fsImpl = options.fs || fs
42+
const hostVariant = normalizeHostVariant(host)
43+
const runtimeRoot = path.resolve(options.runtimeRoot || __dirname)
44+
const files = [
45+
'host-adapter-identity.cjs',
46+
'lifecycle-host-adapters.cjs',
47+
'lifecycle.cjs'
48+
].map(name => ({
49+
name,
50+
digest: sha256(fsImpl.readFileSync(path.join(runtimeRoot, name)))
51+
}))
52+
return sha256(JSON.stringify({
53+
schemaVersion: 'HostAdapterIdentityV1',
54+
hostVariant,
55+
files
56+
}))
57+
}
58+
59+
module.exports = {
60+
HOST_VARIANTS,
61+
getLifecycleHostAdapterDigest,
62+
normalizeHostId,
63+
normalizeHostVariant
64+
}

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,28 @@
22
"schemaVersion": "HostSkillRouteCapabilityV1",
33
"capabilities": [
44
{
5-
"hostVariant": "claude-code/user-global-local-stdio",
5+
"hostVariant": "claude-code/print-user-global-local-stdio",
66
"status": "UNVERIFIED",
77
"testedVersion": "2.1.197",
88
"protocol": "MCP 2024-11-05",
99
"evidenceRef": null
1010
},
1111
{
12-
"hostVariant": "codex/user-global-local-stdio",
12+
"hostVariant": "codex-cli/exec-user-global-local-stdio",
1313
"status": "UNVERIFIED",
1414
"testedVersion": "unverified",
1515
"protocol": "MCP 2024-11-05",
1616
"evidenceRef": null
1717
},
1818
{
19-
"hostVariant": "copilot/user-global-local-stdio",
19+
"hostVariant": "copilot-cli/print-user-global-local-stdio",
2020
"status": "UNVERIFIED",
2121
"testedVersion": "unverified",
2222
"protocol": "MCP 2024-11-05",
2323
"evidenceRef": null
2424
},
2525
{
26-
"hostVariant": "gemini-cli/user-global-local-stdio",
27-
"status": "UNVERIFIED",
28-
"testedVersion": "unverified",
29-
"protocol": "MCP 2024-11-05",
30-
"evidenceRef": null
31-
},
32-
{
33-
"hostVariant": "grok/user-global-local-stdio",
26+
"hostVariant": "gemini-cli/headless-user-global-local-stdio",
3427
"status": "UNVERIFIED",
3528
"testedVersion": "unverified",
3629
"protocol": "MCP 2024-11-05",
@@ -41,10 +34,10 @@
4134
"status": "PASS",
4235
"testedVersion": "grok 0.2.112 (9bbd559437)",
4336
"protocol": "MCP 2024-11-05",
44-
"runtimeContractDigest": "3b2e25d1a1b65050943ea08de5d7348907666ab9a0691d169f953d4e196081f5",
37+
"runtimeContractDigest": "a9772e7f30b2b40f72c16f70ffbcb82844d5a59546b0bdf6a3068eda53794141",
4538
"hostAdapterDigest": "b2ab490df9c881a1a8b67c8391f72c615d05a64aab1db9e4bd6ec8a5f0a99385",
46-
"evidenceDigest": "b18ea51c81782239ab60a685ee9a995c13490e719a04004995f6920ee2e176be",
47-
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-grok-launcher-production-converged-20260728.json",
39+
"evidenceDigest": "667c4481e6cc2634c67a276b724306ae2c77170b10dbf41ca20d18832a6766f4",
40+
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-grok-20260729-final-production.json",
4841
"entrySurface": "devcodex grok -p/--single",
4942
"bootstrapDelivery": "global launcher --rules",
5043
"defaultEligible": true

‎hooks/_runtime/lifecycle-bootstrap-state.cjs‎

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ const {
77
} = require('./content-identity.cjs')
88
const { readContextPlanObservation } = require('./context-plan-observation.cjs')
99

10+
function parseContextToolIdentity (rawName, explicitServer = '') {
11+
const raw = String(rawName || '').trim()
12+
const lower = raw.toLowerCase()
13+
let server = String(explicitServer || '').trim().toLowerCase()
14+
let tool = lower
15+
const mcp = lower.match(/^mcp__(.+)__([a-z0-9_]+)$/)
16+
const doubleUnderscore = lower.match(/^([^_]+(?:-[^_]+)*)__([a-z0-9_]+)$/)
17+
const pair = lower.match(/^([^/]+)\/([a-z0-9_]+)$/)
18+
if (mcp) {
19+
server = mcp[1]
20+
tool = mcp[2]
21+
} else if (doubleUnderscore) {
22+
server = doubleUnderscore[1]
23+
tool = doubleUnderscore[2]
24+
} else if (pair) {
25+
server = pair[1]
26+
tool = pair[2]
27+
}
28+
if (server === 'devcodex_profile') server = 'devcodex-profile'
29+
if (server === 'devcodex_memory') server = 'devcodex-memory'
30+
return { raw, server, tool }
31+
}
32+
1033
function buildLifecycleBootstrapStateUtils(ctx) {
1134
const {
1235
fs,
@@ -104,23 +127,10 @@ function buildLifecycleBootstrapStateUtils(ctx) {
104127
}
105128

106129
function canonicalContextTool(payload) {
107-
const raw = getToolName(payload)
108-
const lower = raw.toLowerCase()
109-
let server = String(payload?.server_name || payload?.serverName || '').trim().toLowerCase()
110-
let tool = lower
111-
const claude = lower.match(/^mcp__([^_]+(?:-[^_]+)*)__([a-z0-9_]+)$/)
112-
const grok = lower.match(/^([^_]+(?:-[^_]+)*)__([a-z0-9_]+)$/)
113-
const pair = lower.match(/^([^/]+)\/([a-z0-9_]+)$/)
114-
if (claude) {
115-
server = claude[1]
116-
tool = claude[2]
117-
} else if (grok) {
118-
server = grok[1]
119-
tool = grok[2]
120-
} else if (pair) {
121-
server = pair[1]
122-
tool = pair[2]
123-
}
130+
const { raw, server, tool } = parseContextToolIdentity(
131+
getToolName(payload),
132+
payload?.server_name || payload?.serverName
133+
)
124134
if (!server && (PROFILE_TOOLS.has(tool) || PROFILE_ROUTE_TOOLS.has(tool) || MEMORY_READ_TOOLS.has(tool))) {
125135
return { raw, server: '', tool, canonical: '', recognizedName: true }
126136
}
@@ -1546,4 +1556,7 @@ function buildLifecycleBootstrapStateUtils(ctx) {
15461556
}
15471557
}
15481558

1549-
module.exports = { buildLifecycleBootstrapStateUtils }
1559+
module.exports = {
1560+
buildLifecycleBootstrapStateUtils,
1561+
parseContextToolIdentity
1562+
}

‎hooks/_runtime/lifecycle-host-adapters.cjs‎

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const CODEX_EVENT_MAP = Object.freeze({
2121
})
2222

2323
const COPILOT_EVENT_MAP = Object.freeze({
24+
userPromptTransformed: 'UserPromptSubmit',
2425
userPromptSubmitted: 'UserPromptSubmit',
2526
UserPromptSubmit: 'UserPromptSubmit',
2627
preToolUse: 'PreToolUse',
@@ -232,6 +233,10 @@ function normalizeHostPayload(host, payload) {
232233
if (normalized.transcriptPath && !normalized.transcript_path) normalized.transcript_path = normalized.transcriptPath
233234
if (normalized.stopReason && !normalized.stop_reason) normalized.stop_reason = normalized.stopReason
234235
if (normalized.toolResult && !normalized.tool_result) normalized.tool_result = normalized.toolResult
236+
if (originalEvent === 'userPromptTransformed' &&
237+
/^Progressive Skill route is incomplete:/i.test(String(normalized.prompt || '').trim())) {
238+
normalized.devcodex_host_continuation = true
239+
}
235240
}
236241
if (host === 'gemini' && typeof payload?.prompt_response === 'string') normalized.response = payload.prompt_response
237242
return { originalEvent, mappedEvent, payload: normalized }
@@ -331,7 +336,7 @@ function adaptGrokOutput(originalEvent, output) {
331336
return next
332337
}
333338

334-
function adaptCopilotOutput(originalEvent, output) {
339+
function adaptCopilotOutput(originalEvent, output, input = {}) {
335340
const value = output && typeof output === 'object' && !Array.isArray(output) ? { ...output } : {}
336341
const event = normalizeEventToken(originalEvent)
337342
const specific = value.hookSpecificOutput && typeof value.hookSpecificOutput === 'object'
@@ -371,6 +376,24 @@ function adaptCopilotOutput(originalEvent, output) {
371376
}
372377
}
373378

379+
if (event === 'userprompttransformed') {
380+
const additionalContext = [
381+
value.systemMessage,
382+
specific.additionalContext,
383+
value.additionalContext
384+
].filter(item => typeof item === 'string' && item.trim()).join('\n\n')
385+
const transformedPrompt = String(
386+
input.transformedPrompt || input.transformed_prompt || input.prompt || ''
387+
)
388+
return additionalContext
389+
? {
390+
modifiedTransformedPrompt: [transformedPrompt, additionalContext]
391+
.filter(Boolean)
392+
.join('\n\n')
393+
}
394+
: {}
395+
}
396+
374397
if (event === 'agentstop' || event === 'stop') {
375398
if (value.decision === 'block') {
376399
return {
@@ -385,7 +408,7 @@ function adaptCopilotOutput(originalEvent, output) {
385408
return {}
386409
}
387410

388-
function adaptHostOutput(host, originalEvent, output) {
411+
function adaptHostOutput(host, originalEvent, output, input = {}) {
389412
const value = output && typeof output === 'object' && !Array.isArray(output) ? { ...output } : { continue: true }
390413
if (value.hookSpecificOutput && typeof value.hookSpecificOutput === 'object') {
391414
value.hookSpecificOutput = { ...value.hookSpecificOutput, hookEventName: originalEvent }
@@ -394,7 +417,7 @@ function adaptHostOutput(host, originalEvent, output) {
394417
return adaptGrokOutput(originalEvent, value)
395418
}
396419
if (host === 'copilot') {
397-
return adaptCopilotOutput(originalEvent, value)
420+
return adaptCopilotOutput(originalEvent, value, input)
398421
}
399422
if (host !== 'gemini') return value
400423

@@ -469,7 +492,11 @@ function runHostAdapter(host, input, options = {}) {
469492
try { output = child.stdout.trim() ? JSON.parse(child.stdout) : { continue: true } } catch (error) {
470493
return { status: 1, error: `invalid lifecycle output: ${error.message}`, output: null }
471494
}
472-
const result = { status: 0, error: '', output: adaptHostOutput(host, normalized.originalEvent, output) }
495+
const result = {
496+
status: 0,
497+
error: '',
498+
output: adaptHostOutput(host, normalized.originalEvent, output, input)
499+
}
473500
appendAdapterTrace(host, 'output', input, normalized, result, options.env || process.env)
474501
return result
475502
}
@@ -503,6 +530,30 @@ function probeHostAdapterContract(host) {
503530
}
504531
}
505532

533+
function applyCliEnvironmentOverrides (argv, env = process.env) {
534+
const authorityIndex = argv.indexOf('--skill-route-probe-authority')
535+
if (authorityIndex >= 0 && argv[authorityIndex + 1]) {
536+
env.DEVCODEX_SKILL_ROUTE_PROBE_AUTHORITY = path.resolve(argv[authorityIndex + 1])
537+
}
538+
const routeTraceIndex = argv.indexOf('--skill-route-trace')
539+
if (routeTraceIndex >= 0 && argv[routeTraceIndex + 1]) {
540+
env.DEVCODEX_SKILL_ROUTE_TRACE = path.resolve(argv[routeTraceIndex + 1])
541+
}
542+
const lifecycleTraceIndex = argv.indexOf('--lifecycle-trace')
543+
if (lifecycleTraceIndex >= 0 && argv[lifecycleTraceIndex + 1]) {
544+
env.DEVCODEX_LIFECYCLE_TRACE = path.resolve(argv[lifecycleTraceIndex + 1])
545+
}
546+
const workspaceRootIndex = argv.indexOf('--workspace-root')
547+
if (workspaceRootIndex >= 0 && argv[workspaceRootIndex + 1]) {
548+
env.DEVCODEX_WORKSPACE_ROOT = path.resolve(argv[workspaceRootIndex + 1])
549+
}
550+
const eventIndex = argv.indexOf('--event')
551+
if (eventIndex >= 0 && argv[eventIndex + 1]) {
552+
env.DEVCODEX_HOST_EVENT = String(argv[eventIndex + 1]).trim()
553+
}
554+
return env
555+
}
556+
506557
if (require.main === module) {
507558
const host = String(process.argv[2] || '').trim().toLowerCase()
508559
if (!Object.prototype.hasOwnProperty.call(EVENT_MAP, host)) {
@@ -514,6 +565,7 @@ if (require.main === module) {
514565
process.stdout.write(JSON.stringify(probe))
515566
process.exit(probe.status === 'passed' ? 0 : 1)
516567
}
568+
applyCliEnvironmentOverrides(process.argv.slice(3))
517569
let input = ''
518570
process.stdin.setEncoding('utf8')
519571
process.stdin.on('data', chunk => { input += chunk })
@@ -524,6 +576,10 @@ if (require.main === module) {
524576
process.exit(2)
525577
return
526578
}
579+
if (!payload.hookEventName && !payload.hook_event_name &&
580+
process.env.DEVCODEX_HOST_EVENT) {
581+
payload.hookEventName = process.env.DEVCODEX_HOST_EVENT
582+
}
527583
const result = runHostAdapter(host, payload)
528584
if (result.status !== 0) {
529585
process.stderr.write(`${result.error}\n`)
@@ -536,6 +592,7 @@ if (require.main === module) {
536592

537593
module.exports = {
538594
EVENT_MAP,
595+
applyCliEnvironmentOverrides,
539596
adaptCopilotOutput,
540597
adaptHostOutput,
541598
adaptGrokOutput,

‎hooks/_runtime/lifecycle.cjs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,12 @@ async function main() {
12411241

12421242
// ── UserPromptSubmit ───────────────────────────────────────────────────────
12431243
if (eventName === 'UserPromptSubmit') {
1244+
if (payload.devcodex_host_continuation === true) {
1245+
state.lastReason = 'host-route-continuation'
1246+
saveState(state)
1247+
writeStdout(noopOutput())
1248+
return
1249+
}
12441250
const workflowCompletionLifecycle = state.workflowCompletionLifecycle
12451251
state = resetState(mode, state)
12461252
state.workflowCompletionLifecycle = workflowCompletionLifecycle
@@ -1355,6 +1361,9 @@ async function main() {
13551361
let progressiveSkillRouteMode = 'legacy'
13561362
try {
13571363
const { bootstrapSkillRouteForTurn } = require('./skill-route-tool.cjs')
1364+
const {
1365+
getLifecycleHostAdapterDigest
1366+
} = require('./host-adapter-identity.cjs')
13581367
const route = bootstrapSkillRouteForTurn({
13591368
project: state.contextAcquisition?.project,
13601369
contextEpoch: state.contextAcquisition?.contextEpoch,
@@ -1363,7 +1372,8 @@ async function main() {
13631372
cwd: CONTEXT_ROOT
13641373
}, {
13651374
inputRoot: CONTEXT_ROOT,
1366-
env: process.env
1375+
env: process.env,
1376+
hostAdapterDigest: getLifecycleHostAdapterDigest(platform)
13671377
})
13681378
progressiveSkillRouteMode = route.modeReceipt?.effective || 'legacy'
13691379
progressiveSkillRouteMsg = route.injectionText || ''

0 commit comments

Comments
 (0)