Skip to content

Commit 6d21a8c

Browse files
committed
fix: harden five-host skill route sampling
1 parent ae94e6c commit 6d21a8c

17 files changed

Lines changed: 820 additions & 114 deletions

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@
3434
"status": "PASS",
3535
"testedVersion": "grok 0.2.112 (9bbd559437)",
3636
"protocol": "MCP 2024-11-05",
37-
"runtimeContractDigest": "a9772e7f30b2b40f72c16f70ffbcb82844d5a59546b0bdf6a3068eda53794141",
37+
"runtimeContractDigest": "2eb23345d426d870b2f45f0c585bc341c87c110612d357efaa79d39c14af3489",
3838
"hostAdapterDigest": "b2ab490df9c881a1a8b67c8391f72c615d05a64aab1db9e4bd6ec8a5f0a99385",
39-
"evidenceDigest": "667c4481e6cc2634c67a276b724306ae2c77170b10dbf41ca20d18832a6766f4",
40-
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-grok-20260729-final-production.json",
39+
"evidenceDigest": "771a886ffc0606ac08429fa4e27675422083b864df76e2d1c43819fac7333c95",
40+
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-grok-20260729-current-candidate.json",
4141
"entrySurface": "devcodex grok -p/--single",
4242
"bootstrapDelivery": "global launcher --rules",
4343
"defaultEligible": true

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function parseContextToolIdentity (rawName, explicitServer = '') {
1515
const mcp = lower.match(/^mcp__(.+)__([a-z0-9_]+)$/)
1616
const doubleUnderscore = lower.match(/^([^_]+(?:-[^_]+)*)__([a-z0-9_]+)$/)
1717
const pair = lower.match(/^([^/]+)\/([a-z0-9_]+)$/)
18+
const copilotFlat = lower.match(/^(devcodex-(?:profile|memory))-([a-z0-9_]+)$/)
1819
if (mcp) {
1920
server = mcp[1]
2021
tool = mcp[2]
@@ -24,6 +25,9 @@ function parseContextToolIdentity (rawName, explicitServer = '') {
2425
} else if (pair) {
2526
server = pair[1]
2627
tool = pair[2]
28+
} else if (copilotFlat) {
29+
server = copilotFlat[1]
30+
tool = copilotFlat[2]
2731
}
2832
if (server === 'devcodex_profile') server = 'devcodex-profile'
2933
if (server === 'devcodex_memory') server = 'devcodex-memory'
@@ -350,9 +354,13 @@ function buildLifecycleBootstrapStateUtils(ctx) {
350354
/^ctx-[A-Za-z0-9-]{8,251}$/.test(String(env.DEVCODEX_CONTEXT_EPOCH || ''))
351355
? String(env.DEVCODEX_CONTEXT_EPOCH)
352356
: ''
357+
const adapterEpoch = env?.DEVCODEX_CONTEXT_EPOCH_SOURCE === 'host-adapter-cli' &&
358+
/^ctx-[A-Za-z0-9-]{8,251}$/.test(String(env.DEVCODEX_CONTEXT_EPOCH || ''))
359+
? String(env.DEVCODEX_CONTEXT_EPOCH)
360+
: ''
353361
state.contextAcquisition = {
354362
schemaVersion: CONTEXT_READ_CONTRACT.schemas.state,
355-
contextEpoch: launcherEpoch || `ctx-${crypto.randomUUID()}`,
363+
contextEpoch: launcherEpoch || adapterEpoch || `ctx-${crypto.randomUUID()}`,
356364
activeRoot: normalizePath(getActiveNamespaceRoot(state)),
357365
project,
358366
targetResolved,

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

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
const fs = require('fs')
55
const path = require('path')
6+
const crypto = require('crypto')
67
const { spawnSync } = require('child_process')
78

89
const CLAUDE_EVENT_MAP = Object.freeze({
@@ -89,13 +90,81 @@ function adapterTracePath(env = process.env) {
8990
return target
9091
}
9192

93+
function lifecycleStateTraceSummary(env = process.env) {
94+
const workspaceRoot = String(env.DEVCODEX_WORKSPACE_ROOT || '').trim()
95+
if (!workspaceRoot) return null
96+
const namespaceRoot = path.join(path.resolve(workspaceRoot), '.devcodex')
97+
if (!fs.existsSync(namespaceRoot)) return null
98+
const candidates = []
99+
for (const projectEntry of fs.readdirSync(namespaceRoot, { withFileTypes: true }).slice(0, 32)) {
100+
if (!projectEntry.isDirectory()) continue
101+
const hooksRoot = path.join(namespaceRoot, projectEntry.name, '.memory', 'hooks')
102+
if (!fs.existsSync(hooksRoot)) continue
103+
for (const hookEntry of fs.readdirSync(hooksRoot, { withFileTypes: true }).slice(0, 32)) {
104+
if (!hookEntry.isDirectory()) continue
105+
const file = path.join(hooksRoot, hookEntry.name, 'lifecycle-state.json')
106+
if (fs.existsSync(file)) {
107+
candidates.push({
108+
file,
109+
workspaceMeta: projectEntry.name.toLowerCase() === 'workspace'
110+
})
111+
}
112+
}
113+
}
114+
if (!candidates.length) return null
115+
const expectedEpoch = String(env.DEVCODEX_CONTEXT_EPOCH || '').trim()
116+
const parsedCandidates = candidates.map(candidate => ({
117+
...candidate,
118+
state: JSON.parse(fs.readFileSync(candidate.file, 'utf8')),
119+
mtimeMs: fs.statSync(candidate.file).mtimeMs
120+
}))
121+
const selected = parsedCandidates.sort((left, right) => {
122+
const leftEpochMatch = left.state.contextAcquisition?.contextEpoch === expectedEpoch ? 1 : 0
123+
const rightEpochMatch = right.state.contextAcquisition?.contextEpoch === expectedEpoch ? 1 : 0
124+
if (leftEpochMatch !== rightEpochMatch) return rightEpochMatch - leftEpochMatch
125+
if (left.workspaceMeta !== right.workspaceMeta) return left.workspaceMeta ? 1 : -1
126+
return right.mtimeMs - left.mtimeMs
127+
})[0]
128+
const file = selected.file
129+
const state = selected.state
130+
const acquisition = state.contextAcquisition || {}
131+
const receipt = acquisition.receipt || {}
132+
return {
133+
file: path.relative(path.resolve(workspaceRoot), file).replace(/\\/g, '/'),
134+
activeProject: state.activeProject || null,
135+
project: acquisition.project || state.activeProject || null,
136+
contextEpoch: acquisition.contextEpoch || null,
137+
planId: acquisition.plan?.planId || null,
138+
receiptStatus: receipt.status || null,
139+
satisfiedSourceIds: receipt.satisfiedSourceIds || [],
140+
missingSourceIds: receipt.missingSourceIds || [],
141+
inFlight: (acquisition.inFlight || []).map(item => ({
142+
canonical: item.canonical,
143+
toolCallId: item.toolCallId
144+
})),
145+
postHistory: (acquisition.postHistory || []).slice(-8).map(item => ({
146+
canonical: item.canonical,
147+
toolCallId: item.toolCallId,
148+
outcome: item.outcome
149+
})),
150+
lastError: acquisition.lastError || receipt.lastError || null
151+
}
152+
}
153+
92154
function appendAdapterTrace(host, phase, input, normalized, result, env = process.env) {
93155
const target = adapterTracePath(env)
94156
if (!target) return
95157
try {
96158
const payload = input && typeof input === 'object' && !Array.isArray(input) ? input : {}
159+
const normalizedPayload = normalized?.payload &&
160+
typeof normalized.payload === 'object' &&
161+
!Array.isArray(normalized.payload)
162+
? normalized.payload
163+
: {}
97164
const toolInput = payload.toolInput || payload.tool_input || payload.toolArgs || {}
98165
const toolResult = payload.toolResult || payload.tool_result || payload.toolResponse || payload.tool_response || {}
166+
const normalizedToolInput = normalizedPayload.tool_input || normalizedPayload.toolInput || {}
167+
const normalizedToolResult = normalizedPayload.tool_result || normalizedPayload.toolResult || {}
99168
const entry = {
100169
schemaVersion: 'LifecycleHostAdapterTraceV1',
101170
observedAt: new Date().toISOString(),
@@ -104,11 +173,23 @@ function appendAdapterTrace(host, phase, input, normalized, result, env = proces
104173
originalEvent: normalized?.originalEvent || null,
105174
mappedEvent: normalized?.mappedEvent || null,
106175
payloadKeys: Object.keys(payload).sort().slice(0, 64),
176+
adapterCwd: process.cwd(),
177+
payloadCwd: String(payload.cwd || payload.workingDirectory || ''),
178+
promptPreview: String(
179+
payload.prompt || payload.userPrompt || payload.user_prompt || ''
180+
).slice(0, 512),
107181
toolName: String(payload.toolName || payload.tool_name || ''),
108182
toolUseId: String(payload.toolUseId || payload.tool_use_id || ''),
183+
normalizedToolCallId: String(
184+
normalizedPayload.tool_call_id || normalizedPayload.toolCallId || ''
185+
),
109186
toolInputKeys: toolInput && typeof toolInput === 'object'
110187
? Object.keys(toolInput).sort().slice(0, 64)
111188
: [],
189+
normalizedToolInputDigest: crypto.createHash('sha256')
190+
.update(JSON.stringify(stableJsonValue(normalizedToolInput)))
191+
.digest('hex'),
192+
normalizedToolInputPreview: JSON.stringify(stableJsonValue(normalizedToolInput)).slice(0, 1024),
112193
dispatchedServer: String(
113194
toolInput?.server || toolInput?.serverName || toolInput?.server_name || ''
114195
),
@@ -118,10 +199,17 @@ function appendAdapterTrace(host, phase, input, normalized, result, env = proces
118199
toolResultKeys: toolResult && typeof toolResult === 'object'
119200
? Object.keys(toolResult).sort().slice(0, 64)
120201
: [],
202+
normalizedToolResultPreview: (() => {
203+
const value = typeof normalizedToolResult === 'string'
204+
? normalizedToolResult
205+
: JSON.stringify(normalizedToolResult)
206+
return String(value || '').slice(0, 512)
207+
})(),
121208
status: result?.status ?? null,
122209
outputKeys: result?.output && typeof result.output === 'object'
123210
? Object.keys(result.output).sort().slice(0, 32)
124-
: []
211+
: [],
212+
lifecycleState: phase === 'output' ? lifecycleStateTraceSummary(env) : null
125213
}
126214
fs.mkdirSync(path.dirname(target), { recursive: true })
127215
const prior = fs.existsSync(target)
@@ -171,6 +259,23 @@ function parseToolInputEnvelope(value) {
171259
}
172260
}
173261

262+
function stableJsonValue(value) {
263+
if (Array.isArray(value)) return value.map(stableJsonValue)
264+
if (!value || typeof value !== 'object') return value
265+
return Object.fromEntries(
266+
Object.keys(value).sort().map(key => [key, stableJsonValue(value[key])])
267+
)
268+
}
269+
270+
function copilotToolCallId(payload) {
271+
const material = JSON.stringify(stableJsonValue({
272+
sessionId: payload.session_id || payload.sessionId || '',
273+
toolName: payload.tool_name || payload.toolName || '',
274+
toolInput: payload.tool_input || payload.toolInput || {}
275+
}))
276+
return `copilot-${crypto.createHash('sha256').update(material).digest('hex').slice(0, 32)}`
277+
}
278+
174279
function normalizeGrokToolResult(value) {
175280
const output = value && typeof value === 'object' && !Array.isArray(value) &&
176281
Object.prototype.hasOwnProperty.call(value, 'output')
@@ -232,11 +337,40 @@ function normalizeHostPayload(host, payload) {
232337
if (normalized.sessionId && !normalized.session_id) normalized.session_id = normalized.sessionId
233338
if (normalized.transcriptPath && !normalized.transcript_path) normalized.transcript_path = normalized.transcriptPath
234339
if (normalized.stopReason && !normalized.stop_reason) normalized.stop_reason = normalized.stopReason
235-
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())) {
340+
if (typeof normalized.tool_input === 'string') {
341+
normalized.tool_input = parseToolInputEnvelope(normalized.tool_input) || normalized.tool_input
342+
}
343+
if (!normalized.tool_call_id && !normalized.toolCallId) {
344+
normalized.tool_call_id = copilotToolCallId(normalized)
345+
normalized.toolCallId = normalized.tool_call_id
346+
}
347+
if (normalized.toolResult && !normalized.tool_result) {
348+
const result = normalized.toolResult
349+
normalized.tool_result = typeof result.textResultForLlm === 'string'
350+
? {
351+
success: result.resultType === 'success',
352+
result: result.textResultForLlm,
353+
...(result.resultType === 'success'
354+
? {}
355+
: { error: result.textResultForLlm || 'Copilot tool call failed.' })
356+
}
357+
: result
358+
}
359+
const continuationPrompts = [
360+
normalized.prompt,
361+
normalized.transformedPrompt,
362+
normalized.transformed_prompt
363+
]
364+
if ((originalEvent === 'userPromptSubmitted' ||
365+
originalEvent === 'userPromptTransformed') &&
366+
continuationPrompts.some(value =>
367+
/^Progressive Skill route is incomplete:/i.test(String(value || '').trim())
368+
)) {
238369
normalized.devcodex_host_continuation = true
239370
}
371+
if (originalEvent === 'userPromptTransformed') {
372+
normalized.devcodex_host_transform_only = true
373+
}
240374
}
241375
if (host === 'gemini' && typeof payload?.prompt_response === 'string') normalized.response = payload.prompt_response
242376
return { originalEvent, mappedEvent, payload: normalized }
@@ -471,10 +605,20 @@ function runHostAdapter(host, input, options = {}) {
471605
}
472606
const lifecycle = options.lifecycle || path.join(__dirname, 'lifecycle.cjs')
473607
const spawn = options.spawnSync || spawnSync
608+
const payloadCwd = String(input?.cwd || input?.workingDirectory || '').trim()
609+
const lifecycleCwd = (() => {
610+
if (!payloadCwd) return undefined
611+
try {
612+
return fs.statSync(payloadCwd).isDirectory() ? path.resolve(payloadCwd) : undefined
613+
} catch {
614+
return undefined
615+
}
616+
})()
474617
const child = spawn(process.execPath, [lifecycle], {
475618
input: JSON.stringify(normalized.payload),
476619
encoding: 'utf8',
477620
maxBuffer: 8 * 1024 * 1024,
621+
...(lifecycleCwd ? { cwd: lifecycleCwd } : {}),
478622
env: { ...(options.env || process.env), DEVCODEX_HOST_PLATFORM: host }
479623
})
480624
if (child.error) {
@@ -547,6 +691,11 @@ function applyCliEnvironmentOverrides (argv, env = process.env) {
547691
if (workspaceRootIndex >= 0 && argv[workspaceRootIndex + 1]) {
548692
env.DEVCODEX_WORKSPACE_ROOT = path.resolve(argv[workspaceRootIndex + 1])
549693
}
694+
const contextEpochIndex = argv.indexOf('--context-epoch')
695+
if (contextEpochIndex >= 0 && argv[contextEpochIndex + 1]) {
696+
env.DEVCODEX_CONTEXT_EPOCH = String(argv[contextEpochIndex + 1]).trim()
697+
env.DEVCODEX_CONTEXT_EPOCH_SOURCE = 'host-adapter-cli'
698+
}
550699
const eventIndex = argv.indexOf('--event')
551700
if (eventIndex >= 0 && argv[eventIndex + 1]) {
552701
env.DEVCODEX_HOST_EVENT = String(argv[eventIndex + 1]).trim()

‎hooks/_runtime/lifecycle-project-target.cjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,12 @@ function buildLifecycleProjectTargetUtils({
170170
}
171171

172172
function resolvePromptTarget(previousState, payload, prompt, projectCandidate) {
173-
if (hasMultiProjectExemption(prompt)) {
174-
return { activeProject: '', activeScope: LAYOUT.enabled ? 'workspace' : 'project', source: 'workspace-exemption', clearSticky: true }
175-
}
176173
if (projectCandidate.project) {
177174
return { activeProject: projectCandidate.project, activeScope: 'project', source: projectCandidate.source || 'explicit' }
178175
}
176+
if (hasMultiProjectExemption(prompt)) {
177+
return { activeProject: '', activeScope: LAYOUT.enabled ? 'workspace' : 'project', source: 'workspace-exemption', clearSticky: true }
178+
}
179179
if (CONTEXT_PROJECT) {
180180
return { activeProject: CONTEXT_PROJECT, activeScope: 'project', source: 'context' }
181181
}

‎hooks/_runtime/lifecycle.cjs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,32 @@ async function main() {
12471247
writeStdout(noopOutput())
12481248
return
12491249
}
1250+
if (payload.devcodex_host_transform_only === true &&
1251+
state.contextAcquisition?.contextEpoch) {
1252+
let progressiveSkillRouteMsg = ''
1253+
try {
1254+
const {
1255+
formatSkillRouteBootstrapInjection
1256+
} = require('./skill-route-tool.cjs')
1257+
if (state.progressiveSkillRoute?.bootstrap) {
1258+
progressiveSkillRouteMsg = formatSkillRouteBootstrapInjection(
1259+
state.progressiveSkillRoute.bootstrap
1260+
)
1261+
}
1262+
} catch {}
1263+
state.lastReason = 'copilot-transform-projection'
1264+
saveState(state)
1265+
writeStdout(contextMessageOutput(
1266+
'UserPromptSubmit',
1267+
[
1268+
buildBootstrapMessage(state),
1269+
buildExecutionModeContextMessage(state),
1270+
buildGovernanceIntakeContextMessage(state.governanceIntake),
1271+
progressiveSkillRouteMsg
1272+
].filter(Boolean).join('\n\n')
1273+
))
1274+
return
1275+
}
12501276
const workflowCompletionLifecycle = state.workflowCompletionLifecycle
12511277
state = resetState(mode, state)
12521278
state.workflowCompletionLifecycle = workflowCompletionLifecycle

‎hooks/_runtime/skill-route-tool.cjs‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,22 @@ function handleSkillRoute (input, options = {}) {
11971197
}
11981198
}
11991199

1200+
function formatSkillRouteBootstrapInjection (bootstrap) {
1201+
const injectionText = [
1202+
'### DevCodex · SkillRouteBootstrapV1',
1203+
JSON.stringify(bootstrap),
1204+
'',
1205+
'Use the local `skill_route` Tool. For a non-explicit task, read every catalog page before one `commit` choice (`skillId` is one id or null).',
1206+
'Do not infer workflow roots, paths, dependencies, or body content. After a complete plan, call `load_stage` only when entering that stage.'
1207+
].join('\n')
1208+
if (byteLength(injectionText) > 4 * 1024) {
1209+
const error = new Error('SKILL_ROUTE_BOOTSTRAP_BUDGET_BLOCKED')
1210+
error.code = 'SKILL_ROUTE_BOOTSTRAP_BUDGET_BLOCKED'
1211+
throw error
1212+
}
1213+
return injectionText
1214+
}
1215+
12001216
function bootstrapSkillRouteForTurn (input, options = {}) {
12011217
const target = resolveProjectTarget(options.inputRoot || input.cwd || process.cwd(), input.project)
12021218
const modeReceipt = resolveSkillRouteMode({
@@ -1229,18 +1245,7 @@ function bootstrapSkillRouteForTurn (input, options = {}) {
12291245
runtimeRoot: options.runtimeRoot,
12301246
packageRoot: options.packageRoot
12311247
})
1232-
const injectionText = [
1233-
'### DevCodex · SkillRouteBootstrapV1',
1234-
JSON.stringify(outcome.bootstrap),
1235-
'',
1236-
'Use the local `skill_route` Tool. For a non-explicit task, read every catalog page before one `commit` choice (`skillId` is one id or null).',
1237-
'Do not infer workflow roots, paths, dependencies, or body content. After a complete plan, call `load_stage` only when entering that stage.'
1238-
].join('\n')
1239-
if (byteLength(injectionText) > 4 * 1024) {
1240-
const error = new Error('SKILL_ROUTE_BOOTSTRAP_BUDGET_BLOCKED')
1241-
error.code = 'SKILL_ROUTE_BOOTSTRAP_BUDGET_BLOCKED'
1242-
throw error
1243-
}
1248+
const injectionText = formatSkillRouteBootstrapInjection(outcome.bootstrap)
12441249
return {
12451250
schemaVersion: 'SkillRouteBootstrapOutcomeV1',
12461251
active: true,
@@ -1260,6 +1265,7 @@ module.exports = {
12601265
finalizeResponse,
12611266
handleSkillRoute,
12621267
evaluateProgressiveSkillRouteStop,
1268+
formatSkillRouteBootstrapInjection,
12631269
bootstrapSkillRouteForTurn,
12641270
summarizePlan,
12651271
buildObligationLedger,

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"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-retirement && npm run test:skill-route-lifecycle",
9191
"probe:skill-route:s15:grok": "node scripts/probe-skill-route-s15-grok.js",
9292
"probe:skill-route:s15:host": "node scripts/probe-skill-route-s15-host.js",
93+
"sample:skill-route:retirement": "node scripts/run-skill-route-retirement-sample.js",
9394
"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",
9495
"test:global-install-smoke": "node scripts/test-global-install-smoke.js",
9596
"benchmark:execution-chain": "node scripts/benchmark-execution-chain.js",
@@ -211,6 +212,7 @@
211212
"scripts/check-skill-route-retirement.js",
212213
"scripts/probe-skill-route-s15-grok.js",
213214
"scripts/probe-skill-route-s15-host.js",
215+
"scripts/run-skill-route-retirement-sample.js",
214216
"scripts/lib/skill-route-test-fixture.js",
215217
"scripts/test-implement-start-gate.js",
216218
"scripts/lib/skills-deploy-mode.js",

0 commit comments

Comments
 (0)