@xulthekl/team-flow 0.65.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +1 -1
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/CHANGELOG.md +31 -0
  9. package/GEMINI.md +1 -1
  10. package/INSTALL.md +1 -1
  11. package/README.md +1 -1
  12. package/agents/architecture-reviewer.md +4 -0
  13. package/agents/prd-completeness-reviewer.md +10 -0
  14. package/agents/prd-writer.md +1 -0
  15. package/agents/release-archivist.md +2 -0
  16. package/docs/README_en.md +1 -1
  17. package/docs/team-flow /344/275/277/347/224/250/350/257/264/346/230/216/357/274/210/347/240/224/345/217/221/345/233/242/351/230/237/347/211/210/357/274/211.md" +8 -6
  18. package/gemini-extension.json +1 -1
  19. package/hooks/session-start +2 -2
  20. package/llms.txt +1 -1
  21. package/package.json +1 -1
  22. package/plugin.json +1 -1
  23. package/prd/v1/prd.md +1 -1
  24. package/scripts/guard/checks/history-risk.mjs +132 -0
  25. package/scripts/guard/checks/prd-clarity-state.mjs +41 -0
  26. package/scripts/guard/checks/prd-clarity.mjs +176 -0
  27. package/scripts/guard/guard.mjs +16 -8
  28. package/scripts/infer-workflow.mjs +20 -0
  29. package/scripts/lib/cmd-doctor.mjs +2 -2
  30. package/scripts/lib/cmd-prd.mjs +84 -1
  31. package/scripts/lib/cmd-solutions.mjs +3 -0
  32. package/scripts/lib/cmd-state.mjs +31 -1
  33. package/scripts/lib/solutions-capture.mjs +5 -0
  34. package/scripts/lib/solutions-index-gen.mjs +33 -3
  35. package/scripts/lib/solutions-inject.mjs +34 -9
  36. package/scripts/lib/state-loader.mjs +13 -0
  37. package/skills/ce-brainstorm/SKILL.md +3 -3
  38. package/skills/ce-brainstorm/references/grounding.md +1 -1
  39. package/skills/ce-brainstorm/references/prd-84-authoring-spec.md +26 -3
  40. package/skills/ce-brainstorm/references/prototype-loop.md +8 -0
  41. package/skills/ce-compound/references/promotion-rules.md +1 -1
  42. package/skills/ce-compound/references/three-tier-index.md +1 -1
  43. package/skills/ce-plan/references/research-workflow.md +1 -1
  44. package/skills/jarvis/references/protocols.md +1 -0
  45. package/skills/workflow-orchestrator/SKILL.md +6 -2
  46. package/skills/workflow-orchestrator/references/s1-path-router.md +7 -0
  47. package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +16 -2
  48. package/skills/workflow-orchestrator/references/s3-plan-pipeline.md +1 -1
  49. package/skills/workflow-start/SKILL.md +1 -1
  50. package/templates/prd.md +9 -1
