@michaelxwb/code-flow 0.5.2 → 0.6.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 (76) hide show
  1. package/docs/migrations/spec-workflow-0.6.0.md +42 -0
  2. package/package.json +4 -2
  3. package/src/adapters/claude/CLAUDE.md +11 -17
  4. package/src/adapters/claude/commands/cf-init.md +5 -4
  5. package/src/adapters/claude/commands/cf-spec.md +50 -0
  6. package/src/adapters/claude/commands/cf-task/align.md +23 -1
  7. package/src/adapters/claude/commands/cf-task/archive.md +7 -0
  8. package/src/adapters/claude/commands/cf-task/plan.md +16 -0
  9. package/src/adapters/claude/commands/cf-task/prd.md +24 -0
  10. package/src/adapters/claude/commands/cf-task/start.md +13 -10
  11. package/src/adapters/claude/settings.local.json +1 -11
  12. package/src/adapters/codex/AGENTS.md +11 -17
  13. package/src/adapters/codex/hooks.json +6 -4
  14. package/src/adapters/codex/skills/cf-init/SKILL.md +5 -4
  15. package/src/adapters/codex/skills/cf-spec/SKILL.md +55 -0
  16. package/src/adapters/codex/skills/cf-task-align/SKILL.md +23 -1
  17. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +7 -0
  18. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +16 -0
  19. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +24 -0
  20. package/src/adapters/codex/skills/cf-task-start/SKILL.md +13 -10
  21. package/src/adapters/costrict/CLAUDE.md +11 -17
  22. package/src/adapters/costrict/commands/cf-init.md +5 -4
  23. package/src/adapters/costrict/commands/cf-spec.md +50 -0
  24. package/src/adapters/costrict/commands/cf-task/align.md +23 -1
  25. package/src/adapters/costrict/commands/cf-task/archive.md +7 -0
  26. package/src/adapters/costrict/commands/cf-task/plan.md +16 -0
  27. package/src/adapters/costrict/commands/cf-task/prd.md +24 -0
  28. package/src/adapters/costrict/commands/cf-task/start.md +13 -10
  29. package/src/adapters/costrict/settings.local.json +1 -11
  30. package/src/adapters/opencode/AGENTS.md +11 -17
  31. package/src/adapters/opencode/commands/cf-init.md +4 -3
  32. package/src/adapters/opencode/commands/cf-spec.md +54 -0
  33. package/src/adapters/opencode/commands/cf-task/align.md +24 -1
  34. package/src/adapters/opencode/commands/cf-task/archive.md +8 -0
  35. package/src/adapters/opencode/commands/cf-task/plan.md +17 -0
  36. package/src/adapters/opencode/commands/cf-task/prd.md +25 -0
  37. package/src/adapters/opencode/commands/cf-task/start.md +14 -10
  38. package/src/adapters/opencode/plugins/code-flow/index.js +0 -1
  39. package/src/cli.js +51 -2
  40. package/src/core/code-flow/config.yml +13 -16
  41. package/src/core/code-flow/scripts/cf_checks.py +30 -0
  42. package/src/core/code-flow/scripts/cf_core.py +12 -255
  43. package/src/core/code-flow/scripts/cf_log.py +8 -0
  44. package/src/core/code-flow/scripts/cf_post_hook.py +33 -9
  45. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +65 -0
  46. package/src/core/code-flow/scripts/cf_spec_context.py +1233 -0
  47. package/src/core/code-flow/scripts/cf_spec_gate.py +224 -0
  48. package/src/core/code-flow/scripts/cf_spec_metadata.py +304 -0
  49. package/src/core/code-flow/scripts/cf_spec_migrate.py +512 -0
  50. package/src/core/code-flow/scripts/cf_spec_resolver.py +271 -0
  51. package/src/core/code-flow/scripts/cf_spec_router.py +145 -0
  52. package/src/core/code-flow/scripts/cf_spec_session.py +122 -0
  53. package/src/core/code-flow/scripts/cf_spec_verify.py +293 -0
  54. package/src/core/code-flow/scripts/cf_stats.py +40 -5
  55. package/src/core/code-flow/scripts/cf_stop_hook.py +22 -2
  56. package/src/core/code-flow/scripts/cf_task_runtime.py +142 -0
  57. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +34 -360
  58. package/src/core/code-flow/specs/backend/code-quality-performance.md +13 -1
  59. package/src/core/code-flow/specs/backend/database.md +13 -1
  60. package/src/core/code-flow/specs/backend/directory-structure.md +13 -1
  61. package/src/core/code-flow/specs/backend/logging.md +13 -1
  62. package/src/core/code-flow/specs/backend/platform-rules.md +13 -1
  63. package/src/core/code-flow/specs/frontend/component-specs.md +13 -1
  64. package/src/core/code-flow/specs/frontend/directory-structure.md +13 -1
  65. package/src/core/code-flow/specs/frontend/quality-standards.md +13 -1
  66. package/src/core/code-flow/specs/shared/design/design-frontend.md +10 -0
  67. package/src/core/code-flow/specs/shared/design/design-full.md +10 -0
  68. package/src/core/code-flow/specs/shared/design/design-lite.md +10 -0
  69. package/src/core/code-flow/specs/shared/prd-template.md +14 -3
  70. package/src/migrate/spec-workflow.js +238 -0
  71. package/src/adapters/claude/commands/cf-inject.md +0 -55
  72. package/src/adapters/codex/skills/cf-inject/SKILL.md +0 -56
  73. package/src/adapters/costrict/commands/cf-inject.md +0 -55
  74. package/src/adapters/opencode/commands/cf-inject.md +0 -59
  75. package/src/core/code-flow/scripts/cf_inject_hook.py +0 -178
  76. package/src/core/code-flow/scripts/cf_session_hook.py +0 -42
