Skip to content

Commit 41ab30d

Browse files
committed
feat(skills): close workspace skill user loop with C1.1 and doctor
Tighten AutoMatch so short ids like test do not fire on long task sentences; keep exact, short-alone, and explicit skill invokes. Surface workspace skill counts in doctor, document the user closed loop in README, add PI-179 six-ring notes to requirement prompts, and ship deploy accept replay under test:skill-automatch-accept.
1 parent cb481e8 commit 41ab30d

9 files changed

Lines changed: 264 additions & 16 deletions

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,18 @@ GlobalOnlyHostConfigModeV1 与 GlobalOnlyWorkspaceCleanModeV1 将宿主配置、
208208
└── skills/ ← 可选自定义 Skill(W;非 reserved 时 W>G,见 skill-resolution)
209209
```
210210

211-
自定义 Skill 只写 `<workspace>/.devcodex/workspace/skills/<id>/SKILL.md`(一次配置,经 DevCodex resolve 五宿主共用);**不要**`~/.agents/skills` 或五宿主 home 树作扩展。强制核 skill(如 `compliance`/`cp-gate`)禁止 W 覆盖。诊断:`devcodex skill resolve <id> --json`;探针:`npm run test:skill-resolve`
211+
自定义 Skill 只写 `<workspace>/.devcodex/workspace/skills/<id>/SKILL.md`(一次配置,经 DevCodex 五宿主共用);**不要**`~/.agents/skills` 或五宿主 home 树作扩展。强制核 skill(如 `compliance`/`cp-gate`)禁止 W 覆盖。
212+
213+
**工作区 Skill 用户可见闭环(W)**
214+
215+
| 步骤 | 做什么 |
216+
|------|--------|
217+
| 放置 | `.devcodex/workspace/skills/<id>/SKILL.md`(YAML `name` / `description` 写清触发语;可用「引号」触发词) |
218+
| 解析 | `devcodex skill resolve <id>``selectedLayer=workspace`(W>G) |
219+
| 匹配预览 | `devcodex skill match "<用户原话>"`(短触发语命中;长开发句中的短 id 如 `test` 默认不误触发) |
220+
| 对话生效 | inject 宿主:UserPromptSubmit 注入正文;**Grok** 无 UPS inject,靠 Stop skill-force 回灌(勿宣称 Grok===Codex inject) |
221+
| 部署 | 改 lifecycle 后 `devcodex global-adapters apply`(或 `npm install -g .`),**新会话**再验 |
222+
| 诊断 | `devcodex doctor` 显示 workspace skills 数量;探针 `npm run test:skill-resolve` / `npm run test:skill-automatch-accept` |
212223

213224
bare `devcodex init/update` 只管理 `.devcodex`。若当前路径没有可复用的祖先 workspace marker,且不存在旧版项目运行态,首次执行会原子创建 `.devcodex/layout.json` 并采用 `workspace-namespace`;若祖先已存在有效 marker,则沿用祖先 owner;本地 marker 非法时返回 `WORKSPACE_LAYOUT_INVALID`,旧 `.devcodex/profile``.memory` 等运行态存在时返回 `WORKSPACE_LAYOUT_MIGRATION_REQUIRED`,不会静默切换目录,需先执行 `devcodex migrate-layout plan/apply``devcodex init --claude``devcodex init --codex``--host``--gemini``--grok`、裸 `devcodex uninstall` 以及 `uninstall --host grok` 首批均返回结构化错误 `CLI_HOST_CONFIG_GLOBAL_ONLY`。安装与更新错误会指向 npm 全局命令;卸载错误会明确首批不支持自动删除用户级配置。现有工作区宿主文件不会被自动迁移、覆盖或删除;删除前仍需用户明确确认。
214225

changelogs/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- **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。
99
- **工作区 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。
1010
- **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 即生效」。
11+
- **WorkspaceSkill 闭环产品化收口(CP1→C1.1→C2/C3)****C1.1** 短/歧义 skill id(如 `test`)在长任务句中不误触发,仅 exact/短句 alone/显式「用 x skill」命中;**doctor** 输出 `workspaceSkills` 数量与 ids;**README** 用户闭环表;Profile `06` 登记 `workspace-skill-auto-match`;PI-179 六环写入 `requirement-overview` / `product-requirement` 模板;部署回放 `npm run test:skill-automatch-accept`。需求:`双层Skill用户可见闭环完整产品`
1112
- **SourceExistenceVerificationGate(规范吸纳防复发)**`spec-absorption` 强制在输出「可吸纳」前对 source-root 做同名/近邻存在性验证;台账 `open/pending absorption` 不得单独成 absorb。`AbsorptionCandidateMatrixV1` 增加 `sourceExistence`;planner 阻断 `unverified``present/equivalent-covered` 的 absorb;并列「可关账清单」。负向:ledger-status-only。验证:`node scripts/plan-absorption-candidates.js --self-test``npm run test:residual-absorption-controls`、V81 needles。
1213
- **ExecutableAbsorption + ProbeNecessity(假吸纳禁止)**:吸纳必须可执行——`enforcementLevel` 不得为 checklist-only/none;每条 absorb 必填 `probeNecessity`(required/conditional/not-required/forbidden、alwaysOnImpact、复杂度与误报)。只写 Skill 正文或「以后再补探针」不得 ready/absorbed。分类器 `classifyExecutableAbsorptionSample`。验证:planner self-test + residual + V81。
1314
- **可执行吸纳主包 A 实施(R1~R4 + T2)**`ProgressReportFastPath`(skill-gap/compliance/SC16);`TaskPhaseProjectionGate`(cp-gate/user-visible-output);`ClosedArtifactNoReviveGate`(cp-gate);`FormalRerunLightClassify` + `FindingObjectLayer`(consumer-validation)。探针:`scripts/lib/executable-absorption-gates.js` + `npm run test:executable-absorption-gates`(并入 host-parity / control-plane)。

hooks/_runtime/workspace-skill-auto-match.cjs

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,38 @@ function listWorkspaceSkillCandidates(options = {}) {
191191
return out
192192
}
193193

194+
/**
195+
* Short / ambiguous skill ids (e.g. test) must not fire from long task sentences (C1.1).
196+
* Allow: exact, short alone prompt, quoted trigger alone, or explicit "use skill X".
197+
*/
198+
function isAmbiguousShortToken(token) {
199+
const t = String(token || '').trim()
200+
if (!t) return false
201+
if (t.length <= 4) return true
202+
return /^(tests?|demo|tmp|temp|ok|hi|ping|foo|bar|baz|sample)$/i.test(t)
203+
}
204+
205+
function isShortAlonePrompt(cleaned, id, name) {
206+
const text = String(cleaned || '').trim()
207+
if (!text) return false
208+
const tokens = text.split(/\s+/).filter(Boolean)
209+
const maxLen = Math.max(String(name || '').length, String(id || '').length, 4)
210+
// 1–2 tokens and not a long sentence; e.g. "test", "测试", "@rocky test" → after normalize "test"
211+
if (tokens.length <= 2 && text.length <= maxLen + 16) return true
212+
// single CJK trigger phrase
213+
if (tokens.length === 1 && /[\u4e00-\u9fff]/.test(text) && text.length <= 8) return true
214+
return false
215+
}
216+
217+
function hasExplicitSkillInvoke(raw, id, name) {
218+
const text = String(raw || '')
219+
// "用 test skill" / "用 skill test" / "workspace skill test" / "加载 skill X"
220+
const invokeShape = /(?:||)\s+(?:workspace\s+)?skill\b|(?:||)\s+\S+\s+skill\b|workspace\s*skill|skill\s*[:=]/i.test(text)
221+
if (!invokeShape) return false
222+
const cleaned = normalizePrompt(text)
223+
return wordBoundaryHit(cleaned, id) || wordBoundaryHit(cleaned, name)
224+
}
225+
194226
/**
195227
* Score a single candidate against user prompt.
196228
* @returns {{ score: number, reasons: string[] }}
@@ -204,37 +236,59 @@ function scoreCandidate(prompt, candidate) {
204236

205237
const id = candidate.skillId
206238
const name = candidate.name || id
207-
const alone = cleaned.length <= Math.max(name.length, id.length) + 12
239+
const alone = isShortAlonePrompt(cleaned, id, name)
240+
const idAmbiguous = isAmbiguousShortToken(id)
241+
const nameAmbiguous = isAmbiguousShortToken(name)
208242

209243
if (cleaned.toLowerCase() === id.toLowerCase() || cleaned === name) {
210-
score += 100
244+
score = Math.max(score, 100)
211245
reasons.push('exact-id-or-name')
212246
} else if (wordBoundaryHit(cleaned, id)) {
213-
score += alone ? 90 : 70
214-
reasons.push('id-token')
247+
// C1.1: short id in long sentences does not auto-match
248+
if (alone || !idAmbiguous) {
249+
score = Math.max(score, alone ? 90 : (idAmbiguous ? 0 : 55))
250+
if (score >= MIN_MATCH_SCORE) reasons.push(alone ? 'id-token-alone' : 'id-token')
251+
else if (idAmbiguous && !alone) reasons.push('id-token-suppressed-long-prompt')
252+
} else {
253+
reasons.push('id-token-suppressed-long-prompt')
254+
}
215255
} else if (wordBoundaryHit(cleaned, name) && name.toLowerCase() !== id.toLowerCase()) {
216-
score += alone ? 85 : 65
217-
reasons.push('name-token')
256+
if (alone || !nameAmbiguous) {
257+
score = Math.max(score, alone ? 85 : (nameAmbiguous ? 0 : 50))
258+
if (score >= MIN_MATCH_SCORE) reasons.push(alone ? 'name-token-alone' : 'name-token')
259+
else if (nameAmbiguous && !alone) reasons.push('name-token-suppressed-long-prompt')
260+
} else {
261+
reasons.push('name-token-suppressed-long-prompt')
262+
}
218263
}
219264

220265
for (const t of candidate.triggers || []) {
266+
const tAmbiguous = isAmbiguousShortToken(t) || t.length <= 4
221267
if (cleaned === t || cleaned.toLowerCase() === t.toLowerCase()) {
222268
score = Math.max(score, 95)
223269
reasons.push(`trigger-exact:${t}`)
224270
} else if (wordBoundaryHit(cleaned, t)) {
225-
score = Math.max(score, alone ? 88 : 60)
226-
reasons.push(`trigger-token:${t}`)
271+
if (alone || !tAmbiguous) {
272+
const next = alone ? 88 : (tAmbiguous ? 0 : 50)
273+
if (next >= MIN_MATCH_SCORE) {
274+
score = Math.max(score, next)
275+
reasons.push(alone ? `trigger-alone:${t}` : `trigger-token:${t}`)
276+
} else if (tAmbiguous && !alone) {
277+
reasons.push(`trigger-suppressed-long-prompt:${t}`)
278+
}
279+
} else {
280+
reasons.push(`trigger-suppressed-long-prompt:${t}`)
281+
}
227282
}
228283
}
229284

230-
// Explicit invoke
231-
if (/\s*(workspace\s*)?skill|workspace\s*skill|\s*skill|\s*skill/i.test(raw) &&
232-
(wordBoundaryHit(cleaned, id) || wordBoundaryHit(cleaned, name))) {
285+
// Explicit invoke always allowed when skill id/name present
286+
if (hasExplicitSkillInvoke(raw, id, name)) {
233287
score = Math.max(score, 92)
234288
reasons.push('explicit-invoke')
235289
}
236290

237-
// Description keyword soft match only when prompt is short (avoid false positives)
291+
// Description keyword soft match only when prompt is short alone (avoid false positives)
238292
if (score < MIN_MATCH_SCORE && alone && cleaned.length >= 2 && cleaned.length <= 24) {
239293
const desc = String(candidate.description || '').toLowerCase()
240294
if (desc && desc.includes(cleaned.toLowerCase()) && cleaned.length >= 2) {
@@ -436,5 +490,8 @@ module.exports = {
436490
toStateRecord,
437491
looksLikeConnectivityPing,
438492
normalizePrompt,
439-
extractLastAssistantMessage
493+
extractLastAssistantMessage,
494+
isAmbiguousShortToken,
495+
isShortAlonePrompt,
496+
hasExplicitSkillInvoke
440497
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@
112112
"validate": "node scripts/validate.js",
113113
"prepublishOnly": "npm run test:all:with-audit",
114114
"test:executable-absorption-gates": "node scripts/test-executable-absorption-gates.js",
115-
"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"
115+
"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:skill-automatch-accept": "node scripts/replay-skill-automatch-accept.cjs"
116117
},
117118
"keywords": [
118119
"copilot",
@@ -222,6 +223,7 @@
222223
"scripts/test-skill-resolve.js",
223224
"scripts/test-skill-resolution-hook-paths.js",
224225
"scripts/test-workspace-skill-auto-match.js",
226+
"scripts/replay-skill-automatch-accept.cjs",
225227
"hooks/_runtime/workspace-skill-auto-match.cjs",
226228
"skills/_schemas/devcodex-skill-contract.v1.schema.json",
227229
"scripts/lib/project-knowledge-store.js",

prompts/product-requirement.prompt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ applyTo: .devcodex/**/requirements/**
1414

1515
> ⚠️ 本模板不是“产品补全 AI 草稿”的模板。它用于公司已有产品角色时,由产品直接提交完整产品需求,AI / 研发不再生成或重写产品需求。
1616
> ⚠️ 无产品角色、研发兼产品或需求方只提供口头诉求时,使用 `00-需求概况.md` → AI 生成 `01-需求确认.md` 草稿 → 产品 / 需求方确认的链路;不要把两条链路混在一起。
17+
> ⚠️ **PI-179 用户可见闭环(强制)**`01` 必须含用户可见六环(触发→加载→生效→失败态→验收);未交付环显式写非目标/分期;禁止底层合同绿冒充体验完成。
1718
> ⚠️ 产品完整需求是业务事实源:产品不填写验收标准、测试用例、数据库字段、接口 Schema、表结构、字段类型、内部 ID 或技术实现步骤。
1819
> ⚠️ AI / 研发只做缺口 / 冲突检查和澄清,不生成或重写产品需求,不改写产品口径;缺口检查记录在 CP1 摘要、`02-技术方案.md` 或报告中,不放入产品填写模板正文。
1920
> ⚠️ 技术方案必须从产品原文、流程节点、字段描述、样例、反例和异常边界派生数据库、接口、实现验收和测试策略。

prompts/requirement-overview.prompt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ applyTo: .devcodex/**/requirements/**
2424
> ⚠️ 换句话说:不填写数据库字段、接口 Schema,也不需要把业务诉求翻译成技术字段。
2525
> ⚠️ AI / 产品必须以本文件作为原始输入锚点,生成独立的 `01-需求确认.md`;不得把本文件缺失的信息静默补成产品事实源。
2626
> ⚠️ 若需求方已经提供 Word、PRD、会议纪要、截图、表格、原型、聊天记录或链接,本文件记录来源和关键摘要,附件本身仍是原始事实源的一部分。
27+
> ⚠️ **PI-179 用户可见闭环(强制)**:整理 `01` / 方案 / 验收时,即使用户未点名「自动触发」,也必须写清六环表——触发 → 加载 → 生效 → 失败态 → 怎么验收;未交付环须在分期/非目标**显式**标注,禁止仅 CLI/单测绿冒充体验完成。
2728
2829
---
2930

scripts/lib/cli-maintenance-commands.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,28 @@ function buildCliMaintenanceCommands(ctx) {
695695
executionOptimization,
696696
hostParity
697697
})
698+
// Workspace skill inventory (W layer) for closed-loop diagnostics (PF-213 / AutoMatch)
699+
let workspaceSkills = {
700+
enabled: true,
701+
root: null,
702+
count: 0,
703+
ids: [],
704+
autoMatchModule: false
705+
}
706+
try {
707+
const skillRes = require('../../hooks/_runtime/skill-resolution.cjs')
708+
const autoMatch = require('../../hooks/_runtime/workspace-skill-auto-match.cjs')
709+
workspaceSkills.autoMatchModule = true
710+
workspaceSkills.enabled = skillRes.isWorkspaceSkillsEnabled(env)
711+
workspaceSkills.root = skillRes.resolveWorkspaceSkillsRoot(cwd, { cwd, env })
712+
if (workspaceSkills.root) {
713+
const list = autoMatch.listWorkspaceSkillCandidates({ cwd, env })
714+
workspaceSkills.count = list.length
715+
workspaceSkills.ids = list.map(item => item.skillId).slice(0, 24)
716+
}
717+
} catch {
718+
workspaceSkills.autoMatchModule = false
719+
}
698720
return {
699721
schemaVersion: 'DoctorDiagnosticV1',
700722
cwd,
@@ -711,6 +733,7 @@ function buildCliMaintenanceCommands(ctx) {
711733
hostConfigPolicy,
712734
globalHostConfig,
713735
globalHostRuntime: globalHostConfig,
736+
workspaceSkills,
714737
enforcement: 'safety-only by default; strict blocks only host-supported events',
715738
installArtifacts: {
716739
hasGithubHooks,
@@ -773,7 +796,7 @@ function buildCliMaintenanceCommands(ctx) {
773796
profile: profileState,
774797
executionOptimization,
775798
governanceSummary,
776-
hostParity, globalHostConfig, globalHostComparison, sourceRepository, completion
799+
hostParity, globalHostConfig, globalHostComparison, sourceRepository, completion, workspaceSkills
777800
} = facts
778801
const {
779802
hasGithubHooks, hasClaudeHooks, hasCodexHooksJson, hasCodexHooks,
@@ -825,6 +848,13 @@ function buildCliMaintenanceCommands(ctx) {
825848
} else if (hostParity && sourceRepository) {
826849
console.log(c.dim(' Grok HostParity: source-candidate comparison only; installed HostParity health is not asserted here.'))
827850
}
851+
if (workspaceSkills) {
852+
const ids = (workspaceSkills.ids || []).join(', ') || '(none)'
853+
const mod = workspaceSkills.autoMatchModule ? 'AutoMatch module OK' : 'AutoMatch module missing'
854+
console.log(` workspace skills: ${c.cyan(String(workspaceSkills.count || 0))} ${c.dim(`enabled=${workspaceSkills.enabled !== false}; ${mod}`)}`)
855+
console.log(c.dim(` W root: ${workspaceSkills.root || '(none)'} · ids: ${ids}`))
856+
console.log(c.dim(' diagnose: devcodex skill match "<prompt>" · skill resolve <id> · npm run test:skill-automatch-accept'))
857+
}
828858
console.log()
829859
console.log(c.bold(' User-global host adapters:'))
830860
for (const host of globalHostConfig.hosts) {

0 commit comments

Comments
 (0)