@jahanxu/code-flow 0.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.
Files changed (30) hide show
  1. package/package.json +13 -0
  2. package/src/adapters/claude/settings.local.json +26 -0
  3. package/src/adapters/claude/skills/cf-init.md +13 -0
  4. package/src/adapters/claude/skills/cf-inject.md +12 -0
  5. package/src/adapters/claude/skills/cf-learn.md +11 -0
  6. package/src/adapters/claude/skills/cf-scan.md +12 -0
  7. package/src/adapters/claude/skills/cf-stats.md +11 -0
  8. package/src/adapters/claude/skills/cf-validate.md +12 -0
  9. package/src/adapters/codex/AGENTS.md +3 -0
  10. package/src/adapters/cursor/cursorrules +1 -0
  11. package/src/cli.js +105 -0
  12. package/src/core/code-flow/config.yml +97 -0
  13. package/src/core/code-flow/scripts/cf_core.py +129 -0
  14. package/src/core/code-flow/scripts/cf_init.py +829 -0
  15. package/src/core/code-flow/scripts/cf_inject.py +150 -0
  16. package/src/core/code-flow/scripts/cf_inject_hook.py +107 -0
  17. package/src/core/code-flow/scripts/cf_learn.py +202 -0
  18. package/src/core/code-flow/scripts/cf_scan.py +157 -0
  19. package/src/core/code-flow/scripts/cf_session_hook.py +16 -0
  20. package/src/core/code-flow/scripts/cf_stats.py +108 -0
  21. package/src/core/code-flow/scripts/cf_validate.py +340 -0
  22. package/src/core/code-flow/specs/backend/code-quality-performance.md +13 -0
  23. package/src/core/code-flow/specs/backend/database.md +13 -0
  24. package/src/core/code-flow/specs/backend/directory-structure.md +13 -0
  25. package/src/core/code-flow/specs/backend/logging.md +13 -0
  26. package/src/core/code-flow/specs/backend/platform-rules.md +13 -0
  27. package/src/core/code-flow/specs/frontend/component-specs.md +14 -0
  28. package/src/core/code-flow/specs/frontend/directory-structure.md +14 -0
  29. package/src/core/code-flow/specs/frontend/quality-standards.md +15 -0
  30. package/src/core/code-flow/validation.yml +30 -0
