Skip to content

Commit 8ecb69f

Browse files
committed
fix: close Codex progressive routing probe
1 parent 075f238 commit 8ecb69f

6 files changed

Lines changed: 114 additions & 16 deletions

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010
},
1111
{
1212
"hostVariant": "codex-cli/exec-user-global-local-stdio",
13-
"status": "UNVERIFIED",
14-
"testedVersion": "unverified",
13+
"status": "PASS",
14+
"testedVersion": "codex-cli 0.145.0",
1515
"protocol": "MCP 2024-11-05",
16-
"evidenceRef": null
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",
20+
"entrySurface": "codex exec --ephemeral",
21+
"bootstrapDelivery": "user-global hooks.json UserPromptSubmit",
22+
"defaultEligible": true
1723
},
1824
{
1925
"hostVariant": "copilot-cli/print-user-global-local-stdio",

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ function normalizeHostPayload(host, payload) {
307307
devcodexHostSurface: host,
308308
devcodexHostEventName: originalEvent
309309
}
310+
if (normalized.toolResponse !== undefined && normalized.tool_result === undefined) {
311+
normalized.tool_result = normalized.toolResponse
312+
}
313+
if (normalized.tool_response !== undefined && normalized.tool_result === undefined) {
314+
normalized.tool_result = normalized.tool_response
315+
}
310316
// Grok and Copilot camelCase payloads are normalized to the lifecycle field names.
311317
if (host === 'grok' || host === 'copilot') {
312318
if (normalized.toolName && !normalized.tool_name) normalized.tool_name = normalized.toolName

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,12 @@ function recordSkillRouteProbeObservation (activeRoot, turnBinding, evidence, op
838838
error.code = 'PROBE_EVIDENCE_DIGEST_INVALID'
839839
throw error
840840
}
841+
const marker = String(evidence.marker || '')
842+
if (!marker || String(evidence.markerDigest || '') !== sha256(marker)) {
843+
const error = new Error('PROBE_MARKER_DIGEST_INVALID')
844+
error.code = 'PROBE_MARKER_DIGEST_INVALID'
845+
throw error
846+
}
841847
const request = {
842848
op: 'probe_observation',
843849
project: evidence.project,
@@ -872,7 +878,7 @@ function recordSkillRouteProbeObservation (activeRoot, turnBinding, evidence, op
872878
hostVariant,
873879
evidenceDigest,
874880
observedOps: [...observedOps].sort(),
875-
markerDigest: sha256(evidence.marker),
881+
markerDigest: evidence.markerDigest,
876882
observedAt: new Date().toISOString()
877883
}
878884
const response = {

‎scripts/probe-skill-route-s15-host.js‎

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ function run (command, args, options = {}) {
134134
timeout: options.timeout || 120000,
135135
maxBuffer: 24 * 1024 * 1024
136136
})
137-
if (result.error) throw result.error
137+
if (result.error) {
138+
result.error.spawnResult = result
139+
throw result.error
140+
}
138141
return result
139142
}
140143

@@ -181,16 +184,16 @@ function buildPrompt ({ contextEpoch, hostId, project, skillId }) {
181184
return [
182185
'This is an isolated direct-observation acceptance probe.',
183186
`Project ${project}.`,
184-
'Treat the task as audit/testing. The probe operator has authorized its temporary internal route-state writes; do not request confirmation.',
187+
'Treat the task as isolated development/testing. The probe operator has authorized its temporary internal route-state writes; do not request confirmation.',
185188
'You MUST make the MCP tool calls below before producing any final answer.',
186189
'A JSON-only answer without actual tool calls is a failed probe, even if it matches the schema.',
187190
'Every final field is independently compared with persisted MCP state; never estimate, shorten, or invent a digest, stage id, marker, or receipt.',
188191
'Do not read the filesystem and do not infer any Skill body content.',
189192
'Use only the devcodex-profile and devcodex-memory MCP tools for the workflow below.',
190193
'First call profile_context_plan with:',
191194
JSON.stringify({
192-
intent: 'audit',
193-
changeTypes: ['testing'],
195+
intent: 'dev',
196+
changeTypes: ['source-code', 'testing'],
194197
contextEpoch,
195198
project,
196199
host: hostId,
@@ -208,6 +211,8 @@ function buildPrompt ({ contextEpoch, hostId, project, skillId }) {
208211
`Choose exactly the top-level Skill whose skillId is ${skillId}; do not substitute another acceptance or probe Skill.`,
209212
'For the first commit call use exactly these fields and no others: op, project, turnBinding, contextEpoch, catalogDigest, skillId, contextBinding.',
210213
'Immediately replan the same choice. Use exactly: op, project, turnBinding, contextEpoch, catalogDigest, skillId, contextBinding, previousPlanDigest, lateConditionId; set lateConditionId=test-validation.',
214+
'Wait for that conditional replan call to return successfully. Do not call load_stage unless its returned plan has generation=1 and activatedConditionIds contains test-validation.',
215+
'Run every skill_route call serially. If a required call returns an error, do not retry it or continue with an older plan.',
211216
'For load_stage use exactly: op, project, turnBinding, contextEpoch, generation, planDigest, stageId, and cursor only when the prior page returned one.',
212217
'Load every page of every stage in dependency order: entry, execution:test-validation, then closeout.',
213218
'Call status and require processComplete=true with every required stage loaded.',
@@ -226,6 +231,7 @@ function hostArgs (hostId, prompt, schemaPath, outputPath) {
226231
'exec',
227232
'--skip-git-repo-check',
228233
'--ephemeral',
234+
'--json',
229235
'--output-schema', schemaPath,
230236
'--output-last-message', outputPath,
231237
'--color', 'never',
@@ -434,6 +440,22 @@ function discoverFixtureState (fixture) {
434440
return found
435441
}
436442

443+
const PROBE_HOOK_FLAGS = Object.freeze([
444+
'--skill-route-probe-authority',
445+
'--skill-route-trace',
446+
'--lifecycle-trace',
447+
'--workspace-root',
448+
'--context-epoch'
449+
])
450+
451+
function stripProbeHookArgs (command) {
452+
const value = String(command || '')
453+
const offsets = PROBE_HOOK_FLAGS
454+
.map(flag => value.indexOf(` ${flag} `))
455+
.filter(offset => offset >= 0)
456+
return offsets.length ? value.slice(0, Math.min(...offsets)).trimEnd() : value
457+
}
458+
437459
function patchProbeHookCommand (
438460
hostId,
439461
authorityPath,
@@ -469,7 +491,7 @@ function patchProbeHookCommand (
469491
for (const [key, child] of Object.entries(value)) {
470492
if (key === 'command' && typeof child === 'string' &&
471493
child.includes('lifecycle-host-adapters.cjs')) {
472-
value[key] = `${child} ${suffix}`
494+
value[key] = `${stripProbeHookArgs(child)} ${suffix}`
473495
patchedCommands += 1
474496
} else {
475497
visit(child)
@@ -481,8 +503,27 @@ function patchProbeHookCommand (
481503
const patched = `${JSON.stringify(config, null, 2)}\n`
482504
fs.writeFileSync(configPath, patched, 'utf8')
483505
return () => {
484-
if (fs.existsSync(configPath) && fs.readFileSync(configPath, 'utf8') === patched) {
485-
fs.writeFileSync(configPath, original, 'utf8')
506+
if (!fs.existsSync(configPath)) return
507+
const current = JSON.parse(fs.readFileSync(configPath, 'utf8'))
508+
let restoredCommands = 0
509+
function restore (value) {
510+
if (!value || typeof value !== 'object') return
511+
for (const [key, child] of Object.entries(value)) {
512+
if (key === 'command' && typeof child === 'string' &&
513+
child.includes('lifecycle-host-adapters.cjs')) {
514+
const clean = stripProbeHookArgs(child)
515+
if (clean !== child) {
516+
value[key] = clean
517+
restoredCommands += 1
518+
}
519+
} else {
520+
restore(child)
521+
}
522+
}
523+
}
524+
restore(current)
525+
if (restoredCommands > 0) {
526+
fs.writeFileSync(configPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8')
486527
}
487528
}
488529
}
@@ -690,6 +731,7 @@ function main () {
690731
loadedStageIds,
691732
processComplete: loadedStageIds.length === requiredStageIds.length,
692733
bodyDigest: selectedChunk.bodyDigest,
734+
marker,
693735
markerDigest: sha256(marker),
694736
contextAcquisition: {
695737
source: 'host-hooks',
@@ -756,7 +798,8 @@ function main () {
756798
evidenceOutput: evidenceOutput ? path.resolve(evidenceOutput) : null
757799
})}\n`)
758800
} catch (error) {
759-
const errorCode = classifyFailure(descriptor, modelRun, error)
801+
const failedRun = modelRun || error.spawnResult || null
802+
const errorCode = classifyFailure(descriptor, failedRun, error)
760803
writeFailure(evidenceOutput, {
761804
errorCode,
762805
host: descriptor.lifecycleHost,
@@ -766,10 +809,10 @@ function main () {
766809
hostAdapterDigest,
767810
startedAt,
768811
failedAt: new Date().toISOString(),
769-
exitCode: modelRun?.status ?? null,
812+
exitCode: failedRun?.status ?? null,
770813
error: error.message,
771-
stdout: String(modelRun?.stdout || '').slice(-12000),
772-
stderr: String(modelRun?.stderr || '').slice(-12000),
814+
stdout: String(failedRun?.stdout || '').slice(-12000),
815+
stderr: String(failedRun?.stderr || '').slice(-12000),
773816
routeTrace: readTraceTail(routeTracePath),
774817
lifecycleTrace: readTraceTail(lifecycleTracePath),
775818
lifecycleState: readLifecycleSnapshot(fixture),
@@ -792,5 +835,6 @@ module.exports = {
792835
hostArgs,
793836
patchProbeHookCommand,
794837
resolveExecutable,
795-
resultSchema
838+
resultSchema,
839+
stripProbeHookArgs
796840
}

‎scripts/test-host-adapters.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ assert.strictEqual(claude.payload.devcodexHostSurface, 'claude')
6868
const codex = normalizeHostPayload('codex', { hook_event_name: 'PreCompact' })
6969
assert.strictEqual(codex.mappedEvent, 'PreCompact')
7070
assert.strictEqual(codex.payload.devcodexHostSurface, 'codex')
71+
const codexPost = normalizeHostPayload('codex', {
72+
hook_event_name: 'PostToolUse',
73+
tool_response: {
74+
success: false,
75+
error: { code: 'CONDITIONAL_UNAVAILABLE' }
76+
}
77+
})
78+
assert.deepStrictEqual(codexPost.payload.tool_result, {
79+
success: false,
80+
error: { code: 'CONDITIONAL_UNAVAILABLE' }
81+
})
7182
const copilot = normalizeHostPayload('copilot', {
7283
hookEventName: 'preToolUse',
7384
sessionId: 'copilot-session',

‎scripts/test-skill-route-retirement.js‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const {
1414
const {
1515
usage: retirementSampleUsage
1616
} = require('./run-skill-route-retirement-sample.js')
17+
const {
18+
buildPrompt: buildHostProbePrompt,
19+
hostArgs: buildHostProbeArgs,
20+
stripProbeHookArgs
21+
} = require('./probe-skill-route-s15-host.js')
1722

1823
const policy = require(POLICY_PATH)
1924
const runtimeDigest = crypto.createHash('sha256').update('runtime').digest('hex')
@@ -150,5 +155,25 @@ assert.strictEqual(unboundCurrentContract.status, 'BLOCK')
150155
assert(unboundCurrentContract.reasons.includes('current-runtime-digest-unbound'))
151156
assert(retirementSampleUsage().includes('--help'))
152157
assert(retirementSampleUsage().includes('--host <name[,name...]>'))
158+
const hostProbePrompt = buildHostProbePrompt({
159+
contextEpoch: 'ctx-probe',
160+
hostId: 'codex',
161+
project: 's15-codex',
162+
skillId: 'workspace-s15-probe'
163+
})
164+
assert(hostProbePrompt.includes('"intent":"dev"'))
165+
assert(hostProbePrompt.includes('"changeTypes":["source-code","testing"]'))
166+
assert(hostProbePrompt.includes('lateConditionId=test-validation'))
167+
assert(hostProbePrompt.includes('generation=1'))
168+
assert(buildHostProbeArgs('codex', 'probe', 'schema.json', 'output.json').includes('--json'))
169+
assert.strictEqual(
170+
stripProbeHookArgs(
171+
'node "C:\\runtime\\lifecycle-host-adapters.cjs" codex ' +
172+
'--skill-route-probe-authority "C:\\stale\\authority.json" ' +
173+
'--skill-route-trace "C:\\stale\\route.jsonl" ' +
174+
'--context-epoch "ctx-stale"'
175+
),
176+
'node "C:\\runtime\\lifecycle-host-adapters.cjs" codex'
177+
)
153178

154179
console.log('skill route retirement gate passed: hosts=5 runs=100 windowHours=72 negativeProbes=5 samplerHelp=PASS')

0 commit comments

Comments
 (0)