Skip to content

Commit 609dcf2

Browse files
committed
fix: 修复安装态 Skill 来源与发布状态语义校验
1 parent ae1e312 commit 609dcf2

15 files changed

Lines changed: 221 additions & 69 deletions

‎changelogs/releases/v1.20.2.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
- Skill 目录保留完整身份、依赖和适用边界;提及 Skill 与实际读取正文分开记录,阶段结束时核对实际加载证据。
1010
- Codex MCP 首屏提供完整工具目录,避免客户端未翻页时遗漏上下文计划与读取工具;其他宿主保留有界分页。完整参数 Schema 保持不变。
1111
- 每代运行时携带自己的 Skill、索引和清单。新连接解析当前代,已打开的连接继续使用其已加载代;安装成功和当前连接加载状态分别说明。
12+
- 修复安装态验证仍使用旧共享 Skill 路径、发布包布局未被识别的问题;安装包从自身文件独立复算运行时与适配器摘要,并与受管代次交叉核对。
1213
- 修复旧版 MCP 计划可选字段缺省及路由注册表来源更新的兼容读取;同一路由定义可迁移到当前注册表,定义改变、身份损坏或不支持的协议版本仍要求刷新连接。
1314
- 卸载根据安装回执、实际文件和其他宿主消费者对账。共享运行时仍被使用,或消费者回执缺失但文件仍存在时,保留共享资产并返回未完成清理及可重试证据。
1415
- 修复卸载配置恢复依赖当前包配置、残留重试和 generation 清理扫描重复问题;项目历史与用户文件遵守各自保留边界。
16+
- 消除卸载中逐阶段重写整份事务日志的重复开销,保留写前恢复资料、最终持久化及旧版本崩溃恢复能力。
1517
- 记忆、报告和任务产物使用对应模板或明确的模板扩展,记录实际生成来源;避免同一产物槽重复生成。
1618
- 模板结构检查与用户意图满足度分别留证。历史产物对比新版模板得到诊断结果,不回填虚假的历史生成依据,也不批量改写历史报告。
1719
- 检查优化任务目录,记录有界扫描未覆盖的范围;包含流程简称的正文不能让整个任务跳过检查。
1820
- 实际隔离 Codex CLI 探针核对工具调用、上下文重绑和业务文件;协议探针、自然语言业务结果、模型自述与外部宿主拒绝分别记录。
21+
- 发布说明文字可以使用任务要求的语言;Profile 生命周期校验采用结构化状态和实际发行证据,不再要求英文前缀。Windows 兼容检查按独立任务分组,保留原有完整检查范围。
1922
- 更新用户说明、Profile 功能证据、验证清单和发行包依赖,补充跨会话、跨 generation、缺失回执与错误证据的负向回归。
2023
- 同步公开站点生成数据和运行代次候选日期;更新覆盖率工具链的 brace-expansion 兼容补丁,并补齐移除文本路由后的协议消费者回归。
2124

‎changelogs/unreleased.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# 未发布变更(Unreleased)
22

33
> **用途**: 记录尚未正式发版的实现级变更。
4-
> **当前**: v1.20.1 已发布;意图驱动执行链与产物一致性整改正在验证。以下旧候选条目保留历史上下文,不能作为当前未发布版本的资格证明。
4+
> **当前**: 本次意图驱动执行链与产物一致性整改已归档至 [v1.20.2 版本说明](releases/v1.20.2.md)。是否已发布以 npm registry、Git tag 与 GitHub Release 为准;下方摘要和旧候选条目保留追踪上下文,不能作为当前发行资格证明。
55
6-
## 当前未发布实现候选
6+
## v1.20.2 归档摘要
77

