Skip to content

Commit 0e09d48

Browse files
committed
fix(hooks): close Stop-gate review findings F-01 through F-16
Recognize Grok lastAssistantMessage and standard completion-check headings, tighten PR-1 evidence, align R11 processGaps, add report/memory exemptions, update host-parity docs, and ship stop-gate regression suite with mode=dev acceptance probe.
1 parent 41ab30d commit 0e09d48

12 files changed

Lines changed: 413 additions & 104 deletions

‎changelogs/unreleased.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
## 当前未发布实现候选
77

8+
- **Stop 审查修复批(F-01~F-16)**:`lastAssistantMessage` 进入 visible 证据;完成检查识别 `### DevCodex · 完成检查`;PR-1 强证据;R11 gap 命名 + `completion-check-missing`;report/memory 可 N/A 豁免;host-parity 站点条件硬续文案;R10 路径/Honesty 探针;hook 矩阵优先源码 + adapter 保留 block。验证:`npm run test:stop-gate`。
89
- **Grok Stop 完成硬续 + EnforcementHonesty(20260726)**:`lifecycle-stop-gate.cjs` + lifecycle Stop 接线;`adaptGrokOutput`/`blockOutput` 保留 Stop `decision:block`;R9 `pr1-skipped`;R10 扩展 `CONTROL_PLANE_SOURCE_RE`(host-projections/host-parity)并复用 `checkCpGate`(safety-only 披露 `cp2-unconfirmed-write`);compliance/report/dev-default/cp-gate/dev-plan-review Honesty+R12 顺序索引;cannotClaim 改为「无正文/softCap fail-open」;探针 `test-lifecycle-stop-gate.js` + host-adapters/parity 更新。需求:`20260726-grok-stop-enforcement-honesty` B1–B4+B2b。
910
- **工作区 Skill 双层 resolve(S2 / W>G)**:新增 `hooks/_runtime/skill-resolution.cjs`(唯一 Owner):`workspace-namespace` 下 `.devcodex/workspace/skills` 优先于 `~/.agents/skills`,**reserved** skill id 禁止 W 覆盖;同次 digest/content 契约;`profile_skill_plan` / `BundleDecisionV2` 附加 `resolutionPlan` 并改写 selected source identity;Hook 对 workspace-skill 做 managed/control-plane carve-out;CLI `devcodex skill resolve`;`npm run test:skill-resolve`(含 hook 路径矩阵;并入 `test:control-plane`);`validation-manifest` 节点 `skill-resolve` + `skill-resolution-hook-paths` 进入 fast/full/profile-deploy/package-release;apply 目标禁入 W 树。需求:`双层Skill用户优先与意图加载` CP2 v1.2 S2 / CP3。
1011
- **WorkspaceSkillAutoMatch(P0.5 用户可见闭环 / PF-213)**:新增 `hooks/_runtime/workspace-skill-auto-match.cjs`:UserPromptSubmit 按 W skill `name`/`description` 引号触发语/id 匹配 → `resolveSkillRead` → `additionalContext` 注入正文(Claude/Codex 等 inject 宿主);Grok UPS 仍诚实不宣称 inject,但 **Stop `decision:block`** 在回复未遵守 skill 时强制再答一轮(官方 Stop Decision Control);CLI `devcodex skill match <prompt>`;探针 `test-workspace-skill-auto-match.js` / manifest 节点 `skill-auto-match`。禁止仅 CLI resolve 绿冒充「发 test 即生效」。

