@xulthekl/team-flow 0.61.0 → 0.63.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 (57) 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/AGENTS.md +6 -4
  9. package/CHANGELOG.md +60 -0
  10. package/GEMINI.md +1 -1
  11. package/INSTALL.md +1 -1
  12. package/README.md +2 -2
  13. package/agents/prd-completeness-reviewer.md +49 -11
  14. package/agents/prd-writer.md +47 -13
  15. package/docs/README_en.md +1 -1
  16. package/docs/decision-points.md +29 -0
  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" +6 -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/scripts/check-project-config.mjs +52 -1
  24. package/scripts/guard/checks/contract-fresh.mjs +48 -4
  25. package/scripts/guard/checks/gates-probed.mjs +175 -0
  26. package/scripts/guard/guard.mjs +17 -2
  27. package/scripts/lib/cmd-config.mjs +9 -5
  28. package/scripts/lib/cmd-prd.mjs +225 -0
  29. package/scripts/lib/cmd-state.mjs +4 -0
  30. package/scripts/lib/cmd-version.mjs +3 -1
  31. package/scripts/lib/config-loader.mjs +39 -0
  32. package/scripts/lib/state-loader.mjs +12 -0
  33. package/scripts/lib/template-hash.mjs +95 -0
  34. package/scripts/team-flow.mjs +1 -0
  35. package/skills/build-executor/SKILL.md +6 -11
  36. package/skills/build-executor/references/wave-delivery-selfcheck.md +92 -0
  37. package/skills/ce-brainstorm/SKILL.md +91 -27
  38. package/skills/ce-brainstorm/references/brainstorm-sections.md +26 -8
  39. package/skills/ce-brainstorm/references/evidence-chain-validation.md +1 -1
  40. package/skills/ce-brainstorm/references/prd-84-authoring-spec.md +182 -0
  41. package/skills/ce-brainstorm/references/prd-mapping.md +9 -4
  42. package/skills/ce-brainstorm/references/prototype-loop.md +2 -2
  43. package/skills/code-reviewer/SKILL.md +4 -0
  44. package/skills/contract-builder/SKILL.md +21 -0
  45. package/skills/contract-builder/references/bridging-gate-dry-run.md +89 -0
  46. package/skills/contract-builder/references/freeze-and-errata.md +81 -0
  47. package/skills/prototype/references/orchestration-flow.md +1 -1
  48. package/skills/release-archivist/SKILL.md +9 -0
  49. package/skills/spec-writer/SKILL.md +3 -0
  50. package/skills/spec-writer/references/facts-referencing.md +64 -0
  51. package/skills/workflow-orchestrator/SKILL.md +1 -1
  52. package/skills/workflow-orchestrator/references/feedback-loops.md +10 -5
  53. package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +3 -3
  54. package/skills/workflow-orchestrator/references/state-model.md +1 -1
  55. package/skills/workflow-start/SKILL.md +1 -0
  56. package/templates/prd-brainstorm-profile.md +9 -3
  57. package/templates/prd.md +76 -49
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-flow",
3
- "version": "0.61.0",
3
+ "version": "0.63.0",
4
4
  "description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact) + design-system (design tokens + component contract + AI primer + showcase) + test-strategy (test strategy design) + project-initialize (new service onboarding) + jarvis (team-flow decision agent, opt-in). 28 skills + 17 agents, one install.",
