@xulthekl/team-flow 0.60.0 → 0.62.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.
- package/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +6 -4
- package/CHANGELOG.md +33 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +2 -2
- package/agents/change-split-auditor.md +1 -0
- package/agents/prd-completeness-reviewer.md +49 -11
- package/agents/prd-writer.md +47 -13
- package/docs/README_en.md +1 -1
- package/docs/decision-points.md +25 -0
- package/docs/plans/2026-09-21-001-three-optimization-eval.md +127 -0
- package/docs/{usage-guide.md → 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" } +234 -104
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/check-project-config.mjs +52 -1
- package/scripts/check-version-consistency.mjs +2 -2
- package/scripts/lib/cmd-config.mjs +9 -5
- package/scripts/lib/cmd-prd.mjs +225 -0
- package/scripts/lib/cmd-version.mjs +3 -1
- package/scripts/lib/config-loader.mjs +39 -0
- package/scripts/lib/template-hash.mjs +95 -0
- package/scripts/team-flow.mjs +1 -0
- package/skills/bug-investigator/SKILL.md +8 -0
- package/skills/ce-brainstorm/SKILL.md +91 -27
- package/skills/ce-brainstorm/references/brainstorm-sections.md +26 -8
- package/skills/ce-brainstorm/references/evidence-chain-validation.md +1 -1
- package/skills/ce-brainstorm/references/prd-84-authoring-spec.md +182 -0
- package/skills/ce-brainstorm/references/prd-mapping.md +9 -4
- package/skills/ce-brainstorm/references/prototype-loop.md +2 -2
- package/skills/ce-plan/references/change-splitting.md +12 -0
- package/skills/prototype/references/orchestration-flow.md +1 -1
- package/skills/workflow-orchestrator/SKILL.md +13 -1
- package/skills/workflow-orchestrator/references/feedback-loops.md +10 -5
- package/skills/workflow-orchestrator/references/s1-path-router.md +7 -1
- package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +3 -3
- package/skills/workflow-orchestrator/references/s4-split-validate.md +1 -0
- package/skills/workflow-orchestrator/references/state-model.md +1 -1
- package/skills/workflow-start/SKILL.md +13 -0
- package/templates/prd-brainstorm-profile.md +9 -3
- package/templates/prd.md +76 -49
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
3
|
"description": "Unified workflow plugin: team-flow (spec-driven dev) + compound-engineering core subset + architecture-design (4A/DDD) + prototype (local HTML) + business-analysis (independent requirement/scenario artifact) + jarvis (team-flow decision agent, opt-in). 28 skills, one install.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.62.0",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/hooks/session-start
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# v0.
|
|
2
|
+
# v0.62.0: auto-sync CLI version with plugin version
|
|
3
3
|
set -e
|
|
4
4
|
|
|
5
5
|
# ═══════════════════════════════════════════════════════════════
|
|
6
6
|
# Plugin version (update this when releasing new versions)
|
|
7
7
|
# ═══════════════════════════════════════════════════════════════
|
|
8
|
-
PLUGIN_VERSION="0.
|
|
8
|
+
PLUGIN_VERSION="0.62.0"
|
|
9
9
|
|
|
10
10
|
# ═══════════════════════════════════════════════════════════════
|
|
11
11
|
# Step 1: Auto-sync CLI version with plugin version
|
package/llms.txt
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Overview
|
|
4
4
|
spec-superflow is a self-contained workflow integration plugin for Claude Code, Cursor, OpenAI Codex CLI/App, GitHub Copilot CLI, Gemini CLI, OpenCode, WorkBuddy, and Trae. It merges spec-driven planning artifacts (proposal, specs, design, tasks) with disciplined execution guardrails (TDD, review gates, controlled handoff) into one unified workflow.
|
|
5
5
|
|
|
6
|
-
Current version: v0.
|
|
6
|
+
Current version: v0.62.0.
|
|
7
7
|
|
|
8
8
|
## Key Documents
|
|
9
9
|
- README.md: Chinese homepage with full usage guide and FAQ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xulthekl/team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "Unified plugin (28 skills + 17 agents) integrating team-flow, compound-engineering, architecture-design, prototype, design-system, workflow-orchestrator, workflow-bootstrap, e2e, session-handoff, workflow-feedback, business-analysis for multi-agent coding tools.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.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
|
-
|
|
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);
|
|
@@ -73,7 +73,7 @@ const TEXT_CHECKS = [
|
|
|
73
73
|
{ file: 'GEMINI.md', extract: /# team-flow v(\d+\.\d+\.\d+) \|/ },
|
|
74
74
|
// v0.59.0:新增 usage-guide 版本锚点(此前不在扫描面内,0.58.0 与 0.59.0 两次发版均靠人工改;
|
|
75
75
|
// P3-15 ② 半项闭合——**检出**已覆盖,本段仍无 --fix 分支,自动修复待做)
|
|
76
|
-
{ file: 'docs/
|
|
76
|
+
{ file: 'docs/team-flow 使用说明(研发团队版).md', extract: /版本锚点:v(\d+\.\d+\.\d+)/ },
|
|
77
77
|
];
|
|
78
78
|
|
|
79
79
|
for (const check of TEXT_CHECKS) {
|
|
@@ -165,7 +165,7 @@ for (const check of SKILL_COUNT_JSON_CHECKS) {
|
|
|
165
165
|
const SKILL_COUNT_TEXT_CHECKS = [
|
|
166
166
|
{ file: 'README.md', extract: /(\d+) skills/, replace: (content, count) => content.replace(/(\d+) skills/, `${count} skills`) },
|
|
167
167
|
{ file: 'AGENTS.md', extract: /Skills 索引((\d+) 个)/, replace: (content, count) => content.replace(/Skills 索引((\d+) 个)/, `Skills 索引(${count} 个)`) },
|
|
168
|
-
{ file: 'docs/
|
|
168
|
+
{ file: 'docs/team-flow 使用说明(研发团队版).md', extract: /(\d+) 个 skills/, replace: (content, count) => content.replace(/(\d+) 个 skills/, `${count} 个 skills`) },
|
|
169
169
|
{ file: 'docs/platform-matrix.md', extract: /(\d+) 个 skill 部署/, replace: (content, count) => content.replace(/(\d+) 个 skill 部署/, `${count} 个 skill 部署`) },
|
|
170
170
|
{ file: 'INSTALL.md', extract: /应有 (\d+) 个 skill 目录/, replace: (content, count) => content.replace(/(\d+) 个 skill 目录/g, `${count} 个 skill 目录`) },
|
|
171
171
|
{ file: 'INSTALL.md', extract: /包含 (\d+) 个 skill、/, replace: (content, count) => content.replace(/(\d+) 个 skill、/, `${count} 个 skill、`) },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tf config — display or modify configuration
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
-
import {
|
|
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
|
-
//
|
|
64
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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' },
|
|
@@ -21,7 +21,25 @@ const DEFAULTS = {
|
|
|
21
21
|
language: 'auto',
|
|
22
22
|
},
|
|
23
23
|
prd: {
|
|
24
|
+
// 模板路径。默认值为 **插件内置** 相对路径 —— 一律以**插件根**为基准解析,
|
|
25
|
+
// 项目根下的同名路径不构成回退目标(v0.62.0 F2,消除默认值基准歧义)。
|
|
24
26
|
template: 'templates/prd.md',
|
|
27
|
+
// §8.4 撰写规范文件(v0.62.0 F1)。规范与体例解耦后,权威物是它而非模板骨架。
|
|
28
|
+
// 路径相对于插件根解析。
|
|
29
|
+
spec: 'skills/ce-brainstorm/references/prd-84-authoring-spec.md',
|
|
30
|
+
// 模板一致性确认记忆(v0.62.0 G2)。
|
|
31
|
+
// 由 `tf prd ack set` 原子写入(G6),落点 = resolveConfigWritePath() 命中的文件。
|
|
32
|
+
// 判据:三个 hash 全等 → 静默按 mode 执行;任一变化 → 重新提问(上一选择作默认项)。
|
|
33
|
+
template_ack: {
|
|
34
|
+
in_use_hash: null, // 实际使用的模板骨架 hash
|
|
35
|
+
plugin_hash: null, // 插件内置模板骨架 hash
|
|
36
|
+
spec_hash: null, // 规范文件 hash —— v0.62.0 新增:规范升级须触发重问
|
|
37
|
+
plugin_version: null,
|
|
38
|
+
mode: null, // 'adopt_latest' | 'keep_legacy'
|
|
39
|
+
diff_digest: null, // 批准时所见差异摘要的摘要值 —— 否则事后无法复现"批了什么"
|
|
40
|
+
acknowledged_at: null,
|
|
41
|
+
acknowledged_by: null,
|
|
42
|
+
},
|
|
25
43
|
},
|
|
26
44
|
conventions: {
|
|
27
45
|
// 项目级规范文件路径映射(v0.11 §33)
|
|
@@ -129,6 +147,27 @@ function findConfigFile(startDir) {
|
|
|
129
147
|
return null;
|
|
130
148
|
}
|
|
131
149
|
|
|
150
|
+
/**
|
|
151
|
+
* v0.62.0(G5):**写入路径与读取路径对齐**。
|
|
152
|
+
*
|
|
153
|
+
* 缺陷:`tf config --set` 原固定写 `<cwd>/team-flow.config.json`(legacy 第 2 顺位),
|
|
154
|
+
* 而读取走 `findConfigFile`,其**首选** `<cwd>/.team-flow/team-flow.config.json`
|
|
155
|
+
* 且**首个命中即返回、不合并** → 在已有 `.team-flow/` 配置的项目里 `--set`
|
|
156
|
+
* **静默无效**(写进一个没人读的文件),并在项目根留下误导性配置。
|
|
157
|
+
*
|
|
158
|
+
* 改法:写入一律落到 `findConfigFile()` 命中的那个文件;未命中则新建
|
|
159
|
+
* `.team-flow/team-flow.config.json`(与读取首选一致)。G6 的 ack 写入复用同一函数。
|
|
160
|
+
*
|
|
161
|
+
* @param {string} startDir
|
|
162
|
+
* @returns {string} 应写入的配置文件绝对路径
|
|
163
|
+
*/
|
|
164
|
+
export function resolveConfigWritePath(startDir) {
|
|
165
|
+
const hit = findConfigFile(startDir);
|
|
166
|
+
if (hit) return hit;
|
|
167
|
+
// 未命中 → 用读取的首选位置新建,避免"写了但读不到"
|
|
168
|
+
return join(startDir, '.team-flow', 'team-flow.config.json');
|
|
169
|
+
}
|
|
170
|
+
|
|
132
171
|
export function loadConfig(projectRoot) {
|
|
133
172
|
const configPath = findConfigFile(projectRoot || process.cwd());
|
|
134
173
|
if (!configPath) return { ...DEFAULTS };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// scripts/lib/template-hash.mjs — 模板 / 规范文件的稳定 hash(v0.62.0,F5)
|
|
2
|
+
//
|
|
3
|
+
// 存在理由:改造前 **F3(Node 侧机械脚本)与 G1(skill 散文侧人工比对)各有一套
|
|
4
|
+
// hash 判据**。同一文件在两处算出不同结果(换行、尾空白、编码差异)会导致
|
|
5
|
+
// 「门失效」(该拦的没拦)或「误报落后」(不该问的乱问)。本模块把判据收敛为
|
|
6
|
+
// **唯一实现**,两侧一律调用,不另立第二套。
|
|
7
|
+
//
|
|
8
|
+
// 判据定义(v1.7 §5.7 F5):
|
|
9
|
+
// ① 算法 sha256,前缀 `sha256:`(与 `hash.mjs` 既有惯例一致)
|
|
10
|
+
// ② 颗粒度 = 整文件字节;**先做换行归一化(CRLF→LF)与首尾空白裁剪**
|
|
11
|
+
// ③ 输出:全长用于比对,`shortHash()`(前 8 位)仅用于展示
|
|
12
|
+
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 归一化:CRLF → LF,并裁剪首尾空白。
|
|
18
|
+
*
|
|
19
|
+
* 只做这两件事,不做更激进的归一化(不折叠空行、不统一缩进)——
|
|
20
|
+
* 内容级差异应当反映到 hash 上,否则"改了却没变 hash",门就形同虚设。
|
|
21
|
+
*
|
|
22
|
+
* @param {string} content
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeForHash(content) {
|
|
26
|
+
return (content ?? '').replace(/\r\n/g, '\n').trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 对内容取稳定 hash。空内容返回 null(不是空串 hash)——
|
|
31
|
+
* 「文件为空/不存在」是独立状态,不应与「内容为空串」混淆。
|
|
32
|
+
*
|
|
33
|
+
* @param {string} content
|
|
34
|
+
* @returns {string|null} `sha256:<hex>` 或 null
|
|
35
|
+
*/
|
|
36
|
+
export function hashContent(content) {
|
|
37
|
+
const normalized = normalizeForHash(content);
|
|
38
|
+
if (normalized.length === 0) return null;
|
|
39
|
+
return `sha256:${crypto.createHash('sha256').update(normalized, 'utf-8').digest('hex')}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 对文件取稳定 hash(文件不存在返回 null)。
|
|
44
|
+
*
|
|
45
|
+
* @param {string} filePath
|
|
46
|
+
* @returns {string|null}
|
|
47
|
+
*/
|
|
48
|
+
export function hashTemplateFile(filePath) {
|
|
49
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
50
|
+
return hashContent(fs.readFileSync(filePath, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 展示用短标识(默认前 8 位,去掉 `sha256:` 前缀)。
|
|
55
|
+
*
|
|
56
|
+
* @param {string|null} hash
|
|
57
|
+
* @param {number} [len=8]
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function shortHash(hash, len = 8) {
|
|
61
|
+
if (!hash) return '—';
|
|
62
|
+
const hex = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
63
|
+
return hex.slice(0, len);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 一次算出「模板骨架 + 规范文件」两个 hash(G1 判据对象,v1.7 DEC-16)。
|
|
68
|
+
*
|
|
69
|
+
* F 类-F1 之后,**权威物是规范文件而非模板骨架**,故判据必须含 spec_hash;
|
|
70
|
+
* 只比骨架会导致「规范升级不触发重问」——正是红队批评的「门管错了对象」。
|
|
71
|
+
*
|
|
72
|
+
* @param {{ templatePath?: string, specPath?: string }} paths
|
|
73
|
+
* @returns {{ template_hash: string|null, spec_hash: string|null }}
|
|
74
|
+
*/
|
|
75
|
+
export function hashTemplatePair(paths = {}) {
|
|
76
|
+
return {
|
|
77
|
+
template_hash: hashTemplateFile(paths.templatePath),
|
|
78
|
+
spec_hash: hashTemplateFile(paths.specPath),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 比对两组 hash(G2 ack 失效判定)。
|
|
84
|
+
*
|
|
85
|
+
* @param {{ template_hash?: string|null, spec_hash?: string|null }} current
|
|
86
|
+
* @param {{ in_use_hash?: string, spec_hash?: string, plugin_hash?: string }} ack
|
|
87
|
+
* @returns {{ matched: boolean, changed: string[] }}
|
|
88
|
+
*/
|
|
89
|
+
export function compareAgainstAck(current, ack = {}) {
|
|
90
|
+
const changed = [];
|
|
91
|
+
// ack 字段名沿用 config schema:in_use_hash 记录的是"项目侧实际使用"的骨架 hash
|
|
92
|
+
if ((current.template_hash ?? null) !== (ack.in_use_hash ?? null)) changed.push('in_use_hash');
|
|
93
|
+
if ((current.spec_hash ?? null) !== (ack.spec_hash ?? null)) changed.push('spec_hash');
|
|
94
|
+
return { matched: changed.length === 0, changed };
|
|
95
|
+
}
|
package/scripts/team-flow.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const COMMANDS = {
|
|
|
11
11
|
version: () => import('./lib/cmd-version.mjs'),
|
|
12
12
|
sync: () => import('./lib/cmd-sync.mjs'),
|
|
13
13
|
config: () => import('./lib/cmd-config.mjs'),
|
|
14
|
+
prd: () => import('./lib/cmd-prd.mjs'),
|
|
14
15
|
state: () => import('./lib/cmd-state.mjs'),
|
|
15
16
|
inject: () => import('./lib/cmd-inject.mjs'),
|
|
16
17
|
audit: () => import('./lib/cmd-audit.mjs'),
|
|
@@ -92,6 +92,14 @@ Write the investigation report with this structure:
|
|
|
92
92
|
- **Mechanism:** [How the bug works, step by step]
|
|
93
93
|
- **Evidence:** [What proves this is the cause]
|
|
94
94
|
|
|
95
|
+
## 源 Change 关联(v0.61.0 新增,设计依据 v0.26 §203)
|
|
96
|
+
|
|
97
|
+
- **是否匹配到源 change**:是 / 否
|
|
98
|
+
- **源 change 目录**:`<changes/<name>/>`(若匹配到;否则填「独立缺陷 / 新文件,无源 change」)
|
|
99
|
+
- **匹配方式**:`git blame` 受影响文件/符号 → 引入 commit → 经 `change_dag` / `change-brief` / `learnings.md` 反查;或全局 `specs/` 按 capability 检索
|
|
100
|
+
- **复用约束**:修复时复用源 change 的 `design.md` / `specs/` / `tests` 作为已知设计约束,避免偏离原设计意图;若源 change 已 closing,仅在本 bugfix change 内复用上下文,**不重开**源 change
|
|
101
|
+
- **用户确认**:匹配结果须由用户确认;失准时本段填「匹配失败,按独立缺陷处理」
|
|
102
|
+
|
|
95
103
|
## Recommended Fix
|
|
96
104
|
[Suggested approach — describe what to change and why, but do NOT implement it]
|
|
97
105
|
- **Fix location:** [where to change]
|