‎hooks/_runtime/lifecycle-payload-utils.cjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ function buildLifecyclePayloadUtils({ fs, path, payloadPreviewLimit, transcriptT
135135

136136
function getVisibleReplyEvidence(payload) {
137137
const directFieldNames = [
138+
// Official Grok/Claude Stop Decision Control field first (F-03).
139+
'lastAssistantMessage', 'last_assistant_message',
138140
'assistantMessage', 'assistant_message', 'response',
139141
'responseText', 'response_text', 'output', 'reply', 'content', 'message',
140142
// Extra Grok / harness aliases when hosts expose final turn text (W8).

‎hooks/_runtime/lifecycle-stop-gate.cjs‎

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
/**
44
* Stop completion / process gate (Grok Stop hard-continue + process gaps).
55
* Spec: requirements/20260726-grok-stop-enforcement-honesty/02-技术方案.md §3 / §9b
6+
* R11 processGaps (canonical):
7+
* entry-check-missing | completion-check-missing | final-validation-summary |
8+
* report-missing | memory-missing | pr1-skipped | cp2-unconfirmed-write |
9+
* stop-continuation-exhausted
610
*/
711

812
const path = require('path')
@@ -15,7 +19,7 @@ try {
1519
analyzeFinalValidationSummarySample = () => ({ classification: 'not-claimed', status: 'not-claimed' })
1620
}
1721

18-
function extractLastAssistantMessage(payload) {
22+
function extractLastAssistantMessage (payload) {
1923
if (!payload || typeof payload !== 'object') return ''
2024
const direct = payload.lastAssistantMessage || payload.last_assistant_message || payload.assistantMessage
2125
if (typeof direct === 'string' && direct.trim()) return direct
@@ -26,27 +30,43 @@ function extractLastAssistantMessage(payload) {
2630
return ''
2731
}
2832

29-
function completionClaimed(text) {
33+
function completionClaimed (text) {
3034
return /完成检查|completion-check|CompletionEvidenceGate|已完成|已收口|宣告完成|任务完成|FinalValidationSummary|全绿|SC15/i.test(text || '')
3135
}
3236

33-
function askingCp2Confirm(text) {
34-
return /确认\s*CP2|CP2\s*确认|请确认(?:技术方案|方案)|确认(?:本|该)?技术方案|确认\s*CP2/i.test(text || '')
37+
function askingCp2Confirm (text) {
38+
// F-05: cover confirm-request presentation, not only "确认 CP2" shorthand
39+
return /确认\s*CP2|CP2\s*确认|请确认(?:技术方案|方案)|确认(?:本|该)?技术方案|确认\s*CP2|技术方案\s*待确认|请求确认\s*CP2|CP2\s*(?待确认)?/i.test(text || '')
3540
}
3641

37-
function hasEntryCheck(text) {
38-
return /###\s*DevCodex\s*·\s*入口检查|PC0\s*[|::]|PC0~PC7|入口检查块/i.test(text || '')
42+
function hasEntryCheck (text) {
43+
return /###\s*DevCodex\s*·\s*入口检查|PC0\s*[|::]|PC0~PC7|入口检查块|DevCodexVisibleEnvelopeV1\s*·\s*entry-check/i.test(text || '')
3944
}
4045

41-
function findActiveTaskRoot(state) {
46+
/** F-14/F-16: standard completion-check block present */
47+
function hasCompletionCheck (text) {
48+
return (
49+
/###\s*DevCodex\s*·\s*完成检查/i.test(text || '') ||
50+
/DevCodexVisibleEnvelopeV1\s*·\s*completion-check/i.test(text || '') ||
51+
/🛡️\s*DEV\s*模式\s*\|\s*合规检查/i.test(text || '')
52+
)
53+
}
54+
55+
/**
56+
* F-08: skip report/memory hard gaps when explicit N/A or simple/probe skipReason.
57+
*/
58+
function artifactGapsExempt (text) {
59+
return /SimpleTaskFastPath|报告\s*[::]\s*N\/A|report\s*[::=]\s*N\/A|记忆\s*[::]\s*N\/A|memory\s*[::=]\s*N\/A|skipReason\s*[::=]\s*(?:simple-task|simple_task|probe|temp|tmp)|产物\s*N\/A\s*\+\s*skipReason/i.test(text || '')
60+
}
61+
62+
function findActiveTaskRoot (state) {
4263
try {
4364
const root = state?.activeNamespaceRoot || state?.workspaceRoot || process.cwd()
4465
const req = path.join(root, 'requirements')
4566
if (!fs.existsSync(req)) return null
4667
const names = fs.readdirSync(req, { withFileTypes: true })
4768
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
4869
.map(d => d.name)
49-
// Prefer most recently modified task with 02-技术方案
5070
let best = null
5171
let bestM = 0
5272
for (const name of names) {
@@ -65,15 +85,29 @@ function findActiveTaskRoot(state) {
6585
}
6686
}
6787

68-
function pr1EvidenceOk(taskRoot) {
88+
/**
89+
* F-04: PR-1 pass evidence must be strong (zero-blocker / explicit pass row).
90+
* Rejects loose "某某通过" alone.
91+
*/
92+
function pr1EvidenceOk (taskRoot) {
6993
if (!taskRoot || !fs.existsSync(taskRoot)) return false
7094
try {
7195
const files = fs.readdirSync(taskRoot)
72-
const review = files.find(f => /^03-.*方案复审/i.test(f) || /^04-.*方案复审/i.test(f) || /方案复审/i.test(f))
96+
const review = files.find(f =>
97+
/^03-.*方案复审/i.test(f) ||
98+
/^04-.*方案复审/i.test(f) ||
99+
/方案复审/i.test(f)
100+
)
73101
if (review) {
74102
const body = fs.readFileSync(path.join(taskRoot, review), 'utf8')
75-
if (/PR-1/i.test(body) && (/zero\s*blocker|open blocker\s*=\s*0|✅\s*通过|通过/i.test(body))) return true
76-
if (/PR-1/i.test(body) && /通过/.test(body) && !/不通过|阻断.*PR-1/i.test(body)) return true
103+
if (!/PR-1/i.test(body)) return false
104+
// Strong pass signals only
105+
if (/open\s*blocker\s*=\s*0|zero\s*blocker|blockers?\s*=\s*0/i.test(body)) return true
106+
if (/PR-1[^\n]{0,40}(?:✅\s*通过|通过\s*✅|=\s*pass|:\s*pass)/i.test(body)) return true
107+
if (/阶段一[^\n]{0,30}PR-1[^\n]{0,30}✅/i.test(body)) return true
108+
if (/PR-1\s*[|::]\s*✅/i.test(body)) return true
109+
// Explicit fail
110+
if (/PR-1[^\n]{0,40}(?:不通过|阻断|fail|❌)/i.test(body)) return false
77111
}
78112
const sessions = path.join(taskRoot, '.memory', 'sessions.md')
79113
if (fs.existsSync(sessions)) {
@@ -86,15 +120,15 @@ function pr1EvidenceOk(taskRoot) {
86120
return false
87121
}
88122

89-
function hasTechDesign(taskRoot) {
123+
function hasTechDesign (taskRoot) {
90124
if (!taskRoot) return false
91125
return fs.existsSync(path.join(taskRoot, '02-技术方案.md'))
92126
}
93127

94128
/**
95129
* @returns {{ decision: 'allow'|'block'|'unverified', gaps: string[], reason: string, honesty: object }}
96130
*/
97-
function evaluateStopCompletionGate(input = {}) {
131+
function evaluateStopCompletionGate (input = {}) {
98132
const {
99133
mode = '',
100134
workflow = '',
@@ -116,7 +150,8 @@ function evaluateStopCompletionGate(input = {}) {
116150
}
117151

118152
const wf = String(workflow || '').toLowerCase()
119-
if ((wf === 'chat' || mode === 'chat') && !mutated) {
153+
const modeL = String(mode || '').toLowerCase()
154+
if ((wf === 'chat' || modeL === 'chat') && !mutated) {
120155
return { decision: 'allow', gaps: [], reason: '', honesty }
121156
}
122157

@@ -143,35 +178,50 @@ function evaluateStopCompletionGate(input = {}) {
143178
}
144179

145180
const gaps = []
146-
const nonChatWork = ['dev', 'fix', 'self-fix'].includes(String(mode).toLowerCase())
181+
const nonChatWork = ['dev', 'fix', 'self-fix'].includes(modeL)
147182
|| ['dev', 'fix', 'self-fix'].includes(wf)
148183
|| mutated
149184
|| reportTouched
150185

151186
if (nonChatWork) {
152-
if (!hasEntryCheck(text)) gaps.push('entry-check')
187+
// F-07: R11 canonical names
188+
if (!hasEntryCheck(text)) gaps.push('entry-check-missing')
189+
190+
// F-16: completion-check block required for non-chat work that claims complete or mutates under dev/fix
191+
const needCompletionBlock = completionClaimed(text)
192+
|| reportTouched
193+
|| ['dev', 'fix', 'self-fix'].includes(modeL)
194+
|| ['dev', 'fix', 'self-fix'].includes(wf)
195+
|| mutated
196+
if (needCompletionBlock && !hasCompletionCheck(text)) {
197+
gaps.push('completion-check-missing')
198+
}
199+
153200
const summary = analyzeFinalValidationSummarySample(text)
154201
const classif = summary.classification || summary.status || 'not-claimed'
155202
if (
156203
(completionClaimed(text) || reportTouched || mutated)
157204
&& (classif === 'not-claimed' || classif === 'thin-green-summary' || classif === 'report-link-only' || summary.status === 'verified-missing')
158205
) {
159206
if (classif === 'not-claimed' && !completionClaimed(text) && !reportTouched && mutated) {
160-
// mutated but no completion claim: still require FVS only if completion-ish; Q1 focuses entry-check
207+
// mutated without completion claim: entry + completion-check cover Q1; FVS optional until claim
161208
} else if (completionClaimed(text) || reportTouched) {
162209
gaps.push('final-validation-summary')
163-
} else if (mutated && summary.status === 'verified-missing') {
210+
} else if (mutated && summary.status === 'verified-missing' && hasCompletionCheck(text)) {
164211
gaps.push('final-validation-summary')
165212
}
166213
}
167-
// Stronger: if completion claimed and thin/not claimed
168214
if (completionClaimed(text) && (classif === 'not-claimed' || classif === 'thin-green-summary' || summary.status === 'verified-missing')) {
169215
if (!gaps.includes('final-validation-summary')) gaps.push('final-validation-summary')
170216
}
171-
if (mutated && !reportTouched && wf !== 'chat') gaps.push('report')
172-
if (mutated && !memoryTouched && wf !== 'chat') gaps.push('memory')
217+
218+
// F-08: report/memory with explicit N/A / SimpleTask / probe exemption
219+
const exempt = artifactGapsExempt(text)
220+
if (mutated && !reportTouched && wf !== 'chat' && !exempt) gaps.push('report-missing')
221+
if (mutated && !memoryTouched && wf !== 'chat' && !exempt) gaps.push('memory-missing')
173222
}
174223

224+
// F-05 / R9: CP2 confirm request without PR-1 strong evidence
175225
const taskRoot = input.taskRoot || findActiveTaskRoot(input.state)
176226
if (askingCp2Confirm(text) && hasTechDesign(taskRoot) && !pr1EvidenceOk(taskRoot)) {
177227
gaps.push('pr1-skipped')
@@ -198,5 +248,7 @@ module.exports = {
198248
pr1EvidenceOk,
199249
completionClaimed,
200250
hasEntryCheck,
251+
hasCompletionCheck,
252+
artifactGapsExempt,
201253
findActiveTaskRoot
202254
}

‎hooks/_runtime/lifecycle-visible-reply.cjs‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,15 @@ function buildLifecycleVisibleReplyUtils(ctx) {
311311
entryCheckCompleteness: entryCompleteness.status,
312312
textBytes: Buffer.byteLength(String(text || ''), 'utf8')
313313
}
314-
if (/🛡️ DEV 模式 \| 合规检查|FC:\s*FC1|DevCodexVisibleEnvelopeV1\s*·\s*completion-check/.test(text)) state.visible.compliance = true
314+
// F-14: Skill-standard heading ### DevCodex · 完成检查 must count as compliance block.
315+
if (
316+
/###\s*DevCodex\s*·\s*完成检查/i.test(text) ||
317+
/🛡️\s*DEV\s*模式\s*\|\s*合规检查/i.test(text) ||
318+
/FC:\s*FC1/i.test(text) ||
319+
/DevCodexVisibleEnvelopeV1\s*·\s*completion-check/i.test(text)
320+
) {
321+
state.visible.compliance = true
322+
}
315323
try {
316324
const { analyzeFinalValidationSummarySample } = require('./visible-output-contract.cjs')
317325
const summaryEvidence = analyzeFinalValidationSummarySample(text)
@@ -444,8 +452,15 @@ function buildLifecycleVisibleReplyUtils(ctx) {
444452
if (eventName === 'Stop' && state.visible?.s07OrderStatus === 'late') {
445453
items.push('S07 order: product mutation before entry-check evidence(VL-004:文首补 PC 不算先输出;reports/.memory/台账写入须在首次可见 PC0~PC7 之后)')
446454
}
447-
if (eventName === 'Stop' && state.mode === 'dev' && state.reportTouched && state.visible && !state.visible.compliance) {
448-
items.push('合规检查状态块未输出(17-compliance:dev 模式非 chat 回复末尾必须含 🛡️ DEV 模式 | 合规检查 状态块)')
455+
// F-14/F-16: dev + (report or mutation) must show completion-check; standard heading accepted
456+
if (
457+
eventName === 'Stop' &&
458+
state.mode === 'dev' &&
459+
(state.reportTouched || state.mutated) &&
460+
state.visible &&
461+
!state.visible.compliance
462+
) {
463+
items.push('完成检查未输出或未识别(须含 ### DevCodex · 完成检查 或 DevCodexVisibleEnvelopeV1 · completion-check;F-14)')
449464
}
450465
if (eventName === 'Stop' && state.mode === 'dev' && state.reportTouched && state.visible?.compliance) {
451466
const summaryStatus = state.visible.finalValidationSummaryStatus || 'verified-missing'

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"prepublishOnly": "npm run test:all:with-audit",
114114
"test:executable-absorption-gates": "node scripts/test-executable-absorption-gates.js",
115115
"test:skill-resolve": "node scripts/test-skill-resolve.js && node scripts/test-skill-resolution-hook-paths.js && node scripts/test-workspace-skill-auto-match.js",
116+
"test:stop-gate": "node scripts/test-lifecycle-stop-gate.js && node scripts/test-r10-control-plane-honesty.js && node scripts/replay-stop-gate-hook-matrix.js",
116117
"test:skill-automatch-accept": "node scripts/replay-skill-automatch-accept.cjs"
117118
},
118119
"keywords": [
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict'
2+
/** Real-cwd acceptance: mode=dev + lastAssistantMessage completion-check recognition (F-03/F-14). */
3+
const { spawnSync } = require('child_process')
4+
const fs = require('fs')
5+
const path = require('path')
6+
7+
const lifecycle = path.join(__dirname, '../hooks/_runtime/lifecycle.cjs')
8+
const cwd = path.resolve(__dirname, '..')
9+
const statePath = path.join(
10+
path.dirname(cwd),
11+
'.devcodex',
12+
'devcodex-v1',
13+
'.memory',
14+
'hooks',
15+
'devcodex-v1',
16+
'lifecycle-state.json'
17+
)
18+
const env = {
19+
...process.env,
20+
DEVCODEX_HOST_PLATFORM: 'grok',
21+
GROK_BUILD: '1',
22+
GROK_HOME: path.join(process.env.USERPROFILE || '', '.grok')
23+
}
24+
25+
function run (payload) {
26+
const r = spawnSync(process.execPath, [lifecycle], {
27+
cwd,
28+
env,
29+
input: JSON.stringify(payload),
30+
encoding: 'utf8',
31+
timeout: 60000
32+
})
33+
try {
34+
return JSON.parse(String(r.stdout || '').trim() || '{}')
35+
} catch {
36+
return { _raw: r.stdout, _err: r.stderr }
37+
}
38+
}
39+
40+
run({
41+
hookEventName: 'UserPromptSubmit',
42+
prompt: 'mode dev completion acceptance',
43+
cwd: cwd.replace(/\\/g, '/')
44+
})
45+
const tmp = path.join(cwd, '_mode_probe_tmp.txt')
46+
fs.writeFileSync(tmp, 'x\n')
47+
run({
48+
hookEventName: 'PostToolUse',
49+
tool_name: 'create_file',
50+
tool_input: { path: tmp, content: 'x' },
51+
cwd: cwd.replace(/\\/g, '/')
52+
})
53+
54+
const body = [
55+
'### DevCodex · 入口检查',
56+
'- PC0 [PASS]',
57+
'- PC1 [PASS]',
58+
'- PC2 [PASS]',
59+
'- PC3 [PASS]',
60+
'- PC4 [N/A] skipReason=probe',
61+
'- PC5 [PASS]',
62+
'- PC6 [PASS]',
63+
'- PC7 [PASS]',
64+
'',
65+
'### DevCodex · 完成检查',
66+
'| 类型 | 命令 | exitCode | runId/计数 |',
67+
'| 权威 | `npm run test:visible-output` | exitCode 0 | runId=validation-202607230001 / checks=42 |',
68+
'WorkspaceSyncStatus: skipped (无需同步)',
69+
'dirty boundary: git status clean; no unrelated dirty',
70+
'Release actions: push/tag/release/publish 未执行',
71+
'`DevCodexVisibleEnvelopeV1 · completion-check · PASS · ' + 'c'.repeat(64) + '`',
72+
'报告: N/A',
73+
'记忆: N/A',
74+
'skipReason: probe'
75+
].join('\n')
76+
77+
const stop = run({
78+
hookEventName: 'Stop',
79+
lastAssistantMessage: body,
80+
stopHookActive: false,
81+
cwd: cwd.replace(/\\/g, '/')
82+
})
83+
84+
const st = JSON.parse(fs.readFileSync(statePath, 'utf8'))
85+
const summary = {
86+
stopDecision: stop.decision || null,
87+
mode: st.mode,
88+
replyEvidence: st.visible?.replyEvidence,
89+
replySource: st.visible?.replySource,
90+
compliance: st.visible?.compliance,
91+
precheckStatus: st.visible?.precheckStatus,
92+
fvs: st.visible?.finalValidationSummaryStatus,
93+
gaps: st.enforcementHonesty?.processGaps || []
94+
}
95+
console.log(JSON.stringify(summary, null, 2))
96+
97+
const ok =
98+
summary.mode === 'dev' &&
99+
summary.replyEvidence === 'verified-present' &&
100+
summary.replySource === 'lastAssistantMessage' &&
101+
summary.compliance === true &&
102+
summary.stopDecision !== 'block'
103+
104+
if (!ok) {
105+
console.error('ACCEPTANCE_FAIL')
106+
process.exitCode = 1
107+
} else {
108+
console.log('ACCEPTANCE_PASS mode=dev lastAssistantMessage completion-check recognized')
109+
}
110+
111+
try { fs.unlinkSync(tmp) } catch { /* ignore */ }

0 commit comments

Comments
 (0)