88
- **意图驱动与中文偏好**:当前模型提交来源绑定的语义决定,统一项目、流程、自动推进、验证控制与语言选择;确认包装、引用和固定词句不再改变权威状态。任务偏好在恢复后保留,计划工具返回时同步传递语言要求。
99
- **Skill 读取与代际一致性**:P/W/G 共用身份和依赖解析,完整分页目录与实际读取回执决定阶段进度;正文引用不激活 Skill,收尾按阶段读取。同一连接绑定同代 runtime、Skill 与 portfolio,旧连接保留旧代。Profile 工具目录按宿主传输边界提供完整 Schema,Codex 实际 CLI S15 已通过。
1010
- **升级与全局卸载**:活进程租约与 GC claim 联动,未结算清理保留回执;原安装配置按原回执核对。其他宿主存在运行残留而回执缺失时保留共享资源。受管卸载不清空项目历史,也不代替 npm 卸载。
1111
- **模板与审查证据**:准入、会话记忆与 SUMMARY 由命名模板生成;结构资格、实际写入和任务完成分别记录。历史文件只记录观察和未知依据,优化任务纳入检查;正文“通过”或轻量流程示例不成为验证豁免。复审结果绑定候选、报告和实际证据摘要。
12-
- **恢复与验证**:未结算操作的 Stop 保留诊断,owner 转换传播本次提交 fence,正式终止后仅收缩会话投影而保留完整任务历史。新增隔离自然任务入口,业务结果以实际文件与计算核验,未完成观察保持 UNVERIFIED;本轮尚未发布或升级生产运行时。
12+
- **恢复与验证**:未结算操作的 Stop 保留诊断,owner 转换传播本次提交 fence,正式终止后仅收缩会话投影而保留完整任务历史。新增隔离自然任务入口,业务结果以实际文件与计算核验,未完成观察保持 UNVERIFIED;实际发布和安装状态以发行证据为准。
1313

1414
- **v1.20.0 发布候选 — 任务连续性与工程控制面升级**:汇总 Stage A/B 的任务写入代际、合法 CP 演进续代、taskless 恢复、项目优先 Profile、多语言连续性、分层 SkillRoute、宿主原生多 Agent 编排合同、验证执行提速与修复批次收敛。完整说明见 [`changelogs/releases/v1.20.0.md`](./releases/v1.20.0.md)。正式发布事实仍只由 tag、registry、GitHub Release 与发布后安装回读共同成立。
1515
- **两阶段精确制品发布防护**:tag 资格阶段只生成一个正式精确 tarball,对同一制品完成隔离 consumer/HOME/prefix/cache/workspace 安装并保存;本机复用该制品完成真实 Codex G1→G2 后,才通过 `publish-qualified` 续发 npm 与 GitHub Release,避免 tag 触发后未经真实宿主验收即发生不可逆发布。