@@ -0,0 +1,829 @@
1
+ #!/usr/bin/env python3
2
+ import difflib
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+ from cf_core import estimate_tokens, load_config
9
+
10
+
11
+ def try_import_yaml():
12
+ try:
13
+ import yaml # type: ignore
14
+
15
+ return yaml
16
+ except Exception:
17
+ pass
18
+
19
+ try:
20
+ subprocess.run(
21
+ [sys.executable, "-m", "pip", "install", "pyyaml"],
22
+ check=False,
23
+ capture_output=True,
24
+ text=True,
25
+ )
26
+ except Exception:
27
+ return None
28
+
29
+ try:
30
+ import yaml # type: ignore
31
+
32
+ return yaml
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ def ensure_dir(path: str) -> None:
38
+ os.makedirs(path, exist_ok=True)
39
+
40
+
41
+ def read_text(path: str) -> str:
42
+ try:
43
+ with open(path, "r", encoding="utf-8") as file:
44
+ return file.read()
45
+ except Exception:
46
+ return ""
47
+
48
+
49
+ def write_text(path: str, content: str) -> bool:
50
+ try:
51
+ with open(path, "w", encoding="utf-8") as file:
52
+ file.write(content)
53
+ return True
54
+ except Exception:
55
+ return False
56
+
57
+
58
+ def merge_list(existing: list, template: list) -> list:
59
+ merged = list(existing)
60
+ seen = set()
61
+ for item in merged:
62
+ key = json.dumps(item, sort_keys=True, ensure_ascii=False) if isinstance(item, (dict, list)) else str(item)
63
+ seen.add(key)
64
+ for item in template:
65
+ key = json.dumps(item, sort_keys=True, ensure_ascii=False) if isinstance(item, (dict, list)) else str(item)
66
+ if key in seen:
67
+ continue
68
+ merged.append(item)
69
+ seen.add(key)
70
+ return merged
71
+
72
+
73
+ def merge_dict(existing: dict, template: dict) -> dict:
74
+ for key, value in template.items():
75
+ if key not in existing:
76
+ existing[key] = value
77
+ continue
78
+ if isinstance(value, dict) and isinstance(existing.get(key), dict):
79
+ existing[key] = merge_dict(existing.get(key) or {}, value)
80
+ elif isinstance(value, list) and isinstance(existing.get(key), list):
81
+ existing[key] = merge_list(existing.get(key) or [], value)
82
+ return existing
83
+
84
+
85
+ def merge_yaml(path: str, template: dict, yaml_module):
86
+ if os.path.exists(path):
87
+ if yaml_module is None:
88
+ return None, "yaml_missing"
89
+ try:
90
+ with open(path, "r", encoding="utf-8") as file:
91
+ existing = yaml_module.safe_load(file) or {}
92
+ except Exception:
93
+ existing = {}
94
+ merged = merge_dict(existing, template)
95
+ if merged == existing:
96
+ return existing, "skipped"
97
+ try:
98
+ with open(path, "w", encoding="utf-8") as file:
99
+ yaml_module.safe_dump(merged, file, sort_keys=False, allow_unicode=True)
100
+ return merged, "updated"
101
+ except Exception:
102
+ return existing, "write_failed"
103
+ else:
104
+ if yaml_module is None:
105
+ try:
106
+ with open(path, "w", encoding="utf-8") as file:
107
+ json.dump(template, file, ensure_ascii=False, indent=2)
108
+ return template, "created"
109
+ except Exception:
110
+ return None, "write_failed"
111
+ try:
112
+ with open(path, "w", encoding="utf-8") as file:
113
+ yaml_module.safe_dump(template, file, sort_keys=False, allow_unicode=True)
114
+ return template, "created"
115
+ except Exception:
116
+ return None, "write_failed"
117
+
118
+
119
+ def merge_json(path: str, template: dict):
120
+ if os.path.exists(path):
121
+ try:
122
+ with open(path, "r", encoding="utf-8") as file:
123
+ existing = json.load(file)
124
+ except Exception:
125
+ existing = {}
126
+ merged = merge_dict(existing, template)
127
+ if merged == existing:
128
+ return existing, "skipped"
129
+ try:
130
+ with open(path, "w", encoding="utf-8") as file:
131
+ json.dump(merged, file, ensure_ascii=False, indent=2)
132
+ return merged, "updated"
133
+ except Exception:
134
+ return existing, "write_failed"
135
+ try:
136
+ with open(path, "w", encoding="utf-8") as file:
137
+ json.dump(template, file, ensure_ascii=False, indent=2)
138
+ return template, "created"
139
+ except Exception:
140
+ return None, "write_failed"
141
+
142
+
143
+ def split_sections(template: str) -> list:
144
+ sections = []
145
+ current = []
146
+ for line in template.splitlines():
147
+ if line.startswith("## "):
148
+ if current:
149
+ sections.append("\n".join(current).rstrip())
150
+ current = [line]
151
+ else:
152
+ if current:
153
+ current.append(line)
154
+ if current:
155
+ sections.append("\n".join(current).rstrip())
156
+ return sections
157
+
158
+
159
+ def merge_markdown(path: str, template: str):
160
+ if not os.path.exists(path):
161
+ if write_text(path, template.rstrip() + "\n"):
162
+ return "created"
163
+ return "write_failed"
164
+
165
+ existing = read_text(path).rstrip()
166
+ if not existing:
167
+ if write_text(path, template.rstrip() + "\n"):
168
+ return "updated"
169
+ return "write_failed"
170
+
171
+ existing_headings = {
172
+ line.strip() for line in existing.splitlines() if line.strip().startswith("## ")
173
+ }
174
+ sections = split_sections(template)
175
+ additions = []
176
+ for section in sections:
177
+ heading = section.splitlines()[0].strip()
178
+ if heading not in existing_headings:
179
+ additions.append(section.strip())
180
+ if not additions:
181
+ return "skipped"
182
+ updated = existing + "\n\n" + "\n\n".join(additions).rstrip() + "\n"
183
+ if write_text(path, updated):
184
+ return "updated"
185
+ return "write_failed"
186
+
187
+
188
+ def build_unified_diff(original: str, template: str, from_label: str, to_label: str) -> str:
189
+ original_lines = original.rstrip().splitlines()
190
+ template_lines = template.rstrip().splitlines()
191
+ diff_lines = difflib.unified_diff(
192
+ original_lines,
193
+ template_lines,
194
+ fromfile=from_label,
195
+ tofile=to_label,
196
+ lineterm="",
197
+ )
198
+ return "\n".join(diff_lines)
199
+
200
+
201
+ def detect_stack(project_root: str, override: str):
202
+ frontend = False
203
+ backend = False
204
+ frameworks = []
205
+ backend_types = []
206
+
207
+ if override in {"frontend", "backend", "fullstack"}:
208
+ frontend = override in {"frontend", "fullstack"}
209
+ backend = override in {"backend", "fullstack"}
210
+ return frontend, backend, frameworks, backend_types, True
211
+
212
+ pkg_path = os.path.join(project_root, "package.json")
213
+ if os.path.exists(pkg_path):
214
+ frontend = True
215
+ try:
216
+ with open(pkg_path, "r", encoding="utf-8") as file:
217
+ pkg = json.load(file)
218
+ deps = {}
219
+ deps.update(pkg.get("dependencies") or {})
220
+ deps.update(pkg.get("devDependencies") or {})
221
+ if "react" in deps:
222
+ frameworks.append("react")
223
+ if "vue" in deps:
224
+ frameworks.append("vue")
225
+ if "@angular/core" in deps:
226
+ frameworks.append("angular")
227
+ except Exception:
228
+ pass
229
+
230
+ if os.path.exists(os.path.join(project_root, "pyproject.toml")) or os.path.exists(
231
+ os.path.join(project_root, "requirements.txt")
232
+ ):
233
+ backend = True
234
+ backend_types.append("python")
235
+
236
+ if os.path.exists(os.path.join(project_root, "go.mod")):
237
+ backend = True
238
+ backend_types.append("go")
239
+
240
+ detected = frontend or backend
241
+ return frontend, backend, frameworks, backend_types, detected
242
+
243
+
244
+ def format_stack(frontend: bool, backend: bool, frameworks: list, backend_types: list, detected: bool) -> str:
245
+ if not detected:
246
+ return "generic"
247
+ stack = []
248
+ if frontend:
249
+ stack.append("frontend")
250
+ if backend:
251
+ stack.append("backend")
252
+ details = []
253
+ if frameworks:
254
+ details.append("frameworks=" + ",".join(sorted(set(frameworks))))
255
+ if backend_types:
256
+ details.append("backend=" + ",".join(sorted(set(backend_types))))
257
+ if details:
258
+ stack.append("(" + "; ".join(details) + ")")
259
+ return " ".join(stack)
260
+
261
+
262
+ def hooks_ready(settings: dict) -> bool:
263
+ hooks = settings.get("hooks") or {}
264
+ pre = hooks.get("PreToolUse") or []
265
+ session = hooks.get("SessionStart") or []
266
+ if not pre or not session:
267
+ return False
268
+ pre_ok = any(
269
+ isinstance(item, dict)
270
+ and any(
271
+ isinstance(hook, dict)
272
+ and hook.get("command") == "python3 .code-flow/scripts/cf_inject_hook.py"
273
+ for hook in (item.get("hooks") or [])
274
+ )
275
+ for item in pre
276
+ )
277
+ session_ok = any(
278
+ isinstance(item, dict)
279
+ and any(
280
+ isinstance(hook, dict)
281
+ and hook.get("command") == "python3 .code-flow/scripts/cf_session_hook.py"
282
+ for hook in (item.get("hooks") or [])
283
+ )
284
+ for item in session
285
+ )
286
+ return pre_ok and session_ok
287
+
288
+
289
+ def main() -> None:
290
+ project_root = os.getcwd()
291
+ override = ""
292
+ if len(sys.argv) > 1:
293
+ override = sys.argv[1].strip().lower()
294
+
295
+ yaml_module = try_import_yaml()
296
+ warnings = []
297
+ if yaml_module is None:
298
+ warnings.append("pyyaml not available; yaml merge skipped")
299
+
300
+ frontend, backend, frameworks, backend_types, detected = detect_stack(project_root, override)
301
+ if not detected and override not in {"frontend", "backend", "fullstack"}:
302
+ frontend = True
303
+ backend = True
304
+
305
+ ensure_dir(os.path.join(project_root, ".code-flow"))
306
+ ensure_dir(os.path.join(project_root, ".code-flow", "scripts"))
307
+ if frontend:
308
+ ensure_dir(os.path.join(project_root, ".code-flow", "specs", "frontend"))
309
+ if backend:
310
+ ensure_dir(os.path.join(project_root, ".code-flow", "specs", "backend"))
311
+ ensure_dir(os.path.join(project_root, ".claude", "skills"))
312
+
313
+ config_template = {
314
+ "version": 1,
315
+ "budget": {"total": 2500, "l0_max": 800, "l1_max": 1700},
316
+ "inject": {
317
+ "auto": True,
318
+ "code_extensions": [
319
+ ".py",
320
+ ".go",
321
+ ".ts",
322
+ ".tsx",
323
+ ".js",
324
+ ".jsx",
325
+ ".java",
326
+ ".rs",
327
+ ".rb",
328
+ ".vue",
329
+ ".svelte",
330
+ ],
331
+ "skip_extensions": [
332
+ ".md",
333
+ ".txt",
334
+ ".json",
335
+ ".yml",
336
+ ".yaml",
337
+ ".toml",
338
+ ".lock",
339
+ ".csv",
340
+ ".xml",
341
+ ".svg",
342
+ ".png",
343
+ ".jpg",
344
+ ".jpeg",
345
+ ".gif",
346
+ ".ico",
347
+ ".pdf",
348
+ ".zip",
349
+ ".gz",
350
+ ".tar",
351
+ ],
352
+ "skip_paths": [
353
+ "docs/**",
354
+ "*.config.*",
355
+ ".code-flow/**",
356
+ ".claude/**",
357
+ "node_modules/**",
358
+ "dist/**",
359
+ "build/**",
360
+ "out/**",
361
+ "coverage/**",
362
+ ".next/**",
363
+ ".cache/**",
364
+ ".venv/**",
365
+ "venv/**",
366
+ "__pycache__/**",
367
+ ".git/**",
368
+ ],
369
+ },
370
+ "path_mapping": {
371
+ "frontend": {
372
+ "patterns": [
373
+ "src/components/**",
374
+ "src/pages/**",
375
+ "src/hooks/**",
376
+ "src/styles/**",
377
+ "**/*.tsx",
378
+ "**/*.jsx",
379
+ "**/*.css",
380
+ "**/*.scss",
381
+ ],
382
+ "specs": [
383
+ "frontend/directory-structure.md",
384
+ "frontend/quality-standards.md",
385
+ "frontend/component-specs.md",
386
+ ],
387
+ "spec_priority": {
388
+ "frontend/directory-structure.md": 1,
389
+ "frontend/quality-standards.md": 2,
390
+ "frontend/component-specs.md": 3,
391
+ },
392
+ },
393
+ "backend": {
394
+ "patterns": [
395
+ "services/**",
396
+ "api/**",
397
+ "models/**",
398
+ "**/*.py",
399
+ "**/*.go",
400
+ ],
401
+ "specs": [
402
+ "backend/directory-structure.md",
403
+ "backend/logging.md",
404
+ "backend/database.md",
405
+ "backend/platform-rules.md",
406
+ "backend/code-quality-performance.md",
407
+ ],
408
+ "spec_priority": {
409
+ "backend/directory-structure.md": 1,
410
+ "backend/database.md": 2,
411
+ "backend/logging.md": 3,
412
+ "backend/code-quality-performance.md": 4,
413
+ "backend/platform-rules.md": 5,
414
+ },
415
+ },
416
+ },
417
+ }
418
+
419
+ validation_template = {
420
+ "validators": [
421
+ {
422
+ "name": "Python 语法检查",
423
+ "trigger": "**/*.py",
424
+ "command": "python3 -m py_compile {files}",
425
+ "timeout": 30000,
426
+ "on_fail": "检查语法错误",
427
+ },
428
+ {
429
+ "name": "TypeScript 类型检查",
430
+ "trigger": "**/*.{ts,tsx}",
431
+ "command": "npx tsc --noEmit",
432
+ "timeout": 30000,
433
+ "on_fail": "检查类型定义,参见 specs/frontend/quality-standards.md",
434
+ },
435
+ {
436
+ "name": "ESLint",
437
+ "trigger": "**/*.{ts,tsx,js,jsx}",
438
+ "command": "npx eslint {files}",
439
+ "timeout": 15000,
440
+ "on_fail": "运行 npx eslint --fix 自动修复",
441
+ },
442
+ {
443
+ "name": "Python 类型检查",
444
+ "trigger": "**/*.py",
445
+ "command": "python3 -m mypy {files}",
446
+ "timeout": 30000,
447
+ "on_fail": "检查类型注解,参见 specs/backend/code-quality-performance.md",
448
+ },
449
+ {
450
+ "name": "Pytest",
451
+ "trigger": "**/*.py",
452
+ "command": "python3 -m pytest --tb=short -q",
453
+ "timeout": 60000,
454
+ "on_fail": "测试失败,检查断言和 mock 是否需要更新",
455
+ },
456
+ ]
457
+ }
458
+
459
+ claude_template = """# Project Guidelines
460
+
461
+ ## Team Identity
462
+ - Team: [team name]
463
+ - Project: [project name]
464
+ - Language: [primary language]
465
+
466
+ ## Core Principles
467
+ - All changes must include tests
468
+ - Single responsibility per function (<= 50 lines)
469
+ - No loose typing or silent exception handling
470
+ - Handle errors explicitly
471
+
472
+ ## Forbidden Patterns
473
+ - Hard-coded secrets or credentials
474
+ - Unparameterized SQL
475
+ - Network calls inside tight loops
476
+
477
+ ## Spec Loading
478
+ This project uses the code-flow layered spec system.
479
+ Specs live in .code-flow/specs/ and are injected on demand.
480
+
481
+ ## Learnings
482
+ """
483
+
484
+ spec_templates = {
485
+ "frontend/directory-structure.md": """# Frontend Directory Structure
486
+
487
+ ## Rules
488
+ - Define where components, pages, and hooks live.
489
+
490
+ ## Patterns
491
+ - Keep module boundaries explicit and predictable.
492
+
493
+ ## Anti-Patterns
494
+ - Avoid ad-hoc folders without owners.
495
+
496
+ ## Learnings
497
+ """,
498
+ "frontend/quality-standards.md": """# Frontend Quality Standards
499
+
500
+ ## Rules
501
+ - Enforce consistent typing and error handling.
502
+
503
+ ## Patterns
504
+ - Use shared utilities for validation and formatting.
505
+
506
+ ## Anti-Patterns
507
+ - Avoid side effects during render.
508
+
509
+ ## Learnings
510
+ """,
511
+ "frontend/component-specs.md": """# Component Specs
512
+
513
+ ## Rules
514
+ - Define Props with interface types.
515
+
516
+ ## Patterns
517
+ - Split container and presentational logic.
518
+
519
+ ## Anti-Patterns
520
+ - Do not mutate props.
521
+
522
+ ## Learnings
523
+ """,
524
+ "backend/directory-structure.md": """# Backend Directory Structure
525
+
526
+ ## Rules
527
+ - Keep service entrypoints and APIs separated.
528
+
529
+ ## Patterns
530
+ - Organize by bounded context.
531
+
532
+ ## Anti-Patterns
533
+ - Avoid dumping scripts in root.
534
+
535
+ ## Learnings
536
+ """,
537
+ "backend/logging.md": """# Backend Logging
538
+
539
+ ## Rules
540
+ - Emit structured logs for critical paths.
541
+
542
+ ## Patterns
543
+ - Include request_id and latency in logs.
544
+
545
+ ## Anti-Patterns
546
+ - Avoid noisy logs in tight loops.
547
+
548
+ ## Learnings
549
+ """,
550
+ "backend/database.md": """# Backend Database
551
+
552
+ ## Rules
553
+ - Use parameterized queries only.
554
+
555
+ ## Patterns
556
+ - Keep migrations idempotent.
557
+
558
+ ## Anti-Patterns
559
+ - Avoid external calls inside transactions.
560
+
561
+ ## Learnings
562
+ """,
563
+ "backend/platform-rules.md": """# Backend Platform Rules
564
+
565
+ ## Rules
566
+ - Ensure API changes are backward compatible.
567
+
568
+ ## Patterns
569
+ - Use feature flags for gradual rollouts.
570
+
571
+ ## Anti-Patterns
572
+ - Avoid debug configs in production.
573
+
574
+ ## Learnings
575
+ """,
576
+ "backend/code-quality-performance.md": """# Backend Code Quality & Performance
577
+
578
+ ## Rules
579
+ - Require structured logging on critical paths.
580
+
581
+ ## Patterns
582
+ - Add timeouts and retries for external calls.
583
+
584
+ ## Anti-Patterns
585
+ - Do not swallow exceptions.
586
+
587
+ ## Learnings
588
+ """,
589
+ }
590
+
591
+ skills_templates = {
592
+ "cf-init.md": """# cf-init
593
+
594
+ Initialize code-flow specs, config, skills, and hooks.
595
+
596
+ ## Usage
597
+ - /cf-init
598
+ - /cf-init frontend|backend|fullstack
599
+
600
+ ## Command
601
+ - python3 .code-flow/scripts/cf_init.py [frontend|backend|fullstack]
602
+ """,
603
+ "cf-scan.md": """# cf-scan
604
+
605
+ Audit spec tokens and redundancy.
606
+
607
+ ## Usage
608
+ - /cf-scan
609
+ - /cf-scan --json
610
+ - /cf-scan --only-issues
611
+ - /cf-scan --limit=10
612
+
613
+ ## Command
614
+ - python3 .code-flow/scripts/cf_scan.py [--json] [--only-issues] [--limit=N]
615
+ """,
616
+ "cf-inject.md": """# cf-inject
617
+
618
+ Manual spec injection (fallback when hooks do not fire).
619
+
620
+ ## Usage
621
+ - /cf-inject frontend|backend
622
+ - /cf-inject path/to/file.ext
623
+ - /cf-inject --list-specs --domain=frontend
624
+
625
+ ## Command
626
+ - python3 .code-flow/scripts/cf_inject.py [domain|file_path]
627
+ - python3 .code-flow/scripts/cf_inject.py --list-specs --domain=frontend
628
+ """,
629
+ "cf-validate.md": """# cf-validate
630
+
631
+ Run validators based on changed files.
632
+
633
+ ## Usage
634
+ - /cf-validate
635
+ - /cf-validate path/to/file.py
636
+ - /cf-validate --files=src/a.ts,src/b.ts
637
+
638
+ ## Command
639
+ - python3 .code-flow/scripts/cf_validate.py [file_path] [--files=...] [--only-failed] [--json-short] [--output=table]
640
+ """,
641
+ "cf-stats.md": """# cf-stats
642
+
643
+ Report L0/L1 token utilization.
644
+
645
+ ## Usage
646
+ - /cf-stats
647
+ - /cf-stats --human
648
+ - /cf-stats --domain=frontend
649
+
650
+ ## Command
651
+ - python3 .code-flow/scripts/cf_stats.py [--human] [--domain=frontend]
652
+ """,
653
+ "cf-learn.md": """# cf-learn
654
+
655
+ Append learnings to a spec file or CLAUDE.md.
656
+
657
+ ## Usage
658
+ - /cf-learn --scope=global --content="..."
659
+ - /cf-learn --scope=frontend --content="..." --file=frontend/component-specs.md
660
+ - /cf-learn --scope=backend --content="..." --file=backend/logging.md
661
+
662
+ ## Command
663
+ - python3 .code-flow/scripts/cf_learn.py --scope=global|frontend|backend --content="..." [--file=spec] [--dry-run]
664
+ """,
665
+ }
666
+
667
+ settings_template = {
668
+ "hooks": {
669
+ "PreToolUse": [
670
+ {
671
+ "matcher": "Edit|Write|MultiEdit",
672
+ "hooks": [
673
+ {
674
+ "type": "command",
675
+ "command": "python3 .code-flow/scripts/cf_inject_hook.py",
676
+ "timeout": 5,
677
+ }
678
+ ],
679
+ }
680
+ ],
681
+ "SessionStart": [
682
+ {
683
+ "hooks": [
684
+ {
685
+ "type": "command",
686
+ "command": "python3 .code-flow/scripts/cf_session_hook.py",
687
+ }
688
+ ]
689
+ }
690
+ ],
691
+ }
692
+ }
693
+
694
+ created = []
695
+ updated = []
696
+ skipped = []
697
+
698
+ config_path = os.path.join(project_root, ".code-flow", "config.yml")
699
+ _, status = merge_yaml(config_path, config_template, yaml_module)
700
+ if status == "created":
701
+ created.append(".code-flow/config.yml")
702
+ elif status == "updated":
703
+ updated.append(".code-flow/config.yml")
704
+ elif status == "skipped":
705
+ skipped.append(".code-flow/config.yml")
706
+ elif status == "yaml_missing":
707
+ warnings.append("config.yml merge skipped (pyyaml missing)")
708
+ skipped.append(".code-flow/config.yml")
709
+
710
+ validation_path = os.path.join(project_root, ".code-flow", "validation.yml")
711
+ _, status = merge_yaml(validation_path, validation_template, yaml_module)
712
+ if status == "created":
713
+ created.append(".code-flow/validation.yml")
714
+ elif status == "updated":
715
+ updated.append(".code-flow/validation.yml")
716
+ elif status == "skipped":
717
+ skipped.append(".code-flow/validation.yml")
718
+ elif status == "yaml_missing":
719
+ warnings.append("validation.yml merge skipped (pyyaml missing)")
720
+ skipped.append(".code-flow/validation.yml")
721
+
722
+ claude_path = os.path.join(project_root, "CLAUDE.md")
723
+ claude_exists = os.path.exists(claude_path)
724
+ claude_original = read_text(claude_path) if claude_exists else ""
725
+ claude_diff = ""
726
+ if claude_exists:
727
+ claude_diff = build_unified_diff(
728
+ claude_original,
729
+ claude_template,
730
+ "CLAUDE.md (current)",
731
+ "CLAUDE.md (template)",
732
+ )
733
+ status = merge_markdown(claude_path, claude_template)
734
+ if status == "created":
735
+ created.append("CLAUDE.md")
736
+ elif status == "updated":
737
+ updated.append("CLAUDE.md")
738
+ elif status == "skipped":
739
+ skipped.append("CLAUDE.md")
740
+
741
+ for rel, template in spec_templates.items():
742
+ domain = rel.split("/", 1)[0]
743
+ if domain == "frontend" and not frontend:
744
+ continue
745
+ if domain == "backend" and not backend:
746
+ continue
747
+ spec_path = os.path.join(project_root, ".code-flow", "specs", rel)
748
+ status = merge_markdown(spec_path, template)
749
+ if status == "created":
750
+ created.append(os.path.join(".code-flow", "specs", rel))
751
+ elif status == "updated":
752
+ updated.append(os.path.join(".code-flow", "specs", rel))
753
+ elif status == "skipped":
754
+ skipped.append(os.path.join(".code-flow", "specs", rel))
755
+
756
+ for name, template in skills_templates.items():
757
+ skill_path = os.path.join(project_root, ".claude", "skills", name)
758
+ status = merge_markdown(skill_path, template)
759
+ rel = os.path.join(".claude", "skills", name)
760
+ if status == "created":
761
+ created.append(rel)
762
+ elif status == "updated":
763
+ updated.append(rel)
764
+ elif status == "skipped":
765
+ skipped.append(rel)
766
+
767
+ settings_path = os.path.join(project_root, ".claude", "settings.local.json")
768
+ settings, status = merge_json(settings_path, settings_template)
769
+ if status == "created":
770
+ created.append(".claude/settings.local.json")
771
+ elif status == "updated":
772
+ updated.append(".claude/settings.local.json")
773
+ elif status == "skipped":
774
+ skipped.append(".claude/settings.local.json")
775
+
776
+ tokens = []
777
+ config = load_config(project_root)
778
+ specs_root = os.path.join(project_root, ".code-flow", "specs")
779
+ if config.get("path_mapping"):
780
+ for domain_cfg in (config.get("path_mapping") or {}).values():
781
+ for rel in domain_cfg.get("specs") or []:
782
+ full_path = os.path.join(specs_root, rel)
783
+ content = read_text(full_path).strip()
784
+ if not content:
785
+ continue
786
+ tokens.append(
787
+ {
788
+ "path": f"specs/{rel}".replace(os.sep, "/"),
789
+ "tokens": estimate_tokens(content),
790
+ }
791
+ )
792
+ elif os.path.isdir(specs_root):
793
+ for root, _, filenames in os.walk(specs_root):
794
+ for name in filenames:
795
+ if not name.endswith(".md"):
796
+ continue
797
+ full_path = os.path.join(root, name)
798
+ content = read_text(full_path).strip()
799
+ if not content:
800
+ continue
801
+ rel_path = os.path.relpath(full_path, os.path.join(project_root, ".code-flow"))
802
+ rel_path = rel_path.replace(os.sep, "/")
803
+ tokens.append(
804
+ {
805
+ "path": rel_path,
806
+ "tokens": estimate_tokens(content),
807
+ }
808
+ )
809
+
810
+ settings_loaded = settings if isinstance(settings, dict) else {}
811
+ hooks_ok = hooks_ready(settings_loaded)
812
+
813
+ summary = {
814
+ "stack": format_stack(frontend, backend, frameworks, backend_types, detected),
815
+ "created": created,
816
+ "updated": updated,
817
+ "skipped": skipped,
818
+ "tokens": tokens,
819
+ "hooks_ready": hooks_ok,
820
+ "warnings": warnings,
821
+ }
822
+ if claude_exists:
823
+ summary["suggestions"] = [{"file": "CLAUDE.md", "diff": claude_diff}]
824
+
825
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
826
+
827
+
828
+ if __name__ == "__main__":
829
+ main()