claude-coding-flow 1.0.0 → 1.1.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/bin/flow.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
+ const { spawn } = require("child_process");
5
6
 
6
7
  const PKG_ROOT = path.resolve(__dirname, "..");
7
8
  const CWD = process.cwd();
@@ -25,29 +26,65 @@ function copyRecursive(src, dest) {
25
26
  function init() {
26
27
  console.log(yellow("flow init"));
27
28
 
29
+ // Copy skill commands
28
30
  const commandsSrc = path.join(PKG_ROOT, "commands");
29
31
  const commandsDest = path.join(CWD, ".claude", "commands");
30
32
 
31
- if (!fs.existsSync(commandsSrc)) {
32
- console.log(red("Error: commands directory not found in package"));
33
+ if (fs.existsSync(commandsSrc)) {
34
+ fs.mkdirSync(commandsDest, { recursive: true });
35
+ const files = fs.readdirSync(commandsSrc).filter((f) => f.endsWith(".md"));
36
+ for (const file of files) {
37
+ const src = path.join(commandsSrc, file);
38
+ const dest = path.join(commandsDest, file);
39
+ if (fs.existsSync(dest)) {
40
+ console.log(yellow(` skip (exists): .claude/commands/${file}`));
41
+ } else {
42
+ fs.copyFileSync(src, dest);
43
+ console.log(green(` copied: .claude/commands/${file}`));
44
+ }
45
+ }
46
+ }
47
+
48
+ // Copy dashboard
49
+ const dashSrc = path.join(PKG_ROOT, "dashboard");
50
+ const dashDest = path.join(CWD, "dashboard");
51
+
52
+ if (fs.existsSync(dashSrc)) {
53
+ fs.mkdirSync(dashDest, { recursive: true });
54
+ copyRecursive(dashSrc, dashDest);
55
+ console.log(green(" copied: dashboard/"));
56
+ }
57
+
58
+ console.log(green("\ndone!"));
59
+ }
60
+
61
+ function dashboard() {
62
+ const dashDir = path.join(CWD, "dashboard");
63
+ const pkgDashDir = path.join(PKG_ROOT, "dashboard");
64
+
65
+ const target = fs.existsSync(dashDir) ? dashDir : pkgDashDir;
66
+
67
+ if (!fs.existsSync(path.join(target, "main.py"))) {
68
+ console.log(red("Error: dashboard/main.py not found. Run `flow init` first."));
33
69
  process.exit(1);
34
70
  }
35
71
 
36
- fs.mkdirSync(commandsDest, { recursive: true });
37
-
38
- const files = fs.readdirSync(commandsSrc).filter((f) => f.endsWith(".md"));
39
- for (const file of files) {
40
- const src = path.join(commandsSrc, file);
41
- const dest = path.join(commandsDest, file);
42
- if (fs.existsSync(dest)) {
43
- console.log(yellow(` skip (exists): .claude/commands/${file}`));
44
- } else {
45
- fs.copyFileSync(src, dest);
46
- console.log(green(` copied: .claude/commands/${file}`));
47
- }
72
+ const requirements = path.join(target, "requirements.txt");
73
+ if (fs.existsSync(requirements)) {
74
+ console.log(yellow("Installing dashboard dependencies..."));
75
+ try {
76
+ require("child_process").execSync(`pip3 install -r "${requirements}"`, {
77
+ stdio: "inherit",
78
+ });
79
+ } catch {}
48
80
  }
49
81
 
50
- console.log(green("\ndone! skills installed to .claude/commands/"));
82
+ console.log(green("Starting dashboard on http://localhost:8000"));
83
+ const child = spawn("python3", [path.join(target, "main.py"), "serve"], {
84
+ stdio: "inherit",
85
+ cwd: CWD,
86
+ });
87
+ child.on("exit", (code) => process.exit(code));
51
88
  }
52
89
 
53
90
  const args = process.argv.slice(2);
@@ -57,10 +94,14 @@ switch (command) {
57
94
  case "init":
58
95
  init();
59
96
  break;
97
+ case "dashboard":
98
+ dashboard();
99
+ break;
60
100
  default:
61
101
  console.log(`Usage: flow <command>
62
102
 
63
103
  Commands:
64
- init Install skills to .claude/commands/ in current project`);
104
+ init Install skills and dashboard to current project
105
+ dashboard Start the dashboard server (http://localhost:8000)`);
65
106
  break;
66
107
  }
@@ -0,0 +1,296 @@
1
+ import json
2
+ import os
3
+ from datetime import datetime
4
+ from typing import Optional, List, Dict
5
+
6
+ WORKTREE_DIR = os.path.join(os.getcwd(), ".worktree")
7
+
8
+ PHASE_NAMES_CODE_GEN = {
9
+ 1: "需求收集", 2: "信息加载", 3: "方案规划",
10
+ 4: "代码生成", 5: "编译校验", 6: "代码反思", 7: "交付",
11
+ }
12
+ PHASE_NAMES_DOC_GEN = {1: "信息收集"}
13
+ PHASE_NAMES_BUG_FIX = {1: "定位", 2: "修复", 3: "验证"}
14
+
15
+
16
+ def get_phase_name(task_type: str, phase_num: int) -> str:
17
+ if task_type == "bug-fix":
18
+ return PHASE_NAMES_BUG_FIX.get(phase_num, f"阶段{phase_num}")
19
+ elif task_type == "doc-gen":
20
+ return PHASE_NAMES_DOC_GEN.get(phase_num, f"阶段{phase_num}")
21
+ return PHASE_NAMES_CODE_GEN.get(phase_num, f"阶段{phase_num}")
22
+
23
+
24
+ def _worktree() -> str:
25
+ os.makedirs(WORKTREE_DIR, exist_ok=True)
26
+ return WORKTREE_DIR
27
+
28
+
29
+ # ── 模块 ──
30
+
31
+ def _scan_module_dirs() -> List[str]:
32
+ """扫描 worktree 下所有类型子目录中包含 module.json 的模块目录"""
33
+ wt = _worktree()
34
+ dirs = []
35
+ if not os.path.exists(wt):
36
+ return dirs
37
+ for type_dir_name in sorted(os.listdir(wt)):
38
+ type_path = os.path.join(wt, type_dir_name)
39
+ if not os.path.isdir(type_path):
40
+ continue
41
+ for name in sorted(os.listdir(type_path)):
42
+ path = os.path.join(type_path, name)
43
+ if os.path.isdir(path) and os.path.exists(os.path.join(path, "module.json")):
44
+ dirs.append(path)
45
+ return dirs
46
+
47
+
48
+ def get_modules() -> List[Dict]:
49
+ result = []
50
+ for path in _scan_module_dirs():
51
+ try:
52
+ with open(os.path.join(path, "module.json"), "r", encoding="utf-8") as f:
53
+ mod = json.load(f)
54
+ mod["_path"] = path
55
+ result.append(mod)
56
+ except Exception:
57
+ pass
58
+ return result
59
+
60
+
61
+ def get_module(module_id: str) -> Optional[Dict]:
62
+ for mod in get_modules():
63
+ if mod["id"] == module_id:
64
+ return mod
65
+ return None
66
+
67
+
68
+ def _module_dir(module_id: str) -> Optional[str]:
69
+ for path in _scan_module_dirs():
70
+ try:
71
+ with open(os.path.join(path, "module.json"), "r", encoding="utf-8") as f:
72
+ mod = json.load(f)
73
+ if mod["id"] == module_id:
74
+ return path
75
+ except Exception:
76
+ pass
77
+ return None
78
+
79
+
80
+ # ── 任务 ──
81
+
82
+ def _scan_task_dirs(module_id: str) -> List[str]:
83
+ """扫描模块目录下所有包含 task.json 的子目录"""
84
+ md = _module_dir(module_id)
85
+ if not md:
86
+ return []
87
+ dirs = []
88
+ for name in sorted(os.listdir(md)):
89
+ path = os.path.join(md, name)
90
+ if os.path.isdir(path) and os.path.exists(os.path.join(path, "task.json")):
91
+ dirs.append(path)
92
+ return dirs
93
+
94
+
95
+ def get_module_tasks(module_id: str) -> List[Dict]:
96
+ result = []
97
+ for path in _scan_task_dirs(module_id):
98
+ try:
99
+ with open(os.path.join(path, "task.json"), "r", encoding="utf-8") as f:
100
+ task = json.load(f)
101
+ result.append(task)
102
+ except Exception:
103
+ pass
104
+ return result
105
+
106
+
107
+ def get_tasks() -> List[Dict]:
108
+ result = []
109
+ for mod in get_modules():
110
+ result.extend(get_module_tasks(mod["id"]))
111
+ return result
112
+
113
+
114
+ def get_task(task_id: str) -> Optional[Dict]:
115
+ for task in get_tasks():
116
+ if task["id"] == task_id:
117
+ return task
118
+ return None
119
+
120
+
121
+ def get_task_dir(task_id: str) -> Optional[str]:
122
+ """根据 task_id 查找其所在目录"""
123
+ for mod in get_modules():
124
+ for path in _scan_task_dirs(mod["id"]):
125
+ try:
126
+ with open(os.path.join(path, "task.json"), "r", encoding="utf-8") as f:
127
+ task = json.load(f)
128
+ if task["id"] == task_id:
129
+ return path
130
+ except Exception:
131
+ pass
132
+ return None
133
+
134
+
135
+ # ── 按类型过滤 ──
136
+
137
+ def get_modules_by_type(module_type: str) -> List[Dict]:
138
+ """按 type 字段过滤模块,未指定 type 的默认为 code-gen"""
139
+ result = []
140
+ for mod in get_modules():
141
+ mod_type = mod.get("type", "code-gen")
142
+ if mod_type == module_type:
143
+ result.append(mod)
144
+ return result
145
+
146
+
147
+ # ── doc-gen 产物 ──
148
+
149
+ def get_doc_gen_artifacts(task_dir: str) -> Dict:
150
+ """读取 doc-gen 任务的产物信息"""
151
+ result = {"requirement_doc": None, "images": [], "develop_doc": None}
152
+
153
+ req_dir = os.path.join(task_dir, "requirement")
154
+ if os.path.exists(req_dir):
155
+ files = [f for f in os.listdir(req_dir) if not f.startswith(".")]
156
+ result["requirement_doc"] = files[0] if files else None
157
+
158
+ img_dir = os.path.join(task_dir, "images")
159
+ if os.path.exists(img_dir):
160
+ result["images"] = sorted(
161
+ f for f in os.listdir(img_dir) if not f.startswith(".")
162
+ )
163
+
164
+ for f in os.listdir(task_dir):
165
+ if f.endswith("-develop.md"):
166
+ result["develop_doc"] = f
167
+ break
168
+
169
+ return result
170
+
171
+
172
+ # ── bug-fix 信息 ──
173
+
174
+ def get_bug_fix_info(task_dir: str) -> Dict:
175
+ """读取 bug-fix 任务的 bug 信息"""
176
+ try:
177
+ with open(os.path.join(task_dir, "task.json"), "r", encoding="utf-8") as f:
178
+ task = json.load(f)
179
+ except Exception:
180
+ return {}
181
+ return {
182
+ "bug_status": task.get("bug_status", "unresolved"),
183
+ "bug_description": task.get("bug_description", ""),
184
+ "source_info": task.get("source_info", {}),
185
+ }
186
+
187
+
188
+ # ── 任务链 ──
189
+
190
+ def get_task_chain(module_id: str) -> Dict:
191
+ """获取模块内的任务链关系"""
192
+ tasks = get_module_tasks(module_id)
193
+ if not tasks:
194
+ return {"chains": [], "task_map": {}}
195
+
196
+ task_map = {t["id"]: t for t in tasks}
197
+ chains = []
198
+ visited = set()
199
+
200
+ # 找到所有起始任务(prev_task_id 为空)
201
+ roots = [t for t in tasks if not t.get("prev_task_id")]
202
+ for root in roots:
203
+ chain = []
204
+ current = root
205
+ while current:
206
+ chain.append(current["id"])
207
+ visited.add(current["id"])
208
+ next_tasks = [t for t in tasks if t.get("prev_task_id") == current["id"]]
209
+ current = next_tasks[0] if next_tasks else None
210
+ chains.append(chain)
211
+
212
+ # 处理未在链中的任务(无 root 且未被访问)
213
+ for t in tasks:
214
+ if t["id"] not in visited:
215
+ chains.append([t["id"]])
216
+
217
+ return {
218
+ "chains": chains,
219
+ "task_map": {
220
+ tid: {"title": t["title"], "status": t["status"]}
221
+ for tid, t in task_map.items()
222
+ },
223
+ }
224
+
225
+
226
+ # ── 跨类型关联 ──
227
+
228
+ def get_cross_relations(task_id: str) -> Dict:
229
+ """查找任务的跨类型关联关系"""
230
+ task = get_task(task_id)
231
+ if not task:
232
+ return {"relations": []}
233
+
234
+ relations = []
235
+
236
+ # code-gen → doc-gen: 通过 dev_doc 反查
237
+ dev_doc = task.get("dev_doc")
238
+ if dev_doc and task.get("type") == "code-gen":
239
+ # 在 doc-gen 任务中查找生成该 develop.md 的任务
240
+ for mod in get_modules():
241
+ if mod.get("type") != "doc-gen":
242
+ continue
243
+ for tp in _scan_task_dirs(mod["id"]):
244
+ try:
245
+ with open(os.path.join(tp, "task.json"), "r", encoding="utf-8") as f:
246
+ dt = json.load(f)
247
+ if dt.get("output_doc") and dev_doc.endswith(dt["output_doc"]):
248
+ relations.append({
249
+ "type": "based-on",
250
+ "label": "基于开发文档",
251
+ "from_task": task_id,
252
+ "to_task": dt["id"],
253
+ "to_task_title": dt.get("title", ""),
254
+ "to_module_id": mod["id"],
255
+ "to_module_name": mod.get("name", ""),
256
+ "artifact": dt.get("output_doc", ""),
257
+ })
258
+ break
259
+ except Exception:
260
+ pass
261
+
262
+ # bug-fix → code-gen: 通过 related_task_id
263
+ related = task.get("related_task_id")
264
+ if related and task.get("type") == "bug-fix":
265
+ rt = get_task(related)
266
+ if rt:
267
+ relations.append({
268
+ "type": "traces-to",
269
+ "label": "溯源",
270
+ "from_task": task_id,
271
+ "to_task": related,
272
+ "to_task_title": rt.get("title", ""),
273
+ })
274
+
275
+ # code-gen ← doc-gen: 反向查找哪些 code-gen 任务基于此 doc-gen 任务
276
+ if task.get("type") == "doc-gen" and task.get("output_doc"):
277
+ for mod in get_modules():
278
+ if mod.get("type") != "code-gen":
279
+ continue
280
+ for tp in _scan_task_dirs(mod["id"]):
281
+ try:
282
+ with open(os.path.join(tp, "task.json"), "r", encoding="utf-8") as f:
283
+ ct = json.load(f)
284
+ dd = ct.get("dev_doc", "")
285
+ if dd and task["output_doc"] and dd.endswith(task["output_doc"]):
286
+ relations.append({
287
+ "type": "used-by",
288
+ "label": "被引用",
289
+ "from_task": ct["id"],
290
+ "from_task_title": ct.get("title", ""),
291
+ "to_task": task_id,
292
+ })
293
+ except Exception:
294
+ pass
295
+
296
+ return {"relations": relations}
@@ -0,0 +1,110 @@
1
+ import json
2
+ import os
3
+ from typing import Optional, List, Dict
4
+
5
+
6
+ def _jsonl_path(task_dir: str) -> str:
7
+ return os.path.join(task_dir, "log.jsonl")
8
+
9
+
10
+ def _text_log_path(task_dir: str) -> str:
11
+ return os.path.join(task_dir, "log")
12
+
13
+
14
+ def _snapshot_base(task_dir: str) -> str:
15
+ return os.path.join(task_dir, "snapshots")
16
+
17
+
18
+ # ── 执行日志 ──
19
+
20
+ def get_logs(task_dir: str) -> List[Dict]:
21
+ path = _jsonl_path(task_dir)
22
+ if not os.path.exists(path):
23
+ return []
24
+ with open(path, "r", encoding="utf-8") as f:
25
+ return [json.loads(line) for line in f if line.strip()]
26
+
27
+
28
+ def get_token_summary(task_dir: str) -> Dict:
29
+ logs = get_logs(task_dir)
30
+ total = sum(e.get("tokens", 0) or 0 for e in logs)
31
+ phases = {}
32
+ for e in logs:
33
+ p = e.get("phase", 0)
34
+ t = e.get("tokens", 0) or 0
35
+ phases[p] = phases.get(p, 0) + t
36
+ return {"total_tokens": total, "tokens_by_phase": phases}
37
+
38
+
39
+ def get_text_log(task_dir: str) -> Optional[str]:
40
+ # 优先读 log.md,兼容旧 log 文件
41
+ md_path = os.path.join(task_dir, "log.md")
42
+ if os.path.exists(md_path):
43
+ with open(md_path, "r", encoding="utf-8") as f:
44
+ return f.read()
45
+ path = _text_log_path(task_dir)
46
+ if os.path.exists(path):
47
+ with open(path, "r", encoding="utf-8") as f:
48
+ return f.read()
49
+ return None
50
+
51
+
52
+ # ── Plan 存储 ──
53
+
54
+ def get_plan(task_dir: str) -> Optional[str]:
55
+ plan_path = os.path.join(task_dir, "plan.md")
56
+ if not os.path.exists(plan_path):
57
+ return None
58
+ with open(plan_path, "r", encoding="utf-8") as f:
59
+ return f.read()
60
+
61
+
62
+ # ── 代码快照 ──
63
+
64
+ def list_snapshots(task_dir: str) -> List[Dict]:
65
+ snap_dir = _snapshot_base(task_dir)
66
+ if not os.path.exists(snap_dir):
67
+ return []
68
+
69
+ files = []
70
+ for name in sorted(os.listdir(snap_dir)):
71
+ path = os.path.join(snap_dir, name)
72
+ if os.path.isfile(path) and not name.startswith("."):
73
+ files.append(name)
74
+
75
+ # 检查子目录(兼容旧结构)
76
+ sub_dirs = []
77
+ for name in sorted(os.listdir(snap_dir)):
78
+ path = os.path.join(snap_dir, name)
79
+ if os.path.isdir(path):
80
+ sub_files = []
81
+ for root, _, fs in os.walk(path):
82
+ for fname in fs:
83
+ if fname == "_summary.json":
84
+ continue
85
+ rel = os.path.relpath(os.path.join(root, fname), path)
86
+ sub_files.append(rel)
87
+ if sub_files:
88
+ sub_dirs.append({"name": name, "files": sub_files})
89
+
90
+ result = []
91
+ if files:
92
+ result.append({"name": "files", "files": files})
93
+ result.extend(sub_dirs)
94
+ return result
95
+
96
+
97
+ def get_snapshot_file(task_dir: str, snapshot_name: str, filepath: str) -> Optional[str]:
98
+ snap_dir = _snapshot_base(task_dir)
99
+ # 扁平结构(snapshot_name == "files"):文件直接在 snapshots/ 下
100
+ if snapshot_name == "files":
101
+ path = os.path.join(snap_dir, filepath)
102
+ else:
103
+ path = os.path.join(snap_dir, snapshot_name, filepath)
104
+ if not os.path.exists(path):
105
+ return None
106
+ try:
107
+ with open(path, "r", encoding="utf-8") as f:
108
+ return f.read()
109
+ except UnicodeDecodeError:
110
+ return None