@@ -0,0 +1,176 @@
1
+ // scripts/guard/checks/prd-clarity.mjs — 产品级 PRD 清晰度机械门(agent-governance P0-1)
2
+ //
3
+ // 背景(2026-09-25):LT 拍板 Q5 推翻 DEC-8 中的弱词机械门决策(DEC-8a)——
4
+ // 「接活前 fail-closed」必须有机械层;LLM reviewer(prd-completeness-reviewer)保留,
5
+ // 与本 checker 并存:checker 管机械可判定项,reviewer 管语义完整性。
6
+ //
7
+ // 检查对象:**产品级 PRD**(requirement/vN/prd.md)。不查变更级 change spec
8
+ // (归 spec-writer/评审既有职责);tasks_skipped 类 change 不经产品级冻结路径,天然不适用。
9
+ //
10
+ // 接入 = 三处(SKILL 调用纪律级,非 hook 强制——如实声明):
11
+ // 1. `tf prd check-clarity <dir|file>`(cmd-prd.mjs,末行 STATUS: PASS | FAIL)
12
+ // 2. standalone:`references/prototype-loop.md` §3.5.5 前置段(ce-brainstorm SKILL QA-4 引用之)
13
+ // 3. orchestrated:`s2-prd-prototype-loop.md` 步骤 3.4(3.5 完整性评审之前)
14
+ //
15
+ // 判定语义 fail-closed:必需段缺失 / 判据无法执行 → FAIL,不得落 N/A 放行。
16
+ // 首版**不建豁免通道**(不过就改 PRD 或调 checker,走正常代码变更流程)。
17
+ // 产品级无 state 文件(.team-flow.yaml 属 change 级),故不使用 gates-probed 式
18
+ // skip-with-reason 豁免模式——那是首版不建豁免的根本原因,不得照抄。
19
+ //
20
+ // 规则源(单真相源裁定,memo v0.5 §4.1 / 反方 I7;**回写已闭合 2026-09-25**):
21
+ // 权威正文 = skills/ce-brainstorm/references/prd-84-authoring-spec.md §4「精确性」,
22
+ // DEC-8a 复议时已扩为 8 类全集(第 6–8 类:否定句/连接歧义/被动语态),
23
+ // 镜像弱词表(工作区 prd-business-readable-design)不再独立维护。
24
+
25
+ import fs from 'node:fs';
26
+
27
+ /**
28
+ * ISO/IEC/IEEE 29148 弱词 8 类规则表(数据可调——误报校准的唯一落点)。
29
+ * 词表来源:prd-84 §4(权威,8 类全集,DEC-8a 复议后收口于此)。
30
+ * 校准纪律:调词表 = 改本文件规则数据并同步权威正文,不得在调用侧打补丁。
31
+ * 格式:{ category, words: string[], note? };words 为子串匹配(中文无词边界)。
32
+ *
33
+ * **2026-09-25 校准排除决策(刻意不收,权威正文 §4 已留痕)**:
34
+ * - `处理`(§4 类 3 示例词):子串会命中模板段名「系统功能处理说明书」→ 冻结死循环;
35
+ * - 单字 `等`(§4 类 4 示例):子串命中「等待/等于/以及」等一切含字场景 → 噪声失控;
36
+ * 词表只收「等等」。两者判读由 LLM reviewer 精确性检查覆盖(并存层职责)。
37
+ */
38
+ export const WEAK_RULES = [
39
+ { category: '比较级与最高级', words: ['更好', '较好', '更快', '更佳', '更稳定', '最快', '最高级', '较快'] },
40
+ { category: '主观评价词', words: ['友好', '易用', '良好', '美观', '简洁', '直观'] },
41
+ { category: '歧义词', words: ['尽量', '总是', '最优', '最小化', '最大化', '适当', '必要时'] },
42
+ { category: '开放式表述', words: ['至少', '不限于', '尽可能', '视情况', '等等'] },
43
+ { category: '漏洞词', words: ['可能', '如适用', '一般', '通常', '支持'] },
44
+ { category: '否定句', words: ['不支持'], note: '改为「当 X 时,系统执行 Y」' },
45
+ { category: '连接歧义', words: ['和/或'], note: '拆分条目或用决策表' },
46
+ { category: '被动语态', words: ['应被'], note: '明确动作主体' },
47
+ ];
48
+
49
+ /** 段缺失判据(fail-closed 最小必需集):命中任一 pattern 即认为段存在。 */
50
+ export const REQUIRED_SECTIONS = [
51
+ // version-1.2:只认 1.2 修订行 / 1.1 版本信息子段(h2「版本修订记录」兜底过松——
52
+ // 子段全删时 h2 仍在,首版实测会漏判,故收紧到子段级)。
53
+ { id: 'version-1.2', desc: '### 1.1 版本信息 / 1.2 修订记录', pattern: /^#{2,4}\s*.*(?:1\.[12]\s*(?:版本信息|修订)|版本信息\s*$)/m },
54
+ { id: 'func-list', desc: '功能清单章节(## 七 / 功能清单)', pattern: /^#{2,3}\s*.*(?:七、|功能清单|系统功能)/m },
55
+ { id: 'sec-8.4', desc: '### 8.4 功能模块详细说明', pattern: /^#{2,4}\s*.*8\.4/m },
56
+ ];
57
+
58
+ /**
59
+ * §8.4 左列语义漂移判据(判据源 = prd-completeness-reviewer.md D6/G7 第 1 条):
60
+ * 左列须为画面/入口锚点;技术维度名 = 违规。
61
+ * 机械实现的边界:**整格匹配**(防「单据类型」这类业务词误杀),另加强技术词子串。
62
+ */
63
+ const DRIFT_EXACT = /^(维度|字段|类型|属性|参数|枚举|单位|数据项)(列表|清单|定义)?$/;
64
+ const DRIFT_SUBSTRING = /(字段名|维度名|数据类型|技术维度|参数名|表字段)/;
65
+
66
+ /** 去除围栏代码块与行内 code(弱词扫描的误报源),保留原文行结构用于定位。 */
67
+ export function stripCode(text) {
68
+ const lines = text.split(/\r?\n/);
69
+ let inFence = false;
70
+ return lines.map((line) => {
71
+ if (/^\s*```/.test(line)) { inFence = !inFence; return ''; }
72
+ if (inFence) return '';
73
+ return line.replace(/`[^`]*`/g, '');
74
+ }).join('\n');
75
+ }
76
+
77
+ /** 提取标题段正文(到下一个同级或更高级标题)。返回 null 表示段不存在。 */
78
+ export function extractSection(text, headingRe) {
79
+ const lines = text.split(/\r?\n/);
80
+ let start = -1;
81
+ let level = 0;
82
+ for (let i = 0; i < lines.length; i += 1) {
83
+ const m = lines[i].match(/^(#{1,6})\s/);
84
+ if (m && headingRe.test(lines[i])) { start = i; level = m[1].length; break; }
85
+ }
86
+ if (start === -1) return null;
87
+ const body = [];
88
+ for (let i = start + 1; i < lines.length; i += 1) {
89
+ const m = lines[i].match(/^(#{1,6})\s/);
90
+ if (m && m[1].length <= level) break;
91
+ body.push(lines[i]);
92
+ }
93
+ return body.join('\n');
94
+ }
95
+
96
+ /** 收集段内所有 markdown 表格的首列数据单元格(跳过表头与分隔行)。 */
97
+ export function firstColumnCells(sectionBody) {
98
+ const cells = [];
99
+ const lines = sectionBody.split(/\r?\n/);
100
+ let inTable = false;
101
+ let headerSeen = false;
102
+ for (const line of lines) {
103
+ if (!line.trim().startsWith('|')) { inTable = false; headerSeen = false; continue; }
104
+ const cols = line.split('|').map((c) => c.trim());
105
+ // cols[0] === ''(前导竖线),数据首列在 index 1
106
+ if (!inTable) { inTable = true; headerSeen = false; continue; } // 表头行
107
+ if (!headerSeen) { headerSeen = true; continue; } // 分隔行(|---|---|)
108
+ const first = cols[1] || '';
109
+ if (first) cells.push(first);
110
+ }
111
+ return cells;
112
+ }
113
+
114
+ /**
115
+ * 核心判定。
116
+ * @param {string} prdPath prd.md 文件绝对/相对路径
117
+ * @returns {{pass: boolean, failures: string[], stats: object}}
118
+ * 失败条目格式:`[类别] 行 N: <片段>` / `[section] ...` / `[drift] ...`
119
+ */
120
+ export function checkPrdClarity(prdPath) {
121
+ const failures = [];
122
+ const stats = { weakHits: 0, driftCells: 0, sectionsMissing: [] };
123
+
124
+ if (!prdPath || !fs.existsSync(prdPath)) {
125
+ return {
126
+ pass: false,
127
+ failures: [`[section] PRD file not found: ${prdPath ?? '(unset)'} — locate requirement/vN/prd.md first`],
128
+ stats,
129
+ };
130
+ }
131
+ const raw = fs.readFileSync(prdPath, 'utf-8');
132
+ if (!raw.trim()) {
133
+ return { pass: false, failures: ['[section] PRD file is empty — write the PRD before running clarity check'], stats };
134
+ }
135
+
136
+ // 1) 必需段存在性(fail-closed:缺失 = FAIL,不得 N/A 放行)。
137
+ for (const sec of REQUIRED_SECTIONS) {
138
+ if (!sec.pattern.test(raw)) {
139
+ stats.sectionsMissing.push(sec.id);
140
+ failures.push(`[section] required section missing: ${sec.desc} — template skeleton incomplete`);
141
+ }
142
+ }
143
+
144
+ // 2) 弱词 8 类扫描(排除代码块/行内 code;表格内容照扫——PRD 表格是业务正文)。
145
+ const clean = stripCode(raw);
146
+ const cleanLines = clean.split(/\r?\n/);
147
+ for (let i = 0; i < cleanLines.length; i += 1) {
148
+ const line = cleanLines[i];
149
+ if (!line.trim()) continue;
150
+ for (const rule of WEAK_RULES) {
151
+ for (const word of rule.words) {
152
+ const idx = line.indexOf(word);
153
+ if (idx >= 0) {
154
+ stats.weakHits += 1;
155
+ const snippet = line.slice(Math.max(0, idx - 12), idx + word.length + 12).trim();
156
+ failures.push(`[weak:${rule.category}] 行 ${i + 1}: …${snippet}… (${rule.note ?? '改为精确表述'})`);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // 3) §8.4 左列语义漂移(G7 第 1 条机械化)。
163
+ const sec84 = extractSection(clean, /8\.4/);
164
+ if (sec84 === null) {
165
+ // 必需段已在其处 FAIL(REQUIRED_SECTIONS sec-8.4),此处不重复计。
166
+ } else {
167
+ for (const cell of firstColumnCells(sec84)) {
168
+ if (DRIFT_EXACT.test(cell) || DRIFT_SUBSTRING.test(cell)) {
169
+ stats.driftCells += 1;
170
+ failures.push(`[drift] §8.4 左列技术维度名: "${cell}" — 左列须为画面/入口锚点(G7-1)`);
171
+ }
172
+ }
173
+ }
174
+
175
+ return { pass: failures.length === 0, failures, stats };
176
+ }
@@ -17,6 +17,9 @@ import { checkCompoundCaptured } from './checks/compound-captured.mjs';
17
17
  import { checkTestMatrixComplete } from './checks/test-matrix-complete.mjs';
18
18
  import { checkTestMatrixReady } from './checks/test-matrix-ready.mjs';
19
19
  import { checkGatesProbed } from './checks/gates-probed.mjs';
20
+ // agent-governance P1-1:接活前双维度(履历 experimental + clarity 跨层管道)
21
+ import { checkPrdClarityState } from './checks/prd-clarity-state.mjs';
22
+ import { checkHistoryRisk } from './checks/history-risk.mjs';
20
23
  import { checkArchReadiness } from './checks/arch-readiness.mjs';
21
24
  import { checkArchSnapshot } from './checks/arch-snapshot.mjs';
22
25
  import { checkArchMerged } from './checks/arch-merged.mjs';
@@ -57,10 +60,12 @@ const TRANSITION_CHECKS = {
57
60
  // 「闸门已实测可跑」文字自证物化为机械证据。检查五件:evidence 存在 / 首行 RED 基线 /
58
61
  // CONTRACT_HASH 值新鲜 / 契约 Gate Registry 的 id 逐 id 覆盖 / **段缺失即 FAIL(fail-closed)**。
59
62
  // hotfix 走 WORKFLOW_TRANSITION_CHECKS 自有覆盖(下方),tweak 由 skip 回退阀放行。
60
- 'bridging:approved-for-build': ['artifacts-exist', 'schema-valid', 'contract-fresh', 'dp-gate-passed', 'dp3-approved', 'gates-probed'],
63
+ // agent-governance P1-1:prd-clarity + history-risk 挂接活门(full 档)——
64
+ // 「接活前验规格 + 履历」;两维缺失态均中性(无记录/INDEX 空 = 放行),存量零变化。
65
+ 'bridging:approved-for-build': ['artifacts-exist', 'schema-valid', 'contract-fresh', 'dp-gate-passed', 'dp3-approved', 'gates-probed', 'prd-clarity', 'history-risk'],
61
66
  // v0.13 §49:test-matrix-ready 门禁前移——full 模式进入 executing 前强制测试准备度
62
67
  //(矩阵存在非空 OR 显式 skip 附理由;legacy 豁免)。hotfix/tweak 沿用 §45.3 豁免,不挂。
63
- 'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed', 'execution-plan-ready', 'test-matrix-ready'],
68
+ 'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed', 'execution-plan-ready', 'test-matrix-ready', 'prd-clarity', 'history-risk'],
64
69
  // v0.35.0 (v0.14 §59.4): arch-snapshot 新增——"先快照后回写"的强制化(legacy 豁免)。
65
70
  // 仅在 full workflow 挂;hotfix/tweak 沿用 §59.4 豁免(紧急/微调不挂)。
66
71
  //
@@ -134,15 +139,15 @@ const TRANSITION_WORKFLOW_REQUIREMENTS = {
134
139
  // 必须显式列出关键跳——未列出的 key 会回落 full 基表(contract-fresh 等对轻路径恒 FAIL)。
135
140
  const QUICK_TRANSITION_CHECKS = {
136
141
  quick: {
137
- 'exploring:approved-for-build': ['direct-short-path'],
138
- 'approved-for-build:executing': ['direct-short-path', 'test-matrix-ready'],
142
+ 'exploring:approved-for-build': ['direct-short-path', 'prd-clarity', 'history-risk'],
143
+ 'approved-for-build:executing': ['direct-short-path', 'test-matrix-ready', 'prd-clarity', 'history-risk'],
139
144
  'executing:closing': ['direct-short-path', 'direct-test-result', 'test-matrix-complete'],
140
145
  'executing:debugging': [],
141
146
  'debugging:executing': ['direct-test-result'],
142
147
  },
143
148
  lightweight: {
144
- 'exploring:approved-for-build': ['direct-short-path'],
145
- 'approved-for-build:executing': ['direct-short-path', 'test-matrix-ready'],
149
+ 'exploring:approved-for-build': ['direct-short-path', 'prd-clarity', 'history-risk'],
150
+ 'approved-for-build:executing': ['direct-short-path', 'test-matrix-ready', 'prd-clarity', 'history-risk'],
146
151
  'executing:closing': ['direct-short-path', 'direct-test-result', 'lightweight-completion-evidence', 'test-matrix-complete'],
147
152
  'executing:debugging': [],
148
153
  'debugging:executing': ['direct-test-result'],
@@ -155,8 +160,8 @@ const PLANNED_TRANSITION_CHECKS = {
155
160
  // 注:不挂 schema-valid——该 checker 强制 canonical specs/<capability>/spec.md,
156
161
  // 与 planned「specs 按需」冲突(P1 实测修正,同步方案 §4.5);反空壳由 artifacts-planned
157
162
  // (proposal≥10 行 + checkbox)+ 最终审查内容契约承担。
158
- 'exploring:approved-for-build': ['artifacts-planned'],
159
- 'approved-for-build:executing': ['execution-plan-ready', 'test-matrix-ready'],
163
+ 'exploring:approved-for-build': ['artifacts-planned', 'prd-clarity', 'history-risk'],
164
+ 'approved-for-build:executing': ['execution-plan-ready', 'test-matrix-ready', 'prd-clarity', 'history-risk'],
160
165
  'executing:closing': [
161
166
  'execution-plan-ready', 'execution-reviews-passed-light', 'test-matrix-complete',
162
167
  'tests-passing', 'arch-merged-light', 'compound-writeback-light', 'test-merged-light',
@@ -293,6 +298,9 @@ async function main() {
293
298
  'test-matrix-complete': (dir) => checkTestMatrixComplete(dir, CTX),
294
299
  'test-matrix-ready': (dir) => checkTestMatrixReady(dir, CTX),
295
300
  'gates-probed': (dir) => checkGatesProbed(dir),
301
+ // agent-governance P1-1(33 维):接活前双维度。缺失态中性——冷启动零行为变化。
302
+ 'prd-clarity': (dir) => checkPrdClarityState(dir),
303
+ 'history-risk': (dir) => checkHistoryRisk(dir),
296
304
  'arch-readiness': (dir) => checkArchReadiness(dir),
297
305
  'arch-snapshot': (dir) => checkArchSnapshot(dir),
298
306
  'arch-merged': (dir) => checkArchMerged(dir),
@@ -55,6 +55,11 @@ function hasKeyword(text, patterns) {
55
55
 
56
56
  function inferMode(changeDir) {
57
57
  const state = readState(changeDir);
58
+ // agent-governance P1-1 双信号(轻量分值化):
59
+ // clarity = 产品级 prd-clarity 跨层记录(pass|fail|null=null 中性——见 prd-clarity-state.mjs);
60
+ // risk = 0.64 双通道信号的分档聚合(low|medium|high|null——显式档位/无产物时 null)。
61
+ // 消费方 = workflow-start 路由推荐(建议级;阻断由 guard 维度承担,这里只影响推荐倾向)。
62
+ const clarity = state.prd_clarity_result ?? null;
58
63
 
59
64
  // Explicit override: honor any non-auto, non-null workflow value
60
65
  // v0.64.0(§3.2 F1):VALID_WORKFLOWS 扩为五值,quick/lightweight 亦为合法显式档位。
@@ -66,6 +71,8 @@ function inferMode(changeDir) {
66
71
  // 双通道(§3.2):显式 workflow 已定时不再建议前门(显式 > 推断)
67
72
  suggested_path: null,
68
73
  explicit: true,
74
+ clarity,
75
+ risk: null, // 显式档位不推断风险分档
69
76
  reason: `workflow explicitly set to '${state.workflow}' in .team-flow.yaml; skipping auto-detection`,
70
77
  };
71
78
  }
@@ -101,6 +108,9 @@ function inferMode(changeDir) {
101
108
  const archPathSignal = hasSchemaChange || hasArchAggregate;
102
109
  // 跨模块 / 高不确定性 → full(D1:full 只留重场景)
103
110
  const fullSignal = hasCrossModule || (taskCount > 4 && fileCount > 6);
111
+ // P1-1 risk 分档:四个布尔信号计数(0=low / 1=medium / ≥2=high)
112
+ const riskSignals = [hasSchemaChange, hasArchAggregate, hasCrossModule, fullSignal].filter(Boolean).length;
113
+ const risk = riskSignals >= 2 ? 'high' : riskSignals === 1 ? 'medium' : 'low';
104
114
 
105
115
  const allExts = files.map(f => {
106
116
  const parts = f.split('.');
@@ -115,6 +125,8 @@ function inferMode(changeDir) {
115
125
  mode: 'full',
116
126
  suggested_path: null,
117
127
  explicit: false,
128
+ clarity,
129
+ risk,
118
130
  reason: 'no planning artifacts detected → full (safe default)',
119
131
  };
120
132
  }
@@ -125,6 +137,8 @@ function inferMode(changeDir) {
125
137
  mode: 'hotfix',
126
138
  suggested_path: 'direct',
127
139
  explicit: false,
140
+ clarity,
141
+ risk,
128
142
  reason: `≤2 tasks, ≤2 files, no schema/API/new-module keywords → hotfix (trivial → suggest path direct; hotfix retained when reusing an existing contract)`,
129
143
  };
130
144
  }
@@ -135,6 +149,8 @@ function inferMode(changeDir) {
135
149
  mode: 'tweak',
136
150
  suggested_path: 'direct',
137
151
  explicit: false,
152
+ clarity,
153
+ risk,
138
154
  reason: `≤4 tasks, only config/doc files, no schema/API/new-module keywords → tweak (trivial → suggest path direct)`,
139
155
  };
140
156
  }
@@ -145,6 +161,8 @@ function inferMode(changeDir) {
145
161
  mode: 'full',
146
162
  suggested_path: 'planned',
147
163
  explicit: false,
164
+ clarity,
165
+ risk,
148
166
  reason: `${taskCount} tasks, ${fileCount} files${hasSchemaChange ? ', schema/API change detected' : ''}${hasArchAggregate ? ', arch/aggregate keywords detected' : ''} → suggest path planned (light architecture); front door: workflow start --path planned (new change) or tf state upgrade <dir> planned (existing change)`,
149
167
  };
150
168
  }
@@ -154,6 +172,8 @@ function inferMode(changeDir) {
154
172
  mode: 'full',
155
173
  suggested_path: null,
156
174
  explicit: false,
175
+ clarity,
176
+ risk,
157
177
  reason: `${taskCount} tasks, ${fileCount} files${codeFileCount > 0 ? ` (${codeFileCount} code files)` : ''}${hasSchemaChange ? ', schema/API change detected' : ''}${hasNewModule ? ', new module detected' : ''}${hasCrossModule ? ', cross-module detected' : ''} → full`,
158
178
  };
159
179
  }
@@ -382,9 +382,9 @@ function checkSolutions(root) {
382
382
  for (const line of readFileSync(indexPath, 'utf-8').split('\n')) {
383
383
  if (!line.startsWith('|') || line.startsWith('| date') || line.startsWith('|--')) continue;
384
384
  const cells = parseTableRow(line);
385
- // v0.57.0 P4:`source` 列新增后列数为 7(存量 INDEX)或 8(本版重建);
385
+ // v0.57.0 P4:`source` 列新增后列数为 7(存量)/ 8;P2-2 `flags` 列后 9(本版重建);
386
386
  // 另校验 `file` 列形状——列数在界内仍可能因未转义的 `|` 而错位。
387
- if (cells.length < 7 || cells.length > 8) continue;
387
+ if (cells.length < 7 || cells.length > 9) continue;
388
388
  if (!/^[a-z][a-z-]*\/.+\.md$/.test(cells[6])) continue;
389
389
  indexRows.set(cells[6], { date: cells[0], phase: cells[1], domain: cells[2], type: cells[3], severity: cells[4] });
390
390
  }
@@ -14,7 +14,9 @@
14
14
  // 供主会话 Phase 0.0 的阻塞提问(G1)取判据——**判据不在 skill 散文里另写一套**。
15
15
 
16
16
  import fs from 'node:fs';
17
+ import { existsSync, mkdirSync } from 'node:fs';
17
18
  import path from 'node:path';
19
+ import { createHash } from 'node:crypto';
18
20
  import { parseArgs } from 'node:util';
19
21
  import { loadConfig, resolveConfigWritePath } from './config-loader.mjs';
20
22
  import {
@@ -23,6 +25,7 @@ import {
23
25
  compareAgainstAck,
24
26
  shortHash,
25
27
  } from './template-hash.mjs';
28
+ import { checkPrdClarity } from '../guard/checks/prd-clarity.mjs';
26
29
 
27
30
  const VALID_MODES = ['adopt_latest', 'keep_legacy'];
28
31
 
@@ -48,6 +51,7 @@ export async function run(args) {
48
51
  const sub = positionals[0];
49
52
  if (sub === 'ack') return ack(positionals.slice(1), values);
50
53
  if (sub === 'check') return check(values);
54
+ if (sub === 'check-clarity') return checkClarity(positionals.slice(1));
51
55
  usage();
52
56
  }
53
57
 
@@ -58,11 +62,90 @@ function usage() {
58
62
  ' tf prd ack set --mode adopt_latest --in-use-hash … [--plugin-hash …] [--spec-hash …]\n' +
59
63
  ' tf prd ack show # 查看当前 ack\n' +
60
64
  ' tf prd ack clear # 清除 ack(下次将重新提问)\n' +
61
- ' tf prd check [--template-path <p>] [--plugin-root <p>] # 比对 hash 与 ack,输出 STATUS'
65
+ ' tf prd check [--template-path <p>] [--plugin-root <p>] # 比对 hash 与 ack,输出 STATUS\n' +
66
+ ' tf prd check-clarity <dir|prd.md> # 清晰度机械门(弱词/段/§8.4 漂移),末行 STATUS: PASS | FAIL,FAIL exit 1'
62
67
  );
63
68
  process.exit(2);
64
69
  }
65
70
 
71
+ /**
72
+ * `tf prd check-clarity`:产品级 PRD 清晰度机械门(agent-governance P0-1,DEC-8a)。
73
+ *
74
+ * 入参 = 目录(自动定位 requirement/vN/prd.md,多个 vN 取最大)或 prd.md 文件路径。
75
+ * 输出末行固定 `STATUS: PASS | FAIL`;**FAIL 时 exit 1**(fail-closed——本命令是门,
76
+ * 不同于 `tf prd check` 的判据工具语义;SKILL 冻结步骤 MUST 读 STATUS 且不得忽略非零退出码)。
77
+ * 判定细节见 scripts/guard/checks/prd-clarity.mjs 头注(规则源 / 豁免边界 / 校准纪律)。
78
+ */
79
+ function checkClarity(positionalArgs) {
80
+ const target = positionalArgs[0];
81
+ if (!target) usage();
82
+
83
+ let prdPath = target;
84
+ if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
85
+ prdPath = resolvePrdInDir(target);
86
+ if (!prdPath) {
87
+ console.error(`no requirement/vN/prd.md found under: ${target}`);
88
+ console.log('STATUS: FAIL');
89
+ process.exit(1);
90
+ }
91
+ }
92
+ if (!fs.existsSync(prdPath)) {
93
+ console.error(`PRD file not found: ${prdPath}`);
94
+ console.log('STATUS: FAIL');
95
+ process.exit(1);
96
+ }
97
+
98
+ const { pass, failures, stats } = checkPrdClarity(prdPath);
99
+ console.log('prd :', prdPath);
100
+ console.log('weak_hits :', stats.weakHits, '| drift_cells:', stats.driftCells,
101
+ '| missing_sections:', stats.sectionsMissing.length ? stats.sectionsMissing.join(',') : '(none)');
102
+ for (const f of failures) console.log(f);
103
+ console.log(`STATUS: ${pass ? 'PASS' : 'FAIL'}`);
104
+ recordClarityResult(pass, prdPath); // P1-1 跨层管道:落产品级记录供 tf state init 拷贝
105
+ if (!pass) process.exit(1);
106
+ }
107
+
108
+ /**
109
+ * P1-1 clarity 跨层管道的写侧:落 `<cwd>/.team-flow/prd-clarity.json`。
110
+ * 消费链 = `tf state init` 拷入 change state(prd_clarity_result/prd_clarity_hash)
111
+ * → guard 维度 `prd-clarity`(checks/prd-clarity-state.mjs)在接活前转换消费。
112
+ * PASS/FAIL 都落盘(fail 记录让绕过 S2 的 direct change 也在变更级被拦——双保险)。
113
+ */
114
+ function recordClarityResult(pass, prdPath) {
115
+ try {
116
+ const dir = path.join(process.cwd(), '.team-flow');
117
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
118
+ const content = fs.readFileSync(prdPath, 'utf-8');
119
+ const sha = createHash('sha256').update(content).digest('hex');
120
+ fs.writeFileSync(path.join(dir, 'prd-clarity.json'), `${JSON.stringify({
121
+ status: pass ? 'pass' : 'fail',
122
+ prd: prdPath,
123
+ prd_hash: `sha256:${sha}`,
124
+ checked_at: new Date().toISOString(),
125
+ }, null, 2)}\n`);
126
+ } catch {
127
+ // 落盘失败不吞主判定——STATUS 已输出;管道缺失 = 变更级维度走中性,fail-closed 不受影响
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 目录内定位 prd.md:<dir>/prd.md、requirement/ 下最大 vN、或老布局 prd/ 下最大 vN
133
+ * (存量项目用 `prd/vN/prd.md`——2026-09-25 存量校准时实测补上,防新门卡死老布局)。
134
+ */
135
+ function resolvePrdInDir(dir) {
136
+ const direct = path.join(dir, 'prd.md');
137
+ if (fs.existsSync(direct)) return direct;
138
+ for (const base of ['requirement', 'prd']) {
139
+ const reqDir = path.join(dir, base);
140
+ if (!fs.existsSync(reqDir)) continue;
141
+ const versions = fs.readdirSync(reqDir)
142
+ .filter((n) => fs.existsSync(path.join(reqDir, n, 'prd.md')))
143
+ .sort((a, b) => parseInt(a.replace(/\D/g, ''), 10) - parseInt(b.replace(/\D/g, ''), 10)); // 数值序:v10 > v9(字典序会取错)
144
+ if (versions.length > 0) return path.join(reqDir, versions[versions.length - 1], 'prd.md');
145
+ }
146
+ return null;
147
+ }
148
+
66
149
  /** 读原始配置文件(未与 DEFAULTS 合并),用于区分「用户显式配置」与「默认值」。 */
67
150
  function readRawConfig() {
68
151
  const p = resolveConfigWritePath(process.cwd());
@@ -21,6 +21,8 @@ export async function run(args) {
21
21
  severity: { type: 'string' },
22
22
  summary: { type: 'string' },
23
23
  source: { type: 'string' },
24
+ // agent-governance P2-1:纠错条目的来源事件锚(HOLD 的 msg_id / 决策日志行)
25
+ 'source-event': { type: 'string' },
24
26
  // v0.57.0 §4.1:inject 的展示条数控制与 backfill 的预演
25
27
  limit: { type: 'string' },
26
28
  all: { type: 'boolean' },
@@ -52,6 +54,7 @@ export async function run(args) {
52
54
  severity: values.severity,
53
55
  summary: values.summary,
54
56
  source: values.source,
57
+ source_event: values['source-event'],
55
58
  dir: values.dir,
56
59
  });
57
60
  return;
@@ -1,7 +1,7 @@
1
1
  // scripts/lib/cmd-state.mjs — tf state subcommand handler
2
2
  import { parseArgs } from 'node:util';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { existsSync, mkdirSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
5
5
  import path, { dirname, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  // VALID_STATES 统一从 state-loader.mjs 引入(状态机合法值唯一真相源),不再本地硬编码。
@@ -11,6 +11,26 @@ import { computeArtifactsHash, computeContractHash, computeTestMatrixHash } from
11
11
 
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
 
14
+ /**
15
+ * P1-1:自 changeDir 向上(限 6 层)找 `<root>/.team-flow/prd-clarity.json` 并解析。
16
+ * 找不到 / 解析失败 → null(= 中性,init 不打字段)。
17
+ */
18
+ function findProductClarityRecord(changeDir) {
19
+ let dir = path.resolve(changeDir);
20
+ for (let i = 0; i < 6; i += 1) {
21
+ const rec = path.join(dir, '.team-flow', 'prd-clarity.json');
22
+ if (existsSync(rec)) {
23
+ const parsed = JSON.parse(readFileSync(rec, 'utf-8'));
24
+ if (parsed && (parsed.status === 'pass' || parsed.status === 'fail')) return parsed;
25
+ return null;
26
+ }
27
+ const parent = path.dirname(dir);
28
+ if (parent === dir) break;
29
+ dir = parent;
30
+ }
31
+ return null;
32
+ }
33
+
14
34
  // v0.13 §50(BUG-A 政策修正):test_result 移出可手工设置字段——测试证据必须经
15
35
  // tf test record 程序化记录,手工自述值不再被 tests-passing 门禁接受。
16
36
  // v0.13 §48.2:test_matrix_skip_reason 可设置(显式 skip 的理由留痕)。
@@ -121,6 +141,16 @@ export async function run(args) {
121
141
  const state = readState(changeDir);
122
142
  if (!stateFileExisted) {
123
143
  state.schema_version = 1;
144
+ // P1-1 clarity 跨层管道:拷贝产品级记录(唯一写入路径——state 字段不在
145
+ // SETTABLE_FIELDS,防自助放行)。find 向上找 <root>/.team-flow/prd-clarity.json。
146
+ // 记录 fail 也拷:绕过 S2 冻结的 direct change 在变更级被 guard 拦下(双保险)。
147
+ try {
148
+ const rec = findProductClarityRecord(changeDir);
149
+ if (rec) {
150
+ state.prd_clarity_result = rec.status === 'fail' ? 'fail' : 'pass';
151
+ state.prd_clarity_hash = rec.prd_hash ?? null;
152
+ }
153
+ } catch { /* 无记录 = 中性,不阻断 init */ }
124
154
  // v0.64.0 P0(§4 扫描基线):init 打戳 base_sha(架构 surface 扫描的 git 基线)。
125
155
  // 非 git 环境 / 尚无提交 → 保持 null,扫描时按 §4 退化参照(origin → FAIL)。
126
156
  try {
@@ -42,6 +42,10 @@ export function run(args = {}) {
42
42
  const severity = args.severity || 'medium';
43
43
  const summary = args.summary || '(no summary)';
44
44
  const source = args.source || '';
45
+ // agent-governance P2-1:来源事件锚——correction 条目的毕业条件消费它
46
+ //(P2-2「锚未标撤销」判定;无锚条目按未毕业处理)。HOLD 场景 = escalation msg_id
47
+ // 或决策日志行;非空才写入 frontmatter。
48
+ const sourceEvent = args.source_event || '';
45
49
  const dir = args.dir || 'docs/solutions';
46
50
 
47
51
  // 注:process.exit 后补 return——真实 CLI 下 exit 即终止;测试环境 mock exit 时
@@ -79,6 +83,7 @@ type: ${type}
79
83
  severity: ${severity}
80
84
  date: ${date}
81
85
  source: ${source}
86
+ ${sourceEvent ? `source_event: ${sourceEvent}` : ''}
82
87
  title: ${title}
83
88
  ---
84
89
 
@@ -41,6 +41,34 @@ import { parseFrontmatter, listSolutionEntries } from './solutions-entry.mjs';
41
41
  /** INDEX 保留的最大**条目数**(非行数——文件还含 4 行头部) */
42
42
  export const MAX_INDEX_ENTRIES = 150;
43
43
 
44
+ /**
45
+ * agent-governance P2-2(earned automation 见习):毕业阈值。
46
+ * `flags` 列派生规则(**纯派生、随重建重算**——INDEX 是投影):
47
+ * - 条目有 `source_event` 锚(P2-1 起的 correction 条目):confirmations ≥ 本阈值 → `auto-ok`,
48
+ * 否则 → `probation`(见习:不进自动注入上下文)。
49
+ * 「锚被标撤销」的毕业否决暂无实现(撤销动作未建,如实声明——source_event 锚已就位,
50
+ * 撤销标记是其消费侧的未来项)。
51
+ * - 无锚存量条目 → `auto-ok`(**grandfather**:它们已在注入通道,见习化 = 行为回退,不采纳;
52
+ * 与 schema_version 存量豁免同构)。
53
+ * - 红区硬排除不在此处:红区判定是**消费侧**职责(inject 按 summary/domain 关键词排除),
54
+ * flags 表示毕业状态、不表示区域。
55
+ */
56
+ export const GRADUATED_MIN_CONFIRMATIONS = 3;
57
+
58
+ /** 解析 frontmatter confirmations 计数(宽松:仅计数,promote 的严格解析不在此复用)。 */
59
+ function countConfirmations(fm) {
60
+ const raw = fm?.confirmations;
61
+ if (raw == null || raw === '') return 0;
62
+ return String(raw).replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).length;
63
+ }
64
+
65
+ /** 派生 flags(见上注释的规则表)。 */
66
+ export function deriveFlags(fm) {
67
+ const hasAnchor = Boolean(fm?.source_event && String(fm.source_event).trim());
68
+ if (!hasAnchor) return 'auto-ok';
69
+ return countConfirmations(fm) >= GRADUATED_MIN_CONFIRMATIONS ? 'auto-ok' : 'probation';
70
+ }
71
+
44
72
  /**
45
73
  * 解析条目文件的 frontmatter(扁平键值)——**收敛到共享层**(v0.57.0 P4 二轮)。
46
74
  *
@@ -111,6 +139,8 @@ export function refreshIndex(dir, { phases = SOLUTION_PHASES, maxEntries = MAX_I
111
139
  file: `${phase}/${file}`,
112
140
  // v0.57.0 P4:source 进 INDEX 供 inject 做同源聚类(此前只存在于条目文件,消费方读不到)
113
141
  source: fm.source || '',
142
+ // P2-2:见习/毕业派生状态(规则见 GRADUATED_MIN_CONFIRMATIONS 注释)
143
+ flags: deriveFlags(fm),
114
144
  });
115
145
  }
116
146
  }
@@ -133,10 +163,10 @@ export function refreshIndex(dir, { phases = SOLUTION_PHASES, maxEntries = MAX_I
133
163
  // "只读 INDEX、不读条目文件",故聚类键必须进索引。消费方对 7/8 列都容错(见 solutions-inject)。
134
164
  let index = '# Solutions Index\n';
135
165
  index += `<!-- 每条一行,排序 severity 降序 → date 降序 → file 兜底,≤${maxEntries} 条上限;summary/source 内的 | 转义为 \\| -->\n`;
136
- index += '| date | phase | domain | type | severity | summary | file | source |\n';
137
- index += '|------|-------|--------|------|----------|---------|------|--------|\n';
166
+ index += '| date | phase | domain | type | severity | summary | file | source | flags |\n';
167
+ index += '|------|-------|--------|------|----------|---------|------|--------|-------|\n';
138
168
  for (const e of kept) {
139
- index += `| ${escapeCell(e.date)} | ${escapeCell(e.phase)} | ${escapeCell(e.domain)} | ${escapeCell(e.type)} | ${escapeCell(e.severity)} | ${escapeCell(e.summary)} | ${escapeCell(e.file)} | ${escapeCell(e.source)} |\n`;
169
+ index += `| ${escapeCell(e.date)} | ${escapeCell(e.phase)} | ${escapeCell(e.domain)} | ${escapeCell(e.type)} | ${escapeCell(e.severity)} | ${escapeCell(e.summary)} | ${escapeCell(e.file)} | ${escapeCell(e.source)} | ${escapeCell(e.flags)} |\n`;
140
170
  }
141
171
 
142
172
  writeFileSync(join(dir, 'INDEX.md'), index, 'utf-8');