Skip to content

Commit 87c6c14

Browse files
committed
feat(hooks): enforce process gates and artifact paths across five hosts
Add MutationCpGate hard-deny for protected paths, ArtifactPathGate for requirements 02/04 slots, and Stop gaps for missing review checklist or process package (04/05/checklist). Include HostEnforcementMatrixV1, E2E 01-10, package route hooks, requirement assess templates, and docs sync.
1 parent a09392d commit 87c6c14

18 files changed

Lines changed: 891 additions & 25 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ DevCodex 通过 npm 全局安装把 Copilot、Claude Code、Codex、Gemini CLI
4444
- **自动报告**: 每次会话自动写入报告,从不询问 — 直接执行
4545
- **安全底线**: S01~S07 七条不可覆盖的安全规则
4646
- **宿主生命周期护栏**: Copilot CLI、Claude Code、OpenAI Codex、Gemini CLI 与 Grok 在各自已支持的 Hook 事件上提供用户级 runtime adapter;Copilot IDE、JetBrains、Cursor 等未接入等价用户级 Hook 的 surface 仍降级为 instruction-fallback。默认 `safety-only` 仅对危险命令硬拦,流程项提醒放行,`strict` 模式才升级可阻断事件
47+
- **流程强制(MutationCpGate / ArtifactPathGate)**: 无 CP2 时对 `hooks/`、`skills/`、`instructions/`、`website/docs/**` 等受保护路径 **hard deny**(即使默认 safety-only);`requirements/*/02-*` 仅允许技术方案语义、分析盘点报告须落 `reports/analysis/…`;Stop 对 R3 完成宣称缺复审清单追加 `review-checklist-missing`。五宿主策略真相源:`scripts/lib/host-enforcement-matrix.js`(Grok UPS inject = N/A)。验证:`npm run test:process-enforcement-e2e`
4748
- **Codex hook guardrail**: Hook 能力按宿主/事件降级;Codex 的阻断行为取决于当前事件是否支持 `decision`、`continue:false` 或 `permissionDecision`,不能把 adapter 已安装等同为所有事件均可硬拦
4849
- **长任务 Turn Liveness**: `TurnLivenessRecoveryGate` 记录 `running / awaiting-continuation / suspect / stalled-recoverable / terminal` 状态、工具租约、continuation ACK、双阶段 checkpoint 与当前 turn 的 `LocalTaskTraceV1`;Hook 只能在事件到达时判断历史停滞,trace replay 只返回数据,不能自行唤醒宿主、执行 payload、重放写操作或把 `PostToolUse` 当成任务完成
4950
- **全过程完成证据(未发布 Shadow)**: `WorkflowCompletionCandidateV1 → Plan/Receipt/Snapshot → Commit → Projection` 将 CP、执行、验证、复审、同步、报告和记忆绑定到同一 candidate;任务输入由 lifecycle 的受管原子写入口生成,`task verify` 只消费通过 schema、候选绑定和回读校验的输入。report-only、marker-only、仅选择测试路线或仅有复审文档都不能标记完成。五宿主共享同一 reducer,direct/portable/fallback 只改变证据上限,不改变完成语义

‎changelogs/unreleased.md‎

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