5
5
  "author": {
6
6
  "name": "LT"
@@ -14,16 +14,25 @@
14
14
  // 零消费者:`prototype.designSystemC` / `prototype.versionWorktree`(全仓无引用)
15
15
  // → 前两类做**存在性检查**;后两类只报"未被消费"(不是错误,是配置残留)。
16
16
  //
17
+ // ── v0.62.0 新增维度⑤:PRD 模板漂移(F3)─────────────────────────────────────
18
+ // 动机同 F1 的根因:模板副本会随时间落后且**无任何提示**(项目侧无感知)。
19
+ // 真消费点(`prd.template` 指向项目内文件)→ 与插件内置比对,落后给两条出路;
20
+ // 零消费者(`.team-flow/templates/` 有副本但 config 未指向)→ 报"影子副本未被消费"。
21
+ // **exit 0 不阻断**——与既有语义一致:提示而非门禁。
22
+ //
17
23
  // Usage: node scripts/check-project-config.mjs [项目根,默认 cwd]
18
24
  // 退出码:0 = 正常(含仅警告);1 = 配置存在但不可解析
19
25
 
20
26
  import { readFileSync, existsSync } from 'node:fs';
21
27
  import { resolve, join, dirname } from 'node:path';
22
28
  import { fileURLToPath } from 'node:url';
29
+ import { resolveConfigWritePath } from './lib/config-loader.mjs';
30
+ import { hashTemplateFile, shortHash } from './lib/template-hash.mjs';
23
31
 
24
32
  const PLUGIN_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
25
33
  const root = resolve(process.argv[2] || process.cwd());
26
- const cfgPath = join(root, '.team-flow', 'team-flow.config.json');
34
+ // v0.62.0(G5 对齐):改用与读取一致的解析(原硬编码只读 .team-flow/,会漏掉 legacy 根目录配置)
35
+ const cfgPath = resolveConfigWritePath(root) || join(root, '.team-flow', 'team-flow.config.json');
27
36
 
28
37
  if (!existsSync(cfgPath)) {
29
38
  console.log(`ℹ️ 未找到 ${cfgPath}(跳过配置漂移检查)`);
@@ -74,6 +83,48 @@ if (cfg.version) {
74
83
  }
75
84
  }
76
85
 
86
+ // ⑤ PRD 模板漂移(v0.62.0 · F3)
87
+ {
88
+ const prd = cfg.prd || {};
89
+ const tplCfg = prd.template;
90
+ const builtinTpl = join(PLUGIN_DIR, 'templates', 'prd.md');
91
+ const shadowTpl = join(root, '.team-flow', 'templates', 'prd.md');
92
+ const shadowProfile = join(root, '.team-flow', 'templates', 'prd-brainstorm-profile.md');
93
+
94
+ if (tplCfg) {
95
+ // 真消费点:config 显式指向某模板
96
+ const tplPath = resolve(root, tplCfg);
97
+ if (!existsSync(tplPath)) {
98
+ warnings.push(`prd.template 指向的路径不存在:${tplCfg}(将导致撰写读不到模板)`);
99
+ } else {
100
+ const a = hashTemplateFile(tplPath);
101
+ const b = hashTemplateFile(builtinTpl);
102
+ if (a && b && a !== b) {
103
+ warnings.push(
104
+ `prd.template 指向的模板与插件内置**不一致**(配置 ${shortHash(a)} vs 内置 ${shortHash(b)})。` +
105
+ `两条出路:① 保留定制 → 人工比对合并;② 回归内置 → tf config --set prd.template=templates/prd.md`
106
+ );
107
+ }
108
+ // profile 成对性(真消费点侧)
109
+ const pairedProfile = `${tplPath.slice(0, -3)}-brainstorm-profile.md`;
110
+ if (!existsSync(pairedProfile)) {
111
+ warnings.push(`模板 ${tplCfg} 未配套 brainstorm profile(期望同目录 …-brainstorm-profile.md),将回退内置 profile——二者应成对处置`);
112
+ }
113
+ }
114
+ } else if (existsSync(shadowTpl)) {
115
+ // 零消费者:有副本但 config 未指向
116
+ warnings.push(
117
+ `存在**未被消费的影子副本** .team-flow/templates/prd.md —— v0.62.0(F2)起默认改用插件内置模板,` +
118
+ `该副本不会被读取。建议删除;若确要定制,用 tf config --set prd.template=.team-flow/templates/prd.md 显式指向`
119
+ );
120
+ if (!existsSync(shadowProfile)) {
121
+ warnings.push(`影子副本未配套 profile:.team-flow/templates/ 下有 prd.md 但无 prd-brainstorm-profile.md(二者应成对处置)`);
122
+ }
123
+ } else if (existsSync(shadowProfile) && !existsSync(shadowTpl)) {
124
+ warnings.push(`.team-flow/templates/ 下有 prd-brainstorm-profile.md 但无 prd.md(二者应成对处置)`);
125
+ }
126
+ }
127
+
77
128
  if (warnings.length === 0) {
78
129
  console.log(`✅ 配置无漂移(${cfgPath})`);
79
130
  process.exit(0);
@@ -1,17 +1,61 @@
1
1
  // scripts/guard/checks/contract-fresh.mjs — check contract staleness via hash comparison
2
+ //
3
+ // v0.63.0(workflow-feedback 20260923-013114 S3):本维度新增挂载到 executing:closing。
4
+ // 动机:S3「DP-3 后 planning 制品冻结」的反查锚——原本 closing 侧反查依赖
5
+ // execution-plan-ready(validatePlan 比对 plan 内嵌的 artifacts_hash/contract_hash),
6
+ // 而该比对可被 `tf execution refresh-hash` 一键刷平(它只改 plan JSON)→ 把 gate-affecting
7
+ // 变更伪记为陈述性勘误后,closing 全维 PASS、wave receipt 亦不失效(wave_fingerprint
8
+ // 不含 artifacts_hash)→ **伪绿静默通过**。本维度直接比对 state.artifacts_hash 与制品实算值,
9
+ // refresh-hash 无法清屏(它不碰 state),故为独立锚。
10
+ //
11
+ // 注意(卡死面):本维度**无 legacy 豁免**(与既有挂载点一致)——存量 change 在 closing
12
+ // 若状态文件缺 artifacts_hash 会被拦。故失败信息必须给出可执行出路(tf state rebuild),
13
+ // 不得让门禁成为无出口的闭门(参照 arch_merge_skipped 的设计原则)。
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
2
16
  import { isContractFresh } from '../../lib/hash.mjs';
17
+ import { readState } from '../../lib/state-loader.mjs';
18
+
19
+ // v0.63.0(P4 评审 L3):裸 rebuild 会重算并覆盖 state.artifacts_hash——若发生在 DP-3 冻结之后,
20
+ // 它就成了与 refresh-hash 并列的「清屏」通道,且不留任何 errata 痕迹。故提示按时间点分流:
21
+ // 冻结前的改动可裸 rebuild;冻结后的改动必须走勘误登记/例外 2,不得用 rebuild 把差异抹平。
22
+ const REBUILD_HINT =
23
+ 'If the planning change predates DP-3 approval, re-capture the hash: `tf state rebuild <dir>`. '
24
+ + 'If it happened AFTER DP-3, the artifacts are FROZEN: record it in the contract `## Errata Register` '
25
+ + 'under the applicable exception (doc-only corrections), or take the gate-affecting path rebuild->revise '
26
+ + '(exception 2). Do NOT rebuild past a frozen change — that erases the diff without any errata trace.';
3
27
 
4
28
  /**
5
29
  * Compare stored artifacts_hash in .team-flow.yaml against current artifact hashes.
6
30
  * Returns { pass, failures[] }.
7
31
  */
8
32
  export function checkContractFresh(changeDir) {
9
- const fresh = isContractFresh(changeDir);
10
- if (fresh) {
33
+ if (isContractFresh(changeDir)) {
11
34
  return { pass: true, failures: [] };
12
35
  }
36
+
37
+ // 区分两类失败,给出各自的出路(否则关闭期死锁无指示)。
38
+ let stored = null;
39
+ try {
40
+ const state = readState(changeDir);
41
+ stored = state?.artifacts_hash ?? null;
42
+ } catch {
43
+ stored = null;
44
+ }
45
+ if (!stored || stored === 'null') {
46
+ return {
47
+ pass: false,
48
+ failures: [
49
+ 'execution-contract.md freshness cannot be judged: the state file carries no artifacts_hash '
50
+ + '(hand-written or truncated .team-flow.yaml). Re-capture it: `tf state rebuild <dir>`.',
51
+ ],
52
+ };
53
+ }
54
+
13
55
  return {
14
56
  pass: false,
15
- failures: ['execution-contract.md is stale: artifacts hash mismatch. Re-run contract-builder to regenerate.'],
57
+ failures: [
58
+ `execution-contract.md is stale: artifacts hash mismatch. ${REBUILD_HINT}`,
59
+ ],
16
60
  };
17
- }
61
+ }
@@ -0,0 +1,175 @@
1
+ // scripts/guard/checks/gates-probed.mjs — bridging dry-run evidence gate
2
+ //
3
+ // v0.63.0(workflow-feedback 20260923-013114 S2):
4
+ // full workflow 的 bridging→approved-for-build 新增本维度——契约产出的 G 类闸门
5
+ // 必须已在**主工作区当前态**跑过 dry-run(预期 FAIL = RED 基线),原始输出留档。
6
+ // 动机:`design.md` R-7「闸门命令已实测可跑」长期只是**文字自证**;实测中 G1 格式约束、
7
+ // grep shim、dist 误入扫描、desc 文案 vs G2 互斥均在施工中才暴露。
8
+ //
9
+ // **存在性强制(Critical 处置,不得退回内容型豁免)**:
10
+ // 契约缺 `## Gate Registry` 段 / 表解析失败 → **FAIL**,不得落入 N/A 放行。
11
+ // 依据:v0.13 RC-1 已删除「以 contract 内容为键」的豁免——test-gate-exemptions.mjs
12
+ // :7-10 明文记载「无法区分真存量与漏生成,导致零测试通过全部门禁」;既有先例
13
+ // `contractDeclaresTestMatrix` 走的是段存在性硬要求。本 check 复刻该先例。
14
+ //
15
+ // 与 test-matrix-ready(:49 入口门禁)同构:legacy 豁免 + 显式 skip 附理由。
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { readState } from '../../lib/state-loader.mjs';
19
+ import { computeContractHash } from '../../lib/hash.mjs';
20
+ import { isLegacyChange } from './test-gate-exemptions.mjs';
21
+
22
+ export const EVIDENCE_REL = path.join('.superpowers', 'test-evidence', 'bridging-gates-red.txt');
23
+ const REGISTRY_HEADING = '## Gate Registry';
24
+ const RED_BASELINE_LINE = 'EXPECTED: FAIL (RED baseline)';
25
+ const SKIP_REASON_HINT =
26
+ 'gates_probed_skipped=true requires gates_probed_skip_reason — record it: '
27
+ + "tf state set <dir> gates_probed_skip_reason '<why this change has no gate dry-run>'";
28
+
29
+ /** 提取 '## Gate Registry' 段正文(到下一个同级标题或文件尾)。 */
30
+ export function extractRegistrySection(contractText) {
31
+ const lines = contractText.split(/\r?\n/);
32
+ const start = lines.findIndex((l) => l.trim() === REGISTRY_HEADING);
33
+ if (start === -1) return null;
34
+ const body = [];
35
+ for (let i = start + 1; i < lines.length; i += 1) {
36
+ if (/^##\s/.test(lines[i])) break;
37
+ body.push(lines[i]);
38
+ }
39
+ return body.join('\n');
40
+ }
41
+
42
+ /**
43
+ * 解析 Gate Registry 表格的 id 列。
44
+ * 表头固定 `| id | phase | command | expected |`;id 形如 G-1。
45
+ * @returns {string[]} id 列表(去重,出现顺序)
46
+ */
47
+ export function parseGateIds(sectionBody) {
48
+ const ids = [];
49
+ for (const line of sectionBody.split(/\r?\n/)) {
50
+ if (!line.trim().startsWith('|')) continue;
51
+ const cells = line.split('|').map((c) => c.trim());
52
+ // cells[0] === ''(前导竖线),故首列在 index 1
53
+ const first = cells[1] || '';
54
+ if (!first || /^-+$/.test(first) || first.toLowerCase() === 'id') continue;
55
+ if (!ids.includes(first)) ids.push(first);
56
+ }
57
+ return ids;
58
+ }
59
+
60
+ /** 段内显式声明零闸门 + 理由(形如 `N/A: <理由>`)。 */
61
+ export function declaredNotApplicable(sectionBody) {
62
+ const m = sectionBody.match(/(?:^|\n)\s*(?:N\/A|NA)\s*[::]\s*(.+)/i);
63
+ return m ? m[1].trim() : null;
64
+ }
65
+
66
+ /**
67
+ * Returns { pass, failures[], reason? }.
68
+ */
69
+ export function checkGatesProbed(changeDir) {
70
+ const state = readState(changeDir);
71
+
72
+ // Exemption 1:legacy change(v0.32.0 之前初始化,无 schema_version 打戳)。
73
+ if (isLegacyChange(state)) {
74
+ return { pass: true, failures: [], reason: 'legacy change — initialized before v0.32.0' };
75
+ }
76
+
77
+ // Exemption 2:显式 skip(必须附理由)。
78
+ if (state.gates_probed_skipped === 'true') {
79
+ const reason = state.gates_probed_skip_reason;
80
+ if (typeof reason !== 'string' || reason.trim().length === 0) {
81
+ return { pass: false, failures: [SKIP_REASON_HINT] };
82
+ }
83
+ return { pass: true, failures: [], reason: `explicitly skipped: ${reason}` };
84
+ }
85
+
86
+ // 段存在性硬要求(fail-closed):缺段 / 解析失败一律 FAIL,不得落 N/A。
87
+ const contractPath = path.join(changeDir, 'execution-contract.md');
88
+ if (!fs.existsSync(contractPath)) {
89
+ return { pass: false, failures: ['execution-contract.md is missing — run contract-builder first'] };
90
+ }
91
+ const contractText = fs.readFileSync(contractPath, 'utf-8');
92
+ const section = extractRegistrySection(contractText);
93
+ if (section === null) {
94
+ return {
95
+ pass: false,
96
+ failures: [
97
+ `contract does not declare ${REGISTRY_HEADING} — return to bridging so contract-builder generates it.`,
98
+ 'A missing section is NOT a "no gates" declaration: absence of the registry cannot be distinguished '
99
+ + 'from an omitted registry (v0.13 RC-1 content-key exemption was removed for exactly this reason).',
100
+ 'If this change genuinely has no gates, write the section with an explicit "N/A: <reason>" line.',
101
+ ],
102
+ };
103
+ }
104
+ const gateIds = parseGateIds(section);
105
+
106
+ // N/A:段存在 + 显式声明零闸门 + 理由(state 键为备用通道)。
107
+ if (gateIds.length === 0) {
108
+ const naReason = declaredNotApplicable(section);
109
+ if (naReason) {
110
+ return { pass: true, failures: [], reason: `not applicable — ${naReason}` };
111
+ }
112
+ if (state.gates_probed_na === 'true') {
113
+ return { pass: true, failures: [], reason: 'not applicable — gates_probed_na set in state' };
114
+ }
115
+ return {
116
+ pass: false,
117
+ failures: [
118
+ `${REGISTRY_HEADING} present but declares no gate id and no "N/A: <reason>" line.`,
119
+ 'Either list the gates (| id | phase | command | expected |) or state "N/A: <reason>".',
120
+ ],
121
+ };
122
+ }
123
+
124
+ // 正常路径:evidence 文件 + 首行 RED 基线 + hash 值新鲜 + id 全覆盖。
125
+ const evidencePath = path.join(changeDir, EVIDENCE_REL);
126
+ if (!fs.existsSync(evidencePath)) {
127
+ return {
128
+ pass: false,
129
+ failures: [
130
+ `${EVIDENCE_REL} is missing — run the bridging gate dry-run (expect FAIL = RED baseline) `
131
+ + 'and persist the raw output there.',
132
+ 'Required first line: ' + RED_BASELINE_LINE,
133
+ ],
134
+ };
135
+ }
136
+ const evidence = fs.readFileSync(evidencePath, 'utf-8');
137
+ const failures = [];
138
+ const evidenceLines = evidence.split(/\r?\n/);
139
+ const firstNonEmpty = evidenceLines.find((l) => l.trim().length > 0) || '';
140
+ if (firstNonEmpty.trim() !== RED_BASELINE_LINE) {
141
+ failures.push(
142
+ `evidence first line must be exactly "${RED_BASELINE_LINE}", got: "${firstNonEmpty.trim().slice(0, 80)}". `
143
+ + 'A gate that already PASSES means the baseline was not RED — record the expected-FAIL run instead.',
144
+ );
145
+ }
146
+
147
+ // 新鲜度 = hash 值比对(非 mtime):
148
+ // refresh-hash / state rebuild 均不重写契约文件,故 mtime 判据只会在「契约字节等价重生成」
149
+ // 或 git 恢复场景下误报;hash 值比对天然免疫。
150
+ const contractHash = computeContractHash(changeDir);
151
+ const hashMatch = evidence.match(/CONTRACT_HASH:\s*(sha256:[0-9a-f]{64})/);
152
+ if (!hashMatch) {
153
+ failures.push(
154
+ 'evidence must carry a "CONTRACT_HASH: sha256:<64 hex>" line so freshness is judged by content, not mtime.',
155
+ );
156
+ } else if (contractHash && hashMatch[1] !== contractHash) {
157
+ failures.push(
158
+ `evidence is stale: CONTRACT_HASH ${hashMatch[1].slice(0, 20)}… does not match current contract `
159
+ + `${String(contractHash).slice(0, 20)}… — re-run the dry-run against the current contract.`,
160
+ );
161
+ }
162
+
163
+ const missingIds = gateIds.filter((id) => {
164
+ const esc = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
165
+ return !new RegExp(`(^|[^\\w-])${esc}([^\\w-]|$)`).test(evidence);
166
+ });
167
+ if (missingIds.length > 0) {
168
+ failures.push(
169
+ `evidence does not cover declared gate id(s): ${missingIds.join(', ')} — every id in ${REGISTRY_HEADING} needs a dry-run record.`,
170
+ );
171
+ }
172
+
173
+ if (failures.length > 0) return { pass: false, failures };
174
+ return { pass: true, failures: [] };
175
+ }
@@ -16,6 +16,7 @@ import { checkExecutionReviewsPassed } from './checks/execution-reviews-passed.m
16
16
  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
+ import { checkGatesProbed } from './checks/gates-probed.mjs';
19
20
  import { checkArchReadiness } from './checks/arch-readiness.mjs';
20
21
  import { checkArchSnapshot } from './checks/arch-snapshot.mjs';
21
22
  import { checkArchMerged } from './checks/arch-merged.mjs';
@@ -39,7 +40,12 @@ const TRANSITION_CHECKS = {
39
40
  // 任何能写 dp_3_result 的主体(含夜间替身的 HOLD 落盘)都能让 change 呈现"已批准"假象,
40
41
  // 从而在无真实批准的前提下跨过 DP-3 硬门。本维度要求取值以 "approved" 开头
41
42
  // (判据实现见 checks/dp3-approved.mjs)。
42
- 'bridging:approved-for-build': ['artifacts-exist', 'schema-valid', 'contract-fresh', 'dp-gate-passed', 'dp3-approved'],
43
+ // v0.63.0(workflow-feedback 20260923-013114 S2):**gates-probed 新增**——契约的 G 类闸门
44
+ // 必须在 bridging 期跑过 dry-run(预期 FAIL = RED 基线)并留档,把 design.md R-7 长期存在的
45
+ // 「闸门已实测可跑」文字自证物化为机械证据。检查五件:evidence 存在 / 首行 RED 基线 /
46
+ // CONTRACT_HASH 值新鲜 / 契约 Gate Registry 的 id 逐 id 覆盖 / **段缺失即 FAIL(fail-closed)**。
47
+ // hotfix 走 WORKFLOW_TRANSITION_CHECKS 自有覆盖(下方),tweak 由 skip 回退阀放行。
48
+ 'bridging:approved-for-build': ['artifacts-exist', 'schema-valid', 'contract-fresh', 'dp-gate-passed', 'dp3-approved', 'gates-probed'],
43
49
  // v0.13 §49:test-matrix-ready 门禁前移——full 模式进入 executing 前强制测试准备度
44
50
  //(矩阵存在非空 OR 显式 skip 附理由;legacy 豁免)。hotfix/tweak 沿用 §45.3 豁免,不挂。
45
51
  'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed', 'execution-plan-ready', 'test-matrix-ready'],
@@ -51,7 +57,15 @@ const TRANSITION_CHECKS = {
51
57
  // 且 arch-merge 在状态机上无任何锚点(VALID_STATES 无 closed、closing→closed 不存在)。
52
58
  // B' 把 arch-merge 前移到本转换**之前**,本维度即其前置条件。
53
59
  // hotfix/tweak 沿用既有豁免口径(WORKFLOW_TRANSITION_CHECKS 各自列维度,天然不挂)。
54
- 'executing:closing': ['tasks-complete', 'tests-passing', 'specs-merged', 'execution-plan-ready', 'execution-reviews-passed', 'compound-captured', 'test-matrix-complete', 'arch-snapshot', 'delegation-status', 'arch-merged'],
60
+ // v0.63.0(feedback 20260923-013114 S3):**contract-fresh 新增**——S3「DP-3 后规划制品冻结」
61
+ // 的反查锚。原状 closing 无 contract-fresh,反查实际依赖 execution-plan-ready(validatePlan
62
+ // 比对 plan 内嵌的 artifacts_hash/contract_hash),而该比对可被 `tf execution refresh-hash`
63
+ // 一键刷平(refreshPlanHash 直接改写 plan JSON、revision 不升)→ 把 gate-affecting 变更
64
+ // 伪记为陈述性勘误后,closing 全维 PASS、wave receipt 亦不失效(wave_fingerprint 不含
65
+ // artifacts_hash)→ **伪绿静默通过**。本维度直接比对 state.artifacts_hash 与制品实算值,
66
+ // refresh-hash 无法清屏(它只改 plan JSON,不碰 state)——故为 refresh-hash 之外的独立锚。
67
+ // hotfix/tweak 不挂:二者跳过 spec-writer、无 planning 四件,冻结机制本身 N/A(设计 §3.9)。
68
+ 'executing:closing': ['contract-fresh', 'tasks-complete', 'tests-passing', 'specs-merged', 'execution-plan-ready', 'execution-reviews-passed', 'compound-captured', 'test-matrix-complete', 'arch-snapshot', 'delegation-status', 'arch-merged'],
55
69
 
56
70
  // Debugging side-path
57
71
  'executing:debugging': [],
@@ -200,6 +214,7 @@ async function main() {
200
214
  'compound-captured': (dir) => checkCompoundCaptured(dir),
201
215
  'test-matrix-complete': (dir) => checkTestMatrixComplete(dir),
202
216
  'test-matrix-ready': (dir) => checkTestMatrixReady(dir),
217
+ 'gates-probed': (dir) => checkGatesProbed(dir),
203
218
  'arch-readiness': (dir) => checkArchReadiness(dir),
204
219
  'arch-snapshot': (dir) => checkArchSnapshot(dir),
205
220
  'arch-merged': (dir) => checkArchMerged(dir),
@@ -1,7 +1,7 @@
1
1
  // tf config — display or modify configuration
2
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { loadConfig, getDefaults, resolveModelProfile } from './config-loader.mjs';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { loadConfig, getDefaults, resolveModelProfile, resolveConfigWritePath } from './config-loader.mjs';
5
5
 
6
6
  export async function run(args) {
7
7
  const config = loadConfig(process.cwd());
@@ -60,8 +60,10 @@ export async function run(args) {
60
60
  else if (/^\d+$/.test(rawValue)) value = parseInt(rawValue, 10);
61
61
  else value = rawValue;
62
62
 
63
- // Load existing config file or start from empty
64
- const configPath = join(process.cwd(), 'team-flow.config.json');
63
+ // v0.62.0(G5):写入路径改为与读取对齐(原固定写 `<cwd>/team-flow.config.json`,
64
+ // 而读取首选 `.team-flow/team-flow.config.json` 且首个命中即返回 → 静默无效)。
65
+ // 现在写 `findConfigFile()` 命中的文件;未命中则新建 `.team-flow/team-flow.config.json`。
66
+ const configPath = resolveConfigWritePath(process.cwd());
65
67
  let fileConfig = {};
66
68
  if (existsSync(configPath)) {
67
69
  fileConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
@@ -78,8 +80,10 @@ export async function run(args) {
78
80
  }
79
81
  target[parts[parts.length - 1]] = value;
80
82
 
83
+ mkdirSync(dirname(configPath), { recursive: true });
81
84
  writeFileSync(configPath, JSON.stringify(fileConfig, null, 2) + '\n');
82
85
  console.log(`Set ${path} = ${JSON.stringify(value)}`);
86
+ console.log(`Written to: ${configPath}`);
83
87
  return;
84
88
  }
85
89
 
@@ -0,0 +1,225 @@
1
+ // scripts/lib/cmd-prd.mjs — tf prd <sub>:PRD 模板一致性确认门(v0.62.0 · G6 / G1 / G2)
2
+ //
3
+ // 子命令分发照 `cmd-arch.mjs` / `cmd-runtime.mjs` 既有惯例。
4
+ //
5
+ // **为何需要原子命令(G6 的由来)**:
6
+ // ack 是「一组 hash + mode + 批准人/时间」的**复合状态**。用 `tf config --set` 逐字段写
7
+ // 既无原子性(中途失败即半写),也无法表达「一组 hash 对应一次批准」的语义;
8
+ // 且 `--set` 的值解析只认 bool/int/string,**不支持对象**(cmd-config.mjs:54-61)。
9
+ //
10
+ // **落点统一**:ack 一律写 `resolveConfigWritePath()` 命中的文件(= findConfigFile() 的读取结果),
11
+ // 消除 v1.5 的「落点三方不一」(G2 写 .team-flow/、G5 写命中文件、check-project-config 只读 .team-flow/)。
12
+ //
13
+ // 另提供 `tf prd check`:用共享模块 `template-hash.mjs` 比对 hash 与 ack,
14
+ // 供主会话 Phase 0.0 的阻塞提问(G1)取判据——**判据不在 skill 散文里另写一套**。
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { parseArgs } from 'node:util';
19
+ import { loadConfig, resolveConfigWritePath } from './config-loader.mjs';
20
+ import {
21
+ hashTemplateFile,
22
+ hashTemplatePair,
23
+ compareAgainstAck,
24
+ shortHash,
25
+ } from './template-hash.mjs';
26
+
27
+ const VALID_MODES = ['adopt_latest', 'keep_legacy'];
28
+
29
+ export async function run(args) {
30
+ const { positionals, values } = parseArgs({
31
+ args,
32
+ options: {
33
+ json: { type: 'string' },
34
+ mode: { type: 'string' },
35
+ 'in-use-hash': { type: 'string' },
36
+ 'plugin-hash': { type: 'string' },
37
+ 'spec-hash': { type: 'string' },
38
+ 'plugin-version': { type: 'string' },
39
+ 'diff-digest': { type: 'string' },
40
+ by: { type: 'string' },
41
+ 'plugin-root': { type: 'string' },
42
+ 'template-path': { type: 'string' },
43
+ 'spec-path': { type: 'string' },
44
+ },
45
+ allowPositionals: true,
46
+ });
47
+
48
+ const sub = positionals[0];
49
+ if (sub === 'ack') return ack(positionals.slice(1), values);
50
+ if (sub === 'check') return check(values);
51
+ usage();
52
+ }
53
+
54
+ function usage() {
55
+ console.error(
56
+ 'Usage:\n' +
57
+ ' tf prd ack set --json \'<ack object>\' # 原子写入完整 ack 对象\n' +
58
+ ' tf prd ack set --mode adopt_latest --in-use-hash … [--plugin-hash …] [--spec-hash …]\n' +
59
+ ' tf prd ack show # 查看当前 ack\n' +
60
+ ' tf prd ack clear # 清除 ack(下次将重新提问)\n' +
61
+ ' tf prd check [--template-path <p>] [--plugin-root <p>] # 比对 hash 与 ack,输出 STATUS'
62
+ );
63
+ process.exit(2);
64
+ }
65
+
66
+ /** 读原始配置文件(未与 DEFAULTS 合并),用于区分「用户显式配置」与「默认值」。 */
67
+ function readRawConfig() {
68
+ const p = resolveConfigWritePath(process.cwd());
69
+ if (!p || !fs.existsSync(p)) return {};
70
+ try {
71
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 解析「实际使用的模板」路径。
79
+ *
80
+ * F2 语义:未配置 `prd.template` → 插件内置;已配置 → 用户路径(相对则按项目根解析)。
81
+ * 因此必须读原始配置区分二者,不能用合并后的 loadConfig 结果判断。
82
+ */
83
+ function resolveInUseTemplatePath(pluginRoot, explicit) {
84
+ if (explicit) {
85
+ return path.isAbsolute(explicit) ? explicit : path.join(process.cwd(), explicit);
86
+ }
87
+ const raw = readRawConfig();
88
+ const configured = raw?.prd?.template;
89
+ if (configured) {
90
+ return path.isAbsolute(configured) ? configured : path.join(process.cwd(), configured);
91
+ }
92
+ // 默认 = 插件内置
93
+ return pluginRoot ? path.join(pluginRoot, 'templates', 'prd.md') : null;
94
+ }
95
+
96
+ function resolvePluginRoot(values) {
97
+ return values['plugin-root'] || process.env.CLAUDE_PLUGIN_ROOT || null;
98
+ }
99
+
100
+ function ack(subArgs, values) {
101
+ const verb = subArgs[0];
102
+
103
+ if (verb === 'show') {
104
+ const cfg = loadConfig(process.cwd());
105
+ console.log(JSON.stringify(cfg.prd?.template_ack ?? {}, null, 2));
106
+ return;
107
+ }
108
+
109
+ if (verb === 'clear') {
110
+ const configPath = resolveConfigWritePath(process.cwd());
111
+ let fileConfig = {};
112
+ if (fs.existsSync(configPath)) fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
113
+ if (fileConfig.prd) fileConfig.prd.template_ack = {};
114
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
115
+ fs.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2) + '\n');
116
+ console.log('ack cleared');
117
+ console.log(`Written to: ${configPath}`);
118
+ return;
119
+ }
120
+
121
+ if (verb !== 'set') usage();
122
+
123
+ // 组装 ack 对象:--json 优先(整体替换),否则逐字段
124
+ let patch = {};
125
+ if (values.json) {
126
+ try {
127
+ patch = JSON.parse(values.json);
128
+ } catch {
129
+ console.error('Invalid --json: not valid JSON');
130
+ process.exit(2);
131
+ }
132
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
133
+ console.error('Invalid --json: expected an object');
134
+ process.exit(2);
135
+ }
136
+ } else {
137
+ if (values.mode) patch.mode = values.mode;
138
+ if (values['in-use-hash']) patch.in_use_hash = values['in-use-hash'];
139
+ if (values['plugin-hash']) patch.plugin_hash = values['plugin-hash'];
140
+ if (values['spec-hash']) patch.spec_hash = values['spec-hash'];
141
+ if (values['plugin-version']) patch.plugin_version = values['plugin-version'];
142
+ if (values['diff-digest']) patch.diff_digest = values['diff-digest'];
143
+ }
144
+
145
+ if (patch.mode && !VALID_MODES.includes(patch.mode)) {
146
+ console.error(`Invalid mode: ${patch.mode} (expected one of ${VALID_MODES.join(' | ')})`);
147
+ process.exit(2);
148
+ }
149
+ if (!values.json && !patch.mode) {
150
+ console.error('Missing --mode (required for field-wise set)');
151
+ process.exit(2);
152
+ }
153
+
154
+ // 补充留痕字段(谁 / 何时)
155
+ if (!patch.acknowledged_at) patch.acknowledged_at = new Date().toISOString();
156
+ if (!patch.acknowledged_by) patch.acknowledged_by = values.by || null;
157
+
158
+ // 原子写入:整块替换 prd.template_ack,一次 writeFileSync
159
+ const configPath = resolveConfigWritePath(process.cwd());
160
+ let fileConfig = {};
161
+ if (fs.existsSync(configPath)) fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
162
+ fileConfig.prd = fileConfig.prd || {};
163
+ fileConfig.prd.template_ack = patch;
164
+
165
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
166
+ fs.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2) + '\n');
167
+ console.log('ack written');
168
+ console.log(`Written to: ${configPath}`);
169
+ return;
170
+ }
171
+
172
+ /**
173
+ * `tf prd check`:比对「实际使用模板 / 插件内置模板 / 规范文件」三个 hash 与 ack。
174
+ *
175
+ * 输出末行固定为 `STATUS: MATCH | MISMATCH | NO_ACK | UNRESOLVED`,供主会话 G1 取判据。
176
+ * **exit code 恒为 0**——本命令是判据工具,不是门禁;是否阻塞由主会话决定(G1)。
177
+ */
178
+ function check(values) {
179
+ const pluginRoot = resolvePluginRoot(values);
180
+ const pluginTemplate = pluginRoot
181
+ ? path.join(pluginRoot, 'templates', 'prd.md')
182
+ : null;
183
+ const specPath =
184
+ values['spec-path'] ||
185
+ (pluginRoot
186
+ ? path.join(pluginRoot, 'skills', 'ce-brainstorm', 'references', 'prd-84-authoring-spec.md')
187
+ : null);
188
+
189
+ const inUsePath = resolveInUseTemplatePath(pluginRoot, values['template-path']);
190
+
191
+ const current = {
192
+ template_hash: hashTemplateFile(inUsePath),
193
+ spec_hash: hashTemplateFile(specPath),
194
+ plugin_hash: hashTemplateFile(pluginTemplate),
195
+ };
196
+
197
+ const cfg = loadConfig(process.cwd());
198
+ const ackObj = cfg.prd?.template_ack || {};
199
+
200
+ console.log('in_use :', inUsePath ?? '(unresolved)');
201
+ console.log(' :', shortHash(current.template_hash));
202
+ console.log('plugin :', pluginTemplate ?? '(unresolved)');
203
+ console.log(' :', shortHash(current.plugin_hash));
204
+ console.log('spec :', specPath ?? '(unresolved)');
205
+ console.log(' :', shortHash(current.spec_hash));
206
+ console.log('ack :', JSON.stringify({
207
+ in_use_hash: ackObj.in_use_hash ? shortHash(ackObj.in_use_hash) : null,
208
+ plugin_hash: ackObj.plugin_hash ? shortHash(ackObj.plugin_hash) : null,
209
+ spec_hash: ackObj.spec_hash ? shortHash(ackObj.spec_hash) : null,
210
+ mode: ackObj.mode ?? null,
211
+ }));
212
+
213
+ if (!inUsePath && !pluginTemplate) {
214
+ console.log('STATUS: UNRESOLVED');
215
+ return;
216
+ }
217
+ if (!ackObj.in_use_hash && !ackObj.plugin_hash && !ackObj.spec_hash) {
218
+ console.log('STATUS: NO_ACK');
219
+ return;
220
+ }
221
+
222
+ const { matched, changed } = compareAgainstAck(current, ackObj);
223
+ console.log('changed :', changed.length ? changed.join(', ') : '(none)');
224
+ console.log(`STATUS: ${matched ? 'MATCH' : 'MISMATCH'}`);
225
+ }
@@ -45,6 +45,10 @@ const SETTABLE_FIELDS = [
45
45
  'tasks_skipped', 'tasks_skip_reason',
46
46
  // Arch merge gate (v0.53.0 §110.2:arch-merged guard 维度的显式跳过键,须附理由)
47
47
  'arch_merge_skipped', 'arch_merge_skip_reason',
48
+ // Bridging gates dry-run gate (v0.63.0;feedback 20260923-013114 S2)
49
+ // 三键必须与 state-loader BUILTIN_DEFAULTS + writeState 序列化分支同步注册,
50
+ // 缺任一环即「回显成功却零写入」(v0.59.0 同型教训,见本文件 :20-24 注释)。
51
+ 'gates_probed_skipped', 'gates_probed_skip_reason', 'gates_probed_na',
48
52
  ];
49
53
 
50
54
  export async function run(args) {
@@ -34,7 +34,9 @@ const TEXT_FILES = [
34
34
  // v0.60.0:补 usage-guide 的**同步面**——v0.59.0 只把它纳入检出(check-version-consistency.mjs),
35
35
  // 漏了本列表,导致升版时「package.json 已改但 usage-guide 未同步 → 检出失败 → npm version 非 0 退出」
36
36
  // 的脏状态。检出与同步现已同源(两处 pattern 对应同一锚点)。
37
- { file: 'docs/usage-guide.md', pattern: /(版本锚点:v)\d+\.\d+\.\d+/g, replacement: '$1%MAJOR%.%MINOR%.%PATCH%' },
37
+ // v0.62.0:文件名同步——文档已更名为「team-flow 使用说明(研发团队版).md」,
38
+ // check-version-consistency.mjs 已改指新名,本处漏改导致升版时该文件被 skip(版本锚点留在旧值 → 复检失败)。
39
+ { file: 'docs/team-flow 使用说明(研发团队版).md', pattern: /(版本锚点:v)\d+\.\d+\.\d+/g, replacement: '$1%MAJOR%.%MINOR%.%PATCH%' },
38
40
  { file: 'skills/workflow-start/SKILL.md', pattern: /(npx --yes --package @xulthekl\/team-flow@)\d+\.\d+\.\d+( tf)/g, replacement: '$1%MAJOR%.%MINOR%.%PATCH%$2' },
39
41
  { file: 'skills/need-explorer/SKILL.md', pattern: /(npx --yes --package @xulthekl\/team-flow@)\d+\.\d+\.\d+( tf)/g, replacement: '$1%MAJOR%.%MINOR%.%PATCH%$2' },
40
42
  { file: 'skills/spec-writer/SKILL.md', pattern: /(npx --yes --package @xulthekl\/team-flow@)\d+\.\d+\.\d+( tf)/g, replacement: '$1%MAJOR%.%MINOR%.%PATCH%$2' },