@@ -1,178 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import os
4
- import sys
5
-
6
- import cf_log
7
- from cf_core import (
8
- _log,
9
- assemble_context,
10
- build_effective_mapping,
11
- debug_log,
12
- ensure_utf8_io,
13
- extract_context_tags,
14
- fallback_domains_for_context,
15
- is_code_file,
16
- load_config,
17
- load_inject_state,
18
- match_domains,
19
- match_specs_by_tags,
20
- read_matched_specs,
21
- resolve_compress,
22
- resolve_quality_loop,
23
- resolve_session_id,
24
- save_inject_state,
25
- select_specs_tiered,
26
- )
27
-
28
-
29
-
30
-
31
-
32
- def main() -> None:
33
- try:
34
- ensure_utf8_io()
35
- raw = sys.stdin.read()
36
- if not raw.strip():
37
- return
38
- data = json.loads(raw)
39
- tool_name = data.get("tool_name", "")
40
- tool_input = data.get("tool_input") or {}
41
- file_path = tool_input.get("file_path", "")
42
- if tool_name not in {"Edit", "Write", "MultiEdit"}:
43
- return
44
- if not isinstance(file_path, str) or not file_path:
45
- return
46
-
47
- project_root = os.getcwd()
48
- abs_path = file_path
49
- if not os.path.isabs(abs_path):
50
- abs_path = os.path.join(project_root, file_path)
51
- rel_path = os.path.relpath(abs_path, project_root)
52
-
53
- config = load_config(project_root)
54
- if not config:
55
- return
56
- inject_config = config.get("inject") or {}
57
- if inject_config.get("auto") is False:
58
- return
59
- if not is_code_file(rel_path, inject_config):
60
- return
61
- compress_enabled = resolve_compress(inject_config)
62
-
63
- mapping = config.get("path_mapping") or {}
64
- effective_mapping = build_effective_mapping(project_root, mapping)
65
- domains = match_domains(rel_path, effective_mapping)
66
-
67
- sid = resolve_session_id(data)
68
-
69
- # Quality-loop instrumentation (FEAT-00): every code-file edit is an
70
- # event — correction pairing and stop-check need them even when no
71
- # spec matches below. append_event never raises (hot path safe).
72
- quality_loop = resolve_quality_loop(config)
73
- if quality_loop["enabled"]:
74
- cf_log.append_event(
75
- project_root, "edit",
76
- {"file": rel_path.replace(os.sep, "/"), "tool": tool_name},
77
- sid,
78
- )
79
-
80
- # Extract context tags from file path
81
- context_tags = extract_context_tags(rel_path)
82
- if not domains:
83
- domains = sorted(fallback_domains_for_context(effective_mapping, context_tags))
84
- if not domains:
85
- return
86
-
87
- # Budget config
88
- budget_cfg = config.get("budget") or {}
89
- l1_budget = 1700
90
- map_max = 400
91
- try:
92
- l1_budget = int(budget_cfg.get("l1_max", 1700))
93
- except (ValueError, TypeError):
94
- pass
95
- try:
96
- map_max = int(budget_cfg.get("map_max", 400))
97
- except (ValueError, TypeError):
98
- pass
99
-
100
- # Strict tag-based matching per domain. No bulk-load fallback: when a
101
- # Tier 1 spec's tags don't intersect context_tags, the spec is NOT
102
- # injected. Tier 0 (_map.md) uses the "*" wildcard so it still reaches
103
- # the model as navigation. Re-inject every call (session dedup removed).
104
- all_matched = []
105
- for domain in domains:
106
- domain_cfg = effective_mapping.get(domain) or {}
107
- specs_config = domain_cfg.get("specs") or []
108
- matched, _ = match_specs_by_tags(specs_config, context_tags)
109
- if matched:
110
- specs = read_matched_specs(
111
- project_root, domain, matched, compress=compress_enabled
112
- )
113
- all_matched.extend(specs)
114
-
115
- if not all_matched:
116
- return
117
-
118
- selected = select_specs_tiered(all_matched, l1_budget, map_max)
119
- if not selected:
120
- return
121
-
122
- # Load state with session isolation (deferred until after match success)
123
- state = load_inject_state(project_root)
124
- same_session = state.get("session_id") == sid
125
- if same_session:
126
- injected_specs = set(state.get("injected_specs") or [])
127
- else:
128
- injected_specs = set()
129
-
130
- # Update state with newly injected spec paths. Preserve
131
- # UserPromptSubmit-owned fields (prompt_count / prompt_inject_window)
132
- # verbatim so cross-hook writes don't reset the dedup window.
133
- new_injected = injected_specs | {s["path"] for s in selected}
134
- debug_log(f"inject_hook injected={[s['path'] for s in selected]} path={rel_path}", project_root)
135
- new_state = {
136
- "session_id": sid,
137
- "injected_specs": sorted(new_injected),
138
- "last_file": abs_path,
139
- }
140
- if same_session:
141
- if "prompt_count" in state:
142
- new_state["prompt_count"] = state["prompt_count"]
143
- if "prompt_inject_window" in state:
144
- new_state["prompt_inject_window"] = state["prompt_inject_window"]
145
- save_inject_state(project_root, new_state)
146
-
147
- if quality_loop["enabled"]:
148
- cf_log.append_event(
149
- project_root, "inject",
150
- {"specs": [s["path"] for s in selected],
151
- "mode": "full", "source": "pretooluse"},
152
- sid,
153
- )
154
-
155
- payload = {
156
- "hookSpecificOutput": {
157
- "hookEventName": "PreToolUse",
158
- "additionalContext": assemble_context(
159
- selected, "## Active Specs (auto-injected)"
160
- ),
161
- }
162
- }
163
- if os.environ.get("CF_DEBUG") == "1":
164
- payload["debug"] = {
165
- "target": abs_path,
166
- "domains": sorted(domains),
167
- "context_tags": sorted(context_tags),
168
- "matched_specs": [s["path"] for s in selected],
169
- }
170
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
171
- except Exception as exc:
172
- # Fix #9: log errors to stderr instead of silently swallowing
173
- _log(f"cf_inject_hook error: {exc}")
174
- return
175
-
176
-
177
- if __name__ == "__main__":
178
- main()
@@ -1,42 +0,0 @@
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()