8+
- **全宿主流程强制 + 产物路径准确(process-enforcement)**:新增 `scripts/lib/process-enforcement.js`(MutationCpGate 受保护路径 hard-deny、ArtifactPathGate、复审清单/过程包齐套判定)与 `host-enforcement-matrix.js`(五宿主策略真相源);`lifecycle.cjs` PreTool 接线;`lifecycle-stop-gate.cjs` 追加 `review-checklist-missing` / `process-artifact-incomplete` / `progress-artifact-missing`;`npm run test:process-enforcement-e2e` 并入 `test:stop-gate` 与 `test:control-plane`。文档:`02-output-paths` ArtifactPathGate 行、README 流程强制条;配套 PI-REQ-ASSESS 需求模板评估字段。
9+
- **需求模板强制项目实况评估(PI-REQ-ASSESS-20260726)**:`requirement-overview.prompt.md` 新增 §10(AI 填写:CodeTruth 锚点 + 合理性/可实施性/收益/验证状态/影响范围 + 推荐);`requirement.prompt.md` 新增 §2.6 同构字段;与 compliance 五项验证对齐;禁止空表过关。台账 PI-REQ-ASSESS / PI-REQ-NO-SPLIT。需求:`全宿主流程强制与产物路径准确` 概况 v0.4。
810
- **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`。
911
- **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。
1012
- **工作区 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。

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ try {
1919
analyzeFinalValidationSummarySample = () => ({ classification: 'not-claimed', status: 'not-claimed' })
2020
}
2121

22+
let classifyReviewChecklistCompletion
23+
let classifyProcessArtifactCompleteness
24+
try {
25+
;({
26+
classifyReviewChecklistCompletion,
27+
classifyProcessArtifactCompleteness
28+
} = require('../../scripts/lib/process-enforcement.js'))
29+
} catch {
30+
classifyReviewChecklistCompletion = () => ({ ok: true, code: null, gap: null })
31+
classifyProcessArtifactCompleteness = () => ({ ok: true, code: null, gap: null, missing: [] })
32+
}
33+
2234
function extractLastAssistantMessage (payload) {
2335
if (!payload || typeof payload !== 'object') return ''
2436
const direct = payload.lastAssistantMessage || payload.last_assistant_message || payload.assistantMessage
@@ -227,6 +239,34 @@ function evaluateStopCompletionGate (input = {}) {
227239
gaps.push('pr1-skipped')
228240
}
229241

242+
// Process-enforcement: R3/R4 completion claim without review-checklist path/status
243+
const checklist = classifyReviewChecklistCompletion({
244+
completionClaimed: completionClaimed(text) || input.completionClaimed === true,
245+
reviewClass: input.reviewClass || '',
246+
text,
247+
hasReviewChecklistPath: input.hasReviewChecklistPath === true
248+
})
249+
if (!checklist.ok && checklist.gap) {
250+
gaps.push(checklist.gap)
251+
}
252+
253+
// Process package: control-plane / multi-batch must cite or possess 04+05+checklist
254+
const processPkg = classifyProcessArtifactCompleteness({
255+
completionClaimed: completionClaimed(text) || input.completionClaimed === true,
256+
reviewClass: input.reviewClass || '',
257+
text,
258+
controlPlaneTask: input.controlPlaneTask === true,
259+
multiBatch: input.multiBatch === true,
260+
hasImplementationPlan: input.hasImplementationPlan === true,
261+
hasProgressFile: input.hasProgressFile === true,
262+
hasReviewChecklist: input.hasReviewChecklist === true || input.hasReviewChecklistPath === true,
263+
taskRoot,
264+
fs
265+
})
266+
if (!processPkg.ok && processPkg.gap) {
267+
gaps.push(processPkg.gap)
268+
}
269+
230270
const uniqueGaps = [...new Set(gaps)]
231271
honesty.processGaps = uniqueGaps
232272

‎hooks/_runtime/lifecycle.cjs‎

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ const {
6060
evaluateStopCompletionGate,
6161
extractLastAssistantMessage
6262
} = require('./lifecycle-stop-gate.cjs')
63+
const {
64+
shouldHardDenyCpMutation,
65+
classifyPathsForArtifacts,
66+
isStrictProtectedPath,
67+
simpleTaskForbidsPath,
68+
ERROR_CODES: PROCESS_ENFORCEMENT_CODES
69+
} = require('../../scripts/lib/process-enforcement.js')
6370

6471
const CONTEXT_ROOT = process.cwd()
6572
const PAYLOAD_PREVIEW_LIMIT = 160
@@ -72,9 +79,8 @@ const CP2_FILE = '02-技术方案.md'
7279
const CP3_FILE = '04-实施计划.md'
7380
const CP3_RUNTIME_FILE_THRESHOLD = 5
7481
// Dual-Track Closure (PI-154 / PF-171): control-plane source paths that require a bound task+CP when mutated.
75-
// Matches package source under repo root (scripts/hooks/skills, host-projections, package.json, host-parity docs).
76-
// R10 (20260726-grok-stop-enforcement-honesty): extended host-projections + website host-parity intro.
77-
const CONTROL_PLANE_SOURCE_RE = /(?:^|[/\\])(?:scripts|hooks|instructions|host-projections)(?:[/\\]|$)|(?:^|[/\\])package\.json$|(?:^|[/\\])skills[/\\]|(?:^|[/\\])website[/\\]docs[/\\]intro[/\\]host-parity/i
82+
// PF-process-enforcement: full website/docs + skills/mcp/prompts (aligned with process-enforcement STRICT_PROTECTED).
83+
const CONTROL_PLANE_SOURCE_RE = /(?:^|[/\\])(?:scripts|hooks|instructions|host-projections|mcp|prompts|agents)(?:[/\\]|$)|(?:^|[/\\])package\.json$|(?:^|[/\\])plugin\.json$|(?:^|[/\\])skills[/\\]|(?:^|[/\\])website[/\\]docs(?:[/\\]|$)/i
7884
const EXECUTION_MODE = { CONFIRM: 'confirm', AUTO: 'auto' }
7985
const ENFORCEMENT_MODE = (() => {
8086
const mode = String(process.env.DEVCODEX_HOOK_ENFORCEMENT || 'safety-only').trim().toLowerCase()
@@ -933,7 +939,7 @@ function checkCpGate(payload, state) {
933939
}
934940

935941
// Source file extensions that indicate code/config being written
936-
const SOURCE_EXT_RE = /\.(js|ts|tsx|jsx|mjs|cjs|py|go|rs|java|cs|rb|php|c|cpp|h|swift|kt|vue|svelte|css|scss|less|html|sql|sh|bash|zsh|ps1|psm1|json|yaml|yml|toml|ini|xml|env)$/i
942+
const SOURCE_EXT_RE = /\.(js|ts|tsx|jsx|mjs|cjs|py|go|rs|java|cs|rb|php|c|cpp|h|swift|kt|vue|svelte|css|scss|less|html|sql|sh|bash|zsh|ps1|psm1|json|yaml|yml|toml|ini|xml|env|md|mdx)$/i
937943
// F-001/F-037: only governance deployment paths and the active .devcodex namespace are exempt.
938944
// This prevents workspace-namespace projects from treating project/.devcodex/.tmp as managed state.
939945
const DEVCODEX_DEPLOYMENT_PATH_RE = /^(?:\.claude|\.github)\/(?:instructions|skills|hooks|agents|prompts|settings\.json|settings\.local\.json|data)(?:\/|$)|^AGENTS\.md$|^\.agents\/skills(?:\/|$)|^\.codex\/(?:hooks\.json|hooks)(?:\/|$)|^codex\/(?:hooks\.json|hooks)(?:\/|$)/
@@ -1458,13 +1464,54 @@ async function main() {
14581464
return
14591465
}
14601466

1467+
// 2.5 ArtifactPathGate — requirements/02|04 slot semantics (always hard when invalid)
1468+
if (isSourceCodeMutation(payload, platform, state) || isProductArtifactMutation(payload, platform)) {
1469+
const toolPaths = extractToolPaths(payload)
1470+
const art = classifyPathsForArtifacts(toolPaths)
1471+
if (!art.ok) {
1472+
state.lastReason = art.code || 'ARTIFACT_PATH_INVALID'
1473+
saveState(state)
1474+
writeStdout(buildInterceptionOutput(
1475+
state, platform, eventName, INTERCEPTION_ACTION.REQUIRE_COMPLETION, art.code || 'ARTIFACT_PATH_INVALID',
1476+
`Artifact path denied: ${art.code}`,
1477+
art.message || 'Illegal requirements artifact path.',
1478+
'Place analysis reports under reports/analysis/…; reserve 02- for technical design.'
1479+
))
1480+
return
1481+
}
1482+
}
1483+
1484+
// 2.6 SimpleTask path forbid — website/docs + control-plane protected paths (D2)
1485+
if (
1486+
(state.simpleTaskFastPath === true || state.taskPathMode === 'simple') &&
1487+
isSourceCodeMutation(payload, platform, state)
1488+
) {
1489+
const toolPaths = extractToolPaths(payload)
1490+
const forbidden = toolPaths.find(p => simpleTaskForbidsPath(p))
1491+
if (forbidden) {
1492+
state.lastReason = PROCESS_ENFORCEMENT_CODES.SIMPLE_TASK_PATH_FORBIDDEN
1493+
saveState(state)
1494+
writeStdout(buildInterceptionOutput(
1495+
state, platform, eventName, INTERCEPTION_ACTION.REQUIRE_COMPLETION,
1496+
PROCESS_ENFORCEMENT_CODES.SIMPLE_TASK_PATH_FORBIDDEN,
1497+
'SimpleTask path forbidden',
1498+
`SimpleTaskFastPath may not mutate protected path: ${forbidden}`,
1499+
'Upgrade to full CP task, or keep changes outside website/docs and control-plane sources.'
1500+
))
1501+
return
1502+
}
1503+
}
1504+
14611505
// 3. CP gate — block source code mutations until checkpoints confirmed
1462-
// payload-aware: when tool paths target specific requirement dirs, only that requirement's CP is checked
1463-
// R10: reuse checkCpGate; safety-only allows write but Honesty discloses cp2-unconfirmed-write
1506+
// PF-process-enforcement: hard-deny for strict-protected paths even under safety-only (D1)
1507+
// Non-protected paths: legacy safety-only warning + Honesty cp2-unconfirmed-write
14641508
const gate = checkCpGate(payload, state)
14651509
if (gate && isSourceCodeMutation(payload, platform, state)) {
1466-
state.lastReason = `cp-gate-${gate.phase}`
1467-
if (!isStrictEnforcement() && (gate.phase === 'CP2' || gate.code === 'cp-gate-orphan-control-plane')) {
1510+
const toolPaths = extractToolPaths(payload)
1511+
const hard = shouldHardDenyCpMutation(gate, toolPaths, { strictEnv: isStrictEnforcement() })
1512+
const useHardDeny = hard.hardDeny === true
1513+
state.lastReason = `cp-gate-${gate.phase}${useHardDeny ? '-hard' : '-warn'}`
1514+
if (!useHardDeny && (gate.phase === 'CP2' || gate.code === 'cp-gate-orphan-control-plane')) {
14681515
const honesty = state.enforcementHonesty && typeof state.enforcementHonesty === 'object'
14691516
? { ...state.enforcementHonesty }
14701517
: {}
@@ -1479,8 +1526,21 @@ async function main() {
14791526
}
14801527
state.enforcementHonesty = honesty
14811528
}
1529+
if (useHardDeny) {
1530+
const honesty = state.enforcementHonesty && typeof state.enforcementHonesty === 'object'
1531+
? { ...state.enforcementHonesty }
1532+
: {}
1533+
honesty.thisTurn = {
1534+
...(honesty.thisTurn || {}),
1535+
preToolHardDeny: true,
1536+
preToolSafetyOnlyAllow: false,
1537+
cpGatePhase: gate.phase,
1538+
processEnforcementReason: hard.reason
1539+
}
1540+
state.enforcementHonesty = honesty
1541+
}
14821542
saveState(state)
1483-
writeStdout(isStrictEnforcement()
1543+
writeStdout(useHardDeny
14841544
? buildCpDenyOutput(state, platform, eventName, gate, getToolName(payload) || 'tool')
14851545
: buildCpWarningOutput(state, platform, eventName, gate, getToolName(payload) || 'tool'))
14861546
return

‎instructions/02-output-paths.instructions.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ version: 1.15.3
101101
| **ExistingRequirementArtifactOverride** | 用户调整/修改/补充既有需求或问题,且已有 `00-需求概况.md`、`00-需求变更概况.md`、`01-需求确认.md`、`01-产品需求.md`、`01-需求变更确认.md`、历史 `01-需求概述.md`、`00-问题概况.md`、`01-问题确认.md`、bug CP 产物、Profile 声明的正式需求文件或 website requirement 时,必须更新已有真相源;SimpleTaskFastPath 只豁免新建完整产物,不能把回复内联摘要当成文件回写 |
102102
| **ArtifactDecisionMatrix** | CP1/CP2/CP3/ECR 需要按任务规模列出关键产物的 `create` / `update` / `skip` / `N/A` 状态、原因、触发条件和升级回退;判定优先级为“已有真相源回写 > 任务触发条件 > SimpleTaskFastPath > 子类型豁免”,覆盖入口分类、00/01(含 `01-需求确认.md`、`01-产品需求.md`、`01-需求变更确认.md`、`01-问题确认.md`)/02/04/05/06、目标文档、报告和记忆 |
103103
| **禁止写入源码目录** | 脚本/测试/辅助文件严禁放入项目源码目录 |
104+
| **ArtifactPathGate(槽位语义)** | `requirements/<任务>/02-*` **仅**技术方案语义(如 `02-技术方案.md`);`04-*` **仅**实施计划语义。功能清单/盘点/遗漏扫/inventory 等分析报告**禁止**占用 02/04 槽位,须写入 `reports/analysis|audit|…/<agent>/YYYYMMDD/`。Hook 对非法槽位 hard deny(错误码 `ARTIFACT_PATH_INVALID`);见 `scripts/lib/process-enforcement.js` |
104105
| **强制产物首轮完成** | 默认 00/01/04 在首轮会话结束前按 ArtifactDecisionMatrix 处理:需要则创建/更新,命中 SimpleTaskFastPath 或子类型豁免则记录 `N/A + skipReason`;PC0~PC7、Profile、报告、记忆、安全底线和必要验证不可省略;02-技术方案.md、实施方案/ 与 `06-关键决策.md` 按条件触发;services/ 在 CP2 后按需创建;强触发条件命中时 `05-实施进度.md` 必须在执行前初始化 |
105106
| **需求归档(v1.9.3+)** | 已完成且不再活跃的需求目录下创建空文件 `.archived`;CP gate 扫描跳过含此标记的需求,避免历史需求全局阻断 dev 工作流 |
106107

0 commit comments

Comments
 (0)