‎content/skills/portfolio.json‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"portfolioEvidenceDigest": "8364a73c66ed2ec9787cdb9300223278d128a45b689a2b8deda0d1f03189f42b",
1616
"consumerInventoryFileCount": 713,
1717
"consumerInventoryDigest": "424ec22febd792e124b514f3a668703849b8d4dee64719c8a7d670f49499c2ca",
18-
"consumerProjectionDigest": "a4f4e30ac928408457dc7ce28437695613dd163be0d1ebf7f6297de9c38d38c0",
19-
"portfolioInputDigest": "65fd1000c4271e76177408af9d9d62b0868dcf281ee13bede753c449a09b6876"
18+
"consumerProjectionDigest": "8aabeb2fab97a794d9466c147f9cd6e4e12c5fda6e00fb16f34a992e03672b60",
19+
"portfolioInputDigest": "6995725b163c1867fe27b23ea3795b928b83530a1f3d4e6cb2e66447c220b15c"
2020
},
2121
"ordering": "skills.id asc; graph edges from/to asc",
2222
"summary": {
@@ -11015,6 +11015,10 @@
1101511015
"path": "scripts/test-feature-inventory-evidence.js",
1101611016
"role": "current"
1101711017
},
11018+
{
11019+
"path": "scripts/test-global-skill-runtime-root.js",
11020+
"role": "current"
11021+
},
1101811022
{
1101911023
"path": "scripts/test-governance-intake.js",
1102011024
"role": "current"
@@ -11166,6 +11170,7 @@
1116611170
"scripts/test-docs-audience-intent.js",
1116711171
"scripts/test-execution-chain-evolution.js",
1116811172
"scripts/test-feature-inventory-evidence.js",
11173+
"scripts/test-global-skill-runtime-root.js",
1116911174
"scripts/test-governance-intake.js",
1117011175
"scripts/test-hooks-ingress-continuity.js",
1117111176
"scripts/test-hooks-runtime.js",
@@ -11279,6 +11284,7 @@
1127911284
"scripts/test-docs-audience-intent.js",
1128011285
"scripts/test-execution-chain-evolution.js",
1128111286
"scripts/test-feature-inventory-evidence.js",
11287+
"scripts/test-global-skill-runtime-root.js",
1128211288
"scripts/test-governance-intake.js",
1128311289
"scripts/test-hooks-ingress-continuity.js",
1128411290
"scripts/test-hooks-runtime.js",

‎hooks/_runtime/evidence/codex-skill-route-pass.v1.json‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@
55
"hostVariant": "codex-cli/exec-user-global-local-stdio",
66
"testedVersion": "codex-cli 0.153.4",
77
"protocol": "MCP 2024-11-05",
8-
"runtimeContractDigest": "2632c548b611e3bfe6d0eeaea4cdb61d2b9c0406f994d257a80ae384405ad729",
8+
"runtimeContractDigest": "817bcbf6b7725c0aaa833246a58c67c9b87b2450256f566ac23a2d1f2d2e77be",
99
"hostAdapterDigest": "1095efd90a80d223c5990a70a8503f5cbbe818e4d2f5253b944bce92536873a3",
10-
"sourceEvidenceDigest": "caf00f55882dbb5d8938f40662631b1313b85afab3db4c7d428b71d89a212a1f",
10+
"sourceEvidenceDigest": "2370b83d264e524303aef870da1f92c8e35cdfccc16c111a7000554e379af59e",
1111
"sourceProbe": {
1212
"schemaVersion": "SkillRouteS15EvidenceV1",
13-
"probeRunId": "s15-codex-probe-17c1fdfd-201f-4717-915b-3c093c1095a7",
13+
"probeRunId": "s15-codex-probe-6d08427b-174f-42fc-87d5-b4d05e19702e",
1414
"authorizationSource": "isolated-probe-authority",
1515
"contextSource": "host-hooks",
1616
"observationMode": "hook-post-history",
1717
"receiptStatus": "relevant-complete",
18-
"completedAt": "2026-09-08T17:19:52.426Z"
18+
"completedAt": "2026-09-09T02:16:04.857Z"
1919
},
2020
"runtimeBinding": {
2121
"source": "isolated-source-candidate",
22-
"expectedDigest": "2632c548b611e3bfe6d0eeaea4cdb61d2b9c0406f994d257a80ae384405ad729",
23-
"generationDigest": "2632c548b611e3bfe6d0eeaea4cdb61d2b9c0406f994d257a80ae384405ad729",
24-
"modeReceiptDigest": "2632c548b611e3bfe6d0eeaea4cdb61d2b9c0406f994d257a80ae384405ad729",
25-
"routeEnvelopeDigest": "2632c548b611e3bfe6d0eeaea4cdb61d2b9c0406f994d257a80ae384405ad729"
22+
"expectedDigest": "817bcbf6b7725c0aaa833246a58c67c9b87b2450256f566ac23a2d1f2d2e77be",
23+
"generationDigest": "817bcbf6b7725c0aaa833246a58c67c9b87b2450256f566ac23a2d1f2d2e77be",
24+
"modeReceiptDigest": "817bcbf6b7725c0aaa833246a58c67c9b87b2450256f566ac23a2d1f2d2e77be",
25+
"routeEnvelopeDigest": "817bcbf6b7725c0aaa833246a58c67c9b87b2450256f566ac23a2d1f2d2e77be"
2626
},
2727
"probe": {
2828
"schemaVersion": "SkillRouteProbeSummaryV1",

‎hooks/_runtime/global-skill-runtime-root.cjs‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,19 @@ function hasPortfolio (root, fsImpl = fs) {
4040
function sourceSkillsRoot (packageRoot, fsImpl = fs) {
4141
const packageJson = readJson(path.join(packageRoot, 'package.json'), fsImpl)
4242
const contentRoot = path.join(packageRoot, 'content', 'skills')
43+
// The published package materializes content/skills as skills/. An existing
44+
// source tree stays authoritative even when its portfolio is incomplete.
45+
const skillsRoot = fsImpl.existsSync(contentRoot) ||
46+
fsImpl.existsSync(path.join(packageRoot, 'content', 'manifest.json'))
47+
? contentRoot
48+
: path.join(packageRoot, 'skills')
4349
if (
4450
packageJson?.name !== 'devcodex' ||
45-
!hasPortfolio(contentRoot, fsImpl)
51+
!hasPortfolio(skillsRoot, fsImpl)
4652
) return null
4753
return {
48-
root: contentRoot,
49-
portfolioPath: path.join(contentRoot, 'portfolio.json')
54+
root: skillsRoot,
55+
portfolioPath: path.join(skillsRoot, 'portfolio.json')
5056
}
5157
}
5258

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"status": "PASS",
1414
"testedVersion": "codex-cli 0.153.4",
1515
"protocol": "MCP 2024-11-05",
16-
"runtimeContractDigest": "2632c548b611e3bfe6d0eeaea4cdb61d2b9c0406f994d257a80ae384405ad729",
16+
"runtimeContractDigest": "817bcbf6b7725c0aaa833246a58c67c9b87b2450256f566ac23a2d1f2d2e77be",
1717
"hostAdapterDigest": "1095efd90a80d223c5990a70a8503f5cbbe818e4d2f5253b944bce92536873a3",
1818
"evidenceRef": "hooks/_runtime/evidence/codex-skill-route-pass.v1.json",
19-
"evidenceDigest": "aac58d8cc0f41aabeb3af4e6ad61d09154a3427730a93500265f1ced4afed802",
19+
"evidenceDigest": "13ef8b58b0335d32a957e8778fcaf2e8dcdcbd188a3e0a231effa5184808b7e0",
2020
"entrySurface": "codex exec --ephemeral",
2121
"bootstrapDelivery": "stable user-global Hook launcher UserPromptSubmit or profile_context_plan fallback",
2222
"defaultEligible": true

‎public-site/data/public-product-projection.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": "PublicProductProjectionV2",
3-
"generatedAt": "2026-09-08T19:22:07.321Z",
3+
"generatedAt": "2026-09-09T02:10:12.178Z",
44
"release": {
55
"version": "1.20.2"
66
},
@@ -1187,7 +1187,7 @@
11871187
"package": "199bae54b2c3243290dcb9830b3af4deaaf933b9351c2de1eeb3125fc070cc38",
11881188
"expression": "60e525d34e5531e6b4bbbb77f8b34bb93c9ca9ebee5df4fe2812749232175c3c",
11891189
"workflows": "d9eb7086866fe3c3815f673957ed978c1594079261009ac775bf3e1336ad44fd",
1190-
"portfolio": "b275c8622b3c16ea1bbbeaf9fedb761f7c6ad7e0529295be3eeeda8cd3d1174c",
1190+
"portfolio": "bd64d4c138ac43491cef4be8ed291d6f2a501e27c3f3c8fa70be4b7e9a2bc150",
11911191
"taxonomy": "b673f27fa58c2d7a57b9aa906562faed2aca094a727c8686d941c39a5490f030"
11921192
}
11931193
}

‎scripts/lib/profile-current-truth.js‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function validateObservedObject(value, label, identityField, errors) {
6060

6161
function validateCandidateObject(value, errors) {
6262
if (!isPlainObject(value)) {
63-
errors.push('candidate must be an object when releaseState is candidate')
63+
errors.push('candidate must be an object for a source candidate')
6464
return
6565
}
6666
if (!/^\d+\.\d+\.\d+$/.test(String(value.targetVersion || ''))) {
@@ -165,6 +165,19 @@ function compareSemver(left, right) {
165165
return 0
166166
}
167167

168+
function currentTruthLifecycle(record) {
169+
if (Object.prototype.hasOwnProperty.call(record, 'sourceCandidate')) {
170+
const status = record.sourceCandidate?.status
171+
if (!SOURCE_CANDIDATE_STATUSES.has(status)) return null
172+
return status === 'RELEASED' ? 'released' : 'candidate'
173+
}
174+
// Read-only compatibility for records predating SourceCandidateTruthV1.
175+
// Display prose never grants a state; the existing evidence is still checked.
176+
if (Object.prototype.hasOwnProperty.call(record, 'candidate')) return 'candidate'
177+
return record.sourceVersion === record.npmLatest &&
178+
record.githubRelease?.tag === `v${record.sourceVersion}` ? 'released' : null
179+
}
180+
168181
function validateTruthRecord(record) {
169182
const errors = []
170183
if (!isPlainObject(record)) return { valid: false, errors: ['record must be a JSON object'] }
@@ -182,7 +195,8 @@ function validateTruthRecord(record) {
182195
if (typeof record.releaseState !== 'string' || !record.releaseState.trim()) {
183196
errors.push('releaseState must be a non-empty string')
184197
}
185-
if (/^candidate\b/i.test(String(record.releaseState || ''))) {
198+
if (currentTruthLifecycle(record) === 'candidate' ||
199+
Object.prototype.hasOwnProperty.call(record, 'candidate')) {
186200
validateCandidateObject(record.candidate, errors)
187201
}
188202
if (!/^[0-9a-f]{40}$/i.test(String(record.gitHead || ''))) {
@@ -315,8 +329,9 @@ function validateDevCodexCurrentTruth(input = {}) {
315329
const errors = [...parsed.errors]
316330
const record = parsed.record
317331
if (record) {
318-
const released = /^released\b/i.test(record.releaseState)
319-
const candidate = /^candidate\b/i.test(record.releaseState)
332+
const lifecycle = currentTruthLifecycle(record)
333+
const released = lifecycle === 'released'
334+
const candidate = lifecycle === 'candidate'
320335
if (input.packageVersion && record.sourceVersion !== input.packageVersion) {
321336
errors.push(`sourceVersion drift: ${record.sourceVersion} != ${input.packageVersion}`)
322337
}
@@ -335,7 +350,7 @@ function validateDevCodexCurrentTruth(input = {}) {
335350
errors.push(`sourceCandidate.candidateId drift: ${record.sourceCandidate?.candidateId} != ${input.candidateId}`)
336351
}
337352
if (!released && !candidate) {
338-
errors.push(`releaseState must describe a released source or an authorized candidate: ${record.releaseState}`)
353+
errors.push('current lifecycle requires a valid sourceCandidate status or legacy candidate/release evidence')
339354
} else if (released) {
340355
if (input.packageVersion && record.npmLatest !== input.packageVersion) {
341356
errors.push(`npmLatest drift: ${record.npmLatest} != ${input.packageVersion}`)

‎scripts/lib/s15-candidate-host.js‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
applyGlobalHostConfig
1010
} = require('./global-host-config')
1111
const { resolveGlobalHostTarget } = require('./global-host-target')
12+
const { resolveGlobalSkillRuntimeRoot, samePath: sameSkillPath } = require('../../hooks/_runtime/global-skill-runtime-root.cjs')
1213
const {
1314
getLifecycleHostAdapterDigest
1415
} = require('../../hooks/_runtime/host-adapter-identity.cjs')
@@ -178,14 +179,6 @@ function bindInstalledProductionRuntime (options = {}) {
178179
`S15 installed ${hostId} production runtime escapes the managed host root`
179180
)
180181
const skillsRuntime = path.resolve(String(receipt.skillsRuntimeRoot || ''))
181-
assert(
182-
receipt.skillsRuntimeRoot && isPathInside(target.shared.root, skillsRuntime),
183-
`S15 installed ${hostId} production skills runtime escapes the managed shared root`
184-
)
185-
assert(
186-
fsImpl.existsSync(skillsRuntime),
187-
`S15 installed ${hostId} production skills runtime is missing`
188-
)
189182
const generationFile = path.join(runtimeRoot, 'runtime-generation.json')
190183
assert(fsImpl.existsSync(generationFile), `S15 installed ${hostId} production generation is missing`)
191184
const generation = JSON.parse(fsImpl.readFileSync(generationFile, 'utf8'))
@@ -199,6 +192,15 @@ function bindInstalledProductionRuntime (options = {}) {
199192
expectedRuntimeDigest,
200193
`S15 installed ${hostId} production runtime does not match current source`
201194
)
195+
const skillsBinding = resolveGlobalSkillRuntimeRoot({ runtimeRoot, packageRoot, env: baseEnv, fs: fsImpl })
196+
assert(
197+
skillsBinding.status === 'resolved' && skillsBinding.source === 'runtime-generation',
198+
`S15 installed ${hostId} production Skill generation or portfolio is invalid`
199+
)
200+
assert(
201+
receipt.skillsRuntimeRoot && sameSkillPath(skillsRuntime, skillsBinding.root),
202+
`S15 installed ${hostId} production Skill receipt does not match its runtime generation`
203+
)
202204
const installedHostAdapterDigest = readAdapterDigest(hostId, {
203205
entrySurface: options.entrySurface,
204206
env: baseEnv,
@@ -332,6 +334,11 @@ function prepareCandidateHostRuntime (options = {}) {
332334
/^[a-f0-9]{64}$/,
333335
`S15 candidate ${hostId} runtime digest missing`
334336
)
337+
const skillsBinding = resolveGlobalSkillRuntimeRoot({ runtimeRoot: installedTarget.runtimeRoot, packageRoot, env, fs: fsImpl })
338+
assert(
339+
skillsBinding.status === 'resolved' && skillsBinding.source === 'runtime-generation',
340+
`S15 candidate ${hostId} Skill generation or portfolio is invalid`
341+
)
335342
const nativeRegistration = hostId === 'grok'
336343
? syncGrok({
337344
pluginPath: installedTarget.files.plugin,
@@ -354,7 +361,7 @@ function prepareCandidateHostRuntime (options = {}) {
354361
hostId,
355362
home,
356363
env,
357-
target: installedTarget,
364+
target: { ...installedTarget, shared: { ...installedTarget.shared, skillsRuntime: skillsBinding.root } },
358365
generation,
359366
credentialFiles,
360367
nativeRegistration: nativeRegistration

0 commit comments

Comments
 (0)