@michaelxwb/code-flow 0.5.1

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 (99) hide show
  1. package/README.md +157 -0
  2. package/package.json +16 -0
  3. package/src/adapters/claude/CLAUDE.md +51 -0
  4. package/src/adapters/claude/commands/cf-init.md +262 -0
  5. package/src/adapters/claude/commands/cf-inject.md +55 -0
  6. package/src/adapters/claude/commands/cf-learn.md +291 -0
  7. package/src/adapters/claude/commands/cf-stats.md +86 -0
  8. package/src/adapters/claude/commands/cf-task/align.md +283 -0
  9. package/src/adapters/claude/commands/cf-task/archive.md +131 -0
  10. package/src/adapters/claude/commands/cf-task/block.md +30 -0
  11. package/src/adapters/claude/commands/cf-task/graph.md +73 -0
  12. package/src/adapters/claude/commands/cf-task/note.md +239 -0
  13. package/src/adapters/claude/commands/cf-task/plan.md +270 -0
  14. package/src/adapters/claude/commands/cf-task/prd.md +138 -0
  15. package/src/adapters/claude/commands/cf-task/start.md +173 -0
  16. package/src/adapters/claude/commands/cf-task/status.md +59 -0
  17. package/src/adapters/claude/commands/cf-validate.md +73 -0
  18. package/src/adapters/claude/settings.local.json +60 -0
  19. package/src/adapters/codex/AGENTS.md +51 -0
  20. package/src/adapters/codex/config.toml +3 -0
  21. package/src/adapters/codex/hooks.json +48 -0
  22. package/src/adapters/codex/skills/cf-init/SKILL.md +271 -0
  23. package/src/adapters/codex/skills/cf-inject/SKILL.md +56 -0
  24. package/src/adapters/codex/skills/cf-learn/SKILL.md +296 -0
  25. package/src/adapters/codex/skills/cf-stats/SKILL.md +87 -0
  26. package/src/adapters/codex/skills/cf-task-align/SKILL.md +284 -0
  27. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +132 -0
  28. package/src/adapters/codex/skills/cf-task-block/SKILL.md +31 -0
  29. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +74 -0
  30. package/src/adapters/codex/skills/cf-task-note/SKILL.md +236 -0
  31. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +271 -0
  32. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +139 -0
  33. package/src/adapters/codex/skills/cf-task-start/SKILL.md +174 -0
  34. package/src/adapters/codex/skills/cf-task-status/SKILL.md +60 -0
  35. package/src/adapters/codex/skills/cf-validate/SKILL.md +74 -0
  36. package/src/adapters/costrict/CLAUDE.md +51 -0
  37. package/src/adapters/costrict/commands/cf-init.md +262 -0
  38. package/src/adapters/costrict/commands/cf-inject.md +55 -0
  39. package/src/adapters/costrict/commands/cf-learn.md +291 -0
  40. package/src/adapters/costrict/commands/cf-stats.md +86 -0
  41. package/src/adapters/costrict/commands/cf-task/align.md +283 -0
  42. package/src/adapters/costrict/commands/cf-task/archive.md +131 -0
  43. package/src/adapters/costrict/commands/cf-task/block.md +30 -0
  44. package/src/adapters/costrict/commands/cf-task/graph.md +73 -0
  45. package/src/adapters/costrict/commands/cf-task/note.md +239 -0
  46. package/src/adapters/costrict/commands/cf-task/plan.md +270 -0
  47. package/src/adapters/costrict/commands/cf-task/prd.md +138 -0
  48. package/src/adapters/costrict/commands/cf-task/start.md +173 -0
  49. package/src/adapters/costrict/commands/cf-task/status.md +59 -0
  50. package/src/adapters/costrict/commands/cf-validate.md +73 -0
  51. package/src/adapters/costrict/settings.local.json +60 -0
  52. package/src/adapters/opencode/AGENTS.md +51 -0
  53. package/src/adapters/opencode/commands/cf-init.md +275 -0
  54. package/src/adapters/opencode/commands/cf-inject.md +59 -0
  55. package/src/adapters/opencode/commands/cf-learn.md +295 -0
  56. package/src/adapters/opencode/commands/cf-stats.md +90 -0
  57. package/src/adapters/opencode/commands/cf-task/align.md +287 -0
  58. package/src/adapters/opencode/commands/cf-task/archive.md +135 -0
  59. package/src/adapters/opencode/commands/cf-task/block.md +34 -0
  60. package/src/adapters/opencode/commands/cf-task/graph.md +77 -0
  61. package/src/adapters/opencode/commands/cf-task/note.md +243 -0
  62. package/src/adapters/opencode/commands/cf-task/plan.md +274 -0
  63. package/src/adapters/opencode/commands/cf-task/prd.md +142 -0
  64. package/src/adapters/opencode/commands/cf-task/start.md +177 -0
  65. package/src/adapters/opencode/commands/cf-task/status.md +63 -0
  66. package/src/adapters/opencode/commands/cf-validate.md +77 -0
  67. package/src/adapters/opencode/opencode.json +4 -0
  68. package/src/adapters/opencode/plugins/code-flow/index.js +132 -0
  69. package/src/adapters/opencode/plugins/code-flow/package.json +6 -0
  70. package/src/cli.js +810 -0
  71. package/src/core/code-flow/config.yml +159 -0
  72. package/src/core/code-flow/scripts/cf_checks.py +306 -0
  73. package/src/core/code-flow/scripts/cf_core.py +954 -0
  74. package/src/core/code-flow/scripts/cf_feedback.py +56 -0
  75. package/src/core/code-flow/scripts/cf_inject_hook.py +178 -0
  76. package/src/core/code-flow/scripts/cf_log.py +146 -0
  77. package/src/core/code-flow/scripts/cf_post_hook.py +196 -0
  78. package/src/core/code-flow/scripts/cf_scan.py +293 -0
  79. package/src/core/code-flow/scripts/cf_session_hook.py +42 -0
  80. package/src/core/code-flow/scripts/cf_stats.py +439 -0
  81. package/src/core/code-flow/scripts/cf_stop_hook.py +191 -0
  82. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +402 -0
  83. package/src/core/code-flow/specs/backend/_map.md +56 -0
  84. package/src/core/code-flow/specs/backend/code-quality-performance.md +47 -0
  85. package/src/core/code-flow/specs/backend/database.md +39 -0
  86. package/src/core/code-flow/specs/backend/directory-structure.md +42 -0
  87. package/src/core/code-flow/specs/backend/logging.md +38 -0
  88. package/src/core/code-flow/specs/backend/platform-rules.md +43 -0
  89. package/src/core/code-flow/specs/frontend/_map.md +52 -0
  90. package/src/core/code-flow/specs/frontend/component-specs.md +56 -0
  91. package/src/core/code-flow/specs/frontend/directory-structure.md +41 -0
  92. package/src/core/code-flow/specs/frontend/quality-standards.md +46 -0
  93. package/src/core/code-flow/specs/shared/_map.md +57 -0
  94. package/src/core/code-flow/specs/shared/design/design-frontend.md +247 -0
  95. package/src/core/code-flow/specs/shared/design/design-full.md +494 -0
  96. package/src/core/code-flow/specs/shared/design/design-lite.md +293 -0
  97. package/src/core/code-flow/specs/shared/prd-template.md +187 -0
  98. package/src/core/code-flow/tasks/.gitkeep +0 -0
  99. package/src/core/code-flow/validation.yml +48 -0
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+
7
+ import cf_log
8
+ from cf_checks import load_check_state, parse_spec_checks
9
+ from cf_core import (
10
+ build_effective_mapping,
11
+ build_spec_catalog,
12
+ estimate_tokens,
13
+ load_config,
14
+ non_injectable_specs,
15
+ parse_spec_frontmatter,
16
+ )
17
+
18
+ REVIEW_MIN_COVERAGE_DAYS = 7
19
+
20
+
21
+ def _log_coverage_sufficient(events: list) -> bool:
22
+ """"未命中"信号需要足够的日志覆盖期——刚部署一天的日志没资格断言
23
+ "30 天未命中"(实测误伤:单条事件就把全部 spec 标为待复审)。"""
24
+ from datetime import datetime, timedelta
25
+ earliest = min((str(e.get("ts", "")) for e in events), default="")
26
+ try:
27
+ first = datetime.fromisoformat(earliest)
28
+ except ValueError:
29
+ return False
30
+ return first <= datetime.now() - timedelta(days=REVIEW_MIN_COVERAGE_DAYS)
31
+
32
+
33
+ def build_review_list(project_root: str, specs: list) -> list:
34
+ """待复审清单(FEAT-06):未命中注入 / 已停用 / 反复误报。
35
+
36
+ 豁免:check-state `_review_exempt` 列表中的条目不再提示(S-09 处置闭环)。
37
+ 日志覆盖不足 REVIEW_MIN_COVERAGE_DAYS 天时跳过"未命中"信号,
38
+ 避免误伤全新安装与刚部署项目。
39
+ """
40
+ state = load_check_state(project_root)
41
+ exempt = set(state.get("_review_exempt") or [])
42
+ review: list = []
43
+
44
+ events = cf_log.read_events(project_root, days=30)
45
+ if events and _log_coverage_sufficient(events):
46
+ engaged: set = set()
47
+ for event in events:
48
+ data = event.get("data") or {}
49
+ if event.get("event") == "inject":
50
+ engaged.update(data.get("specs") or [])
51
+ elif event.get("event") == "violation" and data.get("spec"):
52
+ engaged.add(data["spec"])
53
+ for spec in specs:
54
+ rel = spec.get("rel", "")
55
+ if rel.endswith("_map.md") or rel.startswith("shared/"):
56
+ continue
57
+ if rel not in engaged and rel not in exempt:
58
+ review.append({"item": rel, "reason": "30 天未命中注入/未触发检查"})
59
+
60
+ for cid, entry in state.items():
61
+ if cid.startswith("_") or not isinstance(entry, dict) or cid in exempt:
62
+ continue
63
+ if entry.get("disabled"):
64
+ review.append({
65
+ "item": cid,
66
+ "reason": f"已自动停用({entry.get('disabled_reason', '')}),待改写或删除",
67
+ })
68
+ elif int(entry.get("fp_count", 0)) >= 2:
69
+ review.append({"item": cid, "reason": f"误报 {entry['fp_count']} 次"})
70
+ return review
71
+
72
+
73
+ PATH_PATTERN = re.compile(r"(?:[\w./-]+/)+[\w.-]+\.[A-Za-z0-9]+")
74
+
75
+
76
+ def read_text(path: str) -> str:
77
+ try:
78
+ with open(path, "r", encoding="utf-8") as file:
79
+ return file.read().strip()
80
+ except Exception:
81
+ return ""
82
+
83
+
84
+ def normalize_line(line: str) -> str:
85
+ return " ".join(line.strip().split())
86
+
87
+
88
+ def find_redundant_lines(specs: list) -> dict:
89
+ line_map: dict = {}
90
+ for spec in specs:
91
+ for raw in spec["content"].splitlines():
92
+ line = normalize_line(raw)
93
+ if not line:
94
+ continue
95
+ if line.startswith("#") or line == "---":
96
+ continue
97
+ if len(line) < 6:
98
+ continue
99
+ line_map.setdefault(line, set()).add(spec["path"])
100
+ redundant = {line: paths for line, paths in line_map.items() if len(paths) >= 3}
101
+ return redundant
102
+
103
+
104
+ def find_missing_paths(text: str, project_root: str, base_dir: str = "") -> list:
105
+ """路径引用检查:项目根解析失败时,再按 spec 自身目录二次解析——
106
+ _map.md 常用相对本域目录的引用(如 design/design-lite.md)。"""
107
+ missing: list = []
108
+ for token in set(PATH_PATTERN.findall(text)):
109
+ if token.startswith("http://") or token.startswith("https://"):
110
+ continue
111
+ if os.path.isabs(token):
112
+ if not os.path.exists(token):
113
+ missing.append(token)
114
+ continue
115
+ if os.path.exists(os.path.join(project_root, token)):
116
+ continue
117
+ if base_dir and os.path.exists(os.path.join(base_dir, token)):
118
+ continue
119
+ missing.append(token)
120
+ return sorted(missing)
121
+
122
+
123
+ def build_report(project_root: str) -> dict:
124
+ """规范质量审计数据(cf-scan 引擎,亦供 cf-stats --audit 复用)。
125
+
126
+ files 含 issues / template 标记,review 为待复审清单;展示层负责过滤与渲染。"""
127
+ files = []
128
+ total_tokens = 0
129
+
130
+ claude_path = os.path.join(project_root, "CLAUDE.md")
131
+ if os.path.exists(claude_path):
132
+ content = read_text(claude_path)
133
+ tokens = estimate_tokens(content)
134
+ total_tokens += tokens
135
+ files.append({"path": "CLAUDE.md", "tokens": tokens, "issues": []})
136
+
137
+ specs_root = os.path.join(project_root, ".code-flow", "specs")
138
+ specs = []
139
+ if os.path.isdir(specs_root):
140
+ for root, _, filenames in os.walk(specs_root):
141
+ for name in filenames:
142
+ if not name.endswith(".md"):
143
+ continue
144
+ full_path = os.path.join(root, name)
145
+ content = read_text(full_path)
146
+ if not content:
147
+ continue
148
+ tokens = estimate_tokens(content)
149
+ total_tokens += tokens
150
+ rel = os.path.relpath(full_path, specs_root)
151
+ if rel.replace(os.sep, "/").startswith("_session/"):
152
+ continue # 会话级临时约束(FEAT-08)不参与审计与预算
153
+ rel_path = os.path.join("specs", rel).replace(os.sep, "/")
154
+ spec_entry = {
155
+ "path": rel_path,
156
+ "rel": rel.replace(os.sep, "/"),
157
+ "tokens": tokens,
158
+ "issues": [],
159
+ "content": content,
160
+ }
161
+ specs.append(spec_entry)
162
+
163
+ # 模板标记前置:模板(tags:[])不参与冗长/冗余审计,也不把
164
+ # 模板间共享的文档头计入其他文件的冗余计数
165
+ config = load_config(project_root)
166
+ effective_mapping = build_effective_mapping(
167
+ project_root, config.get("path_mapping") or {}
168
+ )
169
+ excluded_specs = non_injectable_specs(effective_mapping)
170
+ for spec in specs:
171
+ spec["template"] = spec.get("rel", "") in excluded_specs
172
+
173
+ redundant_map = find_redundant_lines([s for s in specs if not s["template"]])
174
+
175
+ for spec in specs:
176
+ issues = []
177
+ if not spec["template"] and spec["tokens"] > 500:
178
+ issues.append(f"冗长: {spec['tokens']} tokens")
179
+
180
+ spec_dir = os.path.join(specs_root, os.path.dirname(spec.get("rel", "")))
181
+ missing_paths = find_missing_paths(spec["content"], project_root, spec_dir)
182
+ for path in missing_paths[:3]:
183
+ issues.append(f"过时: 路径不存在 {path}")
184
+
185
+ redundant_lines = [line for line, paths in redundant_map.items() if spec["path"] in paths]
186
+ for line in redundant_lines[:3]:
187
+ issues.append(f"冗余: '{line}' 出现于 {len(redundant_map[line])} 个文件")
188
+
189
+ # Constraint specs feed the catalog one line each — flag missing
190
+ # frontmatter description (catalog falls back to blockquote/H1).
191
+ rel = spec.get("rel", "")
192
+ is_constraint = not rel.endswith("_map.md") and not rel.startswith("shared/")
193
+ if is_constraint:
194
+ meta, _ = parse_spec_frontmatter(spec["content"])
195
+ if not (meta.get("description") or "").strip():
196
+ issues.append("缺描述: 建议加 frontmatter description(catalog 适用场景一句话)")
197
+ # checks 标注校验(E-01 / B-04 的离线出口)
198
+ _, check_errors = parse_spec_checks(spec["content"])
199
+ for error in check_errors[:3]:
200
+ issues.append(f"checks: {error}")
201
+
202
+ files.append({
203
+ "path": spec["path"],
204
+ "tokens": spec["tokens"],
205
+ "issues": issues,
206
+ "template": spec["template"],
207
+ })
208
+
209
+ for entry in files:
210
+ if total_tokens <= 0:
211
+ entry["percent"] = "0%"
212
+ else:
213
+ entry["percent"] = f"{round(entry['tokens'] * 100 / total_tokens)}%"
214
+
215
+ budget = (config.get("budget") or {}).get("total", 2500)
216
+ try:
217
+ budget = int(budget)
218
+ except Exception:
219
+ budget = 2500
220
+
221
+ try:
222
+ catalog_max = int((config.get("budget") or {}).get("catalog_max", 200))
223
+ except Exception:
224
+ catalog_max = 200
225
+ catalog = build_spec_catalog(project_root, effective_mapping, catalog_max)
226
+ catalog_info = {
227
+ "tokens": estimate_tokens(catalog),
228
+ "max": catalog_max,
229
+ "entries": catalog.count("\n- `"),
230
+ }
231
+
232
+ review = build_review_list(project_root, specs)
233
+
234
+ # 预算口径与 cf-stats 一致:tags:[] 命令模板不计入注入预算
235
+ templates_tokens = sum(e["tokens"] for e in files if e.get("template"))
236
+ injectable_tokens = total_tokens - templates_tokens
237
+
238
+ return {
239
+ "files": files,
240
+ "total_tokens": injectable_tokens,
241
+ "templates_tokens": templates_tokens,
242
+ "budget": budget,
243
+ "catalog": catalog_info,
244
+ "review": review,
245
+ "warnings": [],
246
+ }
247
+
248
+
249
+ def main() -> None:
250
+ json_output = "--json" in sys.argv
251
+ only_issues = "--only-issues" in sys.argv
252
+ limit = None
253
+ for arg in sys.argv[1:]:
254
+ if arg.startswith("--limit="):
255
+ try:
256
+ limit = int(arg.split("=", 1)[1])
257
+ except Exception:
258
+ limit = None
259
+
260
+ report = build_report(os.getcwd())
261
+ files = report["files"]
262
+ if limit is not None:
263
+ files = files[:limit]
264
+ if only_issues:
265
+ files = [entry for entry in files if entry.get("issues")]
266
+
267
+ if json_output:
268
+ print(json.dumps(dict(report, files=files), ensure_ascii=False))
269
+ return
270
+
271
+ catalog_info = report["catalog"]
272
+ print("FILE | TOKENS | PERCENT | ISSUES")
273
+ for entry in files:
274
+ issues = entry.get("issues") or []
275
+ issue_text = " / ".join(issues) if issues else "-"
276
+ marker = " [模板]" if entry.get("template") else ""
277
+ print(f"{entry['path']}{marker} | {entry['tokens']} | {entry['percent']} | {issue_text}")
278
+ print("TOTAL:", f"{report['total_tokens']} / {report['budget']}")
279
+ if report["templates_tokens"]:
280
+ print("TEMPLATES (非注入):", f"{report['templates_tokens']} tokens,不计预算")
281
+ print(
282
+ "CATALOG:",
283
+ f"{catalog_info['tokens']} / {catalog_info['max']} tokens,",
284
+ f"{catalog_info['entries']} entries",
285
+ )
286
+ if report["review"]:
287
+ print("REVIEW (待复审):")
288
+ for item in report["review"]:
289
+ print(f" - {item['item']}: {item['reason']}")
290
+
291
+
292
+ if __name__ == "__main__":
293
+ main()
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ """SessionStart hook: reset .inject-state for the new session.
3
+
4
+ Reads the session_id from the hook payload (stdin JSON) so the value matches
5
+ what PreToolUse / UserPromptSubmit will write later — keeping the
6
+ "hook id 优先、回退 PID" contract in cf_core.resolve_session_id consistent
7
+ across the session. If stdin is empty (older runtime, unit test) we fall back
8
+ to the current PID; the next hook with a real session_id will then trigger
9
+ the normal session-mismatch reset path, so behavior degrades gracefully.
10
+ """
11
+ import json
12
+ import os
13
+ import sys
14
+
15
+ from cf_core import ensure_utf8_io, resolve_session_id
16
+
17
+
18
+ def main() -> None:
19
+ try:
20
+ ensure_utf8_io()
21
+ raw = sys.stdin.read()
22
+ data = json.loads(raw) if raw and raw.strip() else {}
23
+ if not isinstance(data, dict):
24
+ data = {}
25
+ sid = resolve_session_id(data)
26
+
27
+ project_root = os.getcwd()
28
+ state_path = os.path.join(project_root, ".code-flow", ".inject-state")
29
+ os.makedirs(os.path.dirname(state_path), exist_ok=True)
30
+ payload = {
31
+ "session_id": sid,
32
+ "injected_specs": [],
33
+ "last_file": "",
34
+ }
35
+ with open(state_path, "w", encoding="utf-8") as f:
36
+ json.dump(payload, f)
37
+ except Exception:
38
+ return
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()