@michaelxwb/code-flow 0.6.0 → 0.6.2

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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/adapters/claude/commands/cf-spec.md +13 -0
  3. package/src/adapters/claude/commands/cf-sync.md +25 -0
  4. package/src/adapters/claude/commands/cf-task/plan.md +40 -0
  5. package/src/adapters/claude/commands/cf-task/start.md +6 -7
  6. package/src/adapters/claude/commands/cf-task/verify-e2e.md +111 -0
  7. package/src/adapters/codex/skills/cf-spec/SKILL.md +1 -0
  8. package/src/adapters/codex/skills/cf-sync/SKILL.md +30 -0
  9. package/src/adapters/codex/skills/cf-task-start/SKILL.md +6 -7
  10. package/src/adapters/costrict/commands/cf-spec.md +13 -0
  11. package/src/adapters/costrict/commands/cf-sync.md +25 -0
  12. package/src/adapters/costrict/commands/cf-task/plan.md +40 -0
  13. package/src/adapters/costrict/commands/cf-task/start.md +6 -7
  14. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +111 -0
  15. package/src/adapters/opencode/commands/cf-spec.md +1 -0
  16. package/src/adapters/opencode/commands/cf-sync.md +29 -0
  17. package/src/adapters/opencode/commands/cf-task/plan.md +40 -5
  18. package/src/adapters/opencode/commands/cf-task/start.md +6 -7
  19. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +111 -0
  20. package/src/adapters/opencode/plugins/code-flow/index.js +3 -3
  21. package/src/core/code-flow/config.yml +2 -1
  22. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +199 -0
  23. package/src/core/code-flow/scripts/cf_acceptance_runner.py +107 -0
  24. package/src/core/code-flow/scripts/cf_core.py +44 -1
  25. package/src/core/code-flow/scripts/cf_post_hook.py +5 -30
  26. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +41 -17
  27. package/src/core/code-flow/scripts/cf_session_state.py +37 -0
  28. package/src/core/code-flow/scripts/cf_spec_context.py +381 -29
  29. package/src/core/code-flow/scripts/cf_spec_gate.py +22 -6
  30. package/src/core/code-flow/scripts/cf_spec_metadata.py +60 -11
  31. package/src/core/code-flow/scripts/cf_spec_resolver.py +78 -7
  32. package/src/core/code-flow/scripts/cf_spec_router.py +107 -10
  33. package/src/core/code-flow/scripts/cf_spec_session.py +5 -9
  34. package/src/core/code-flow/scripts/cf_spec_verify.py +166 -9
  35. package/src/core/code-flow/scripts/cf_stop_hook.py +35 -12
  36. package/src/core/code-flow/scripts/cf_sync.py +140 -0
  37. package/src/core/code-flow/scripts/cf_task_runtime.py +95 -7
  38. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +106 -8
@@ -21,8 +21,6 @@ import os
21
21
  import sys
22
22
 
23
23
  import cf_log
24
- from cf_spec_context import ContextError, load_active_task
25
- from cf_task_runtime import evaluate_scope, run_done_gate
26
24
  from cf_checks import (
27
25
  load_check_state,
28
26
  load_spec_checks,
@@ -39,8 +37,10 @@ from cf_core import (
39
37
  match_domains,
40
38
  normalize_path,
41
39
  normalize_spec_entry,
40
+ resolve_enforcement,
42
41
  resolve_quality_loop,
43
42
  resolve_session_id,
43
+ timing_log,
44
44
  )
45
45
 
46
46
  FEEDBACK_HINT = '如认为误报,直接告诉我"这是误报",我会标记忽略。'
@@ -93,23 +93,6 @@ def _feedback_text(violations: list) -> str:
93
93
  return "\n".join(lines)
94
94
 
95
95
 
96
- def _active_feedback(project_root: str) -> str:
97
- marker = os.path.join(project_root, ".code-flow", ".active-task.json")
98
- if not os.path.exists(marker):
99
- return ""
100
- active = load_active_task(project_root)
101
- task_dir = os.path.join(project_root, active.task_dir)
102
- scope = evaluate_scope(project_root, task_dir)
103
- if scope.decision == "pause":
104
- return scope.message
105
- result = run_done_gate(project_root, task_dir)
106
- failed = [item for item in result.evidence if item.get("status") != "verified"]
107
- if not failed:
108
- return ""
109
- refs = ", ".join(str(item.get("verifier_ref")) for item in failed)
110
- return f"当前 TASK required verifier 未通过:{refs}。修复后再继续。"
111
-
112
-
113
96
  def main() -> None:
114
97
  try:
115
98
  ensure_utf8_io()
@@ -125,20 +108,11 @@ def main() -> None:
125
108
  return
126
109
 
127
110
  project_root = os.getcwd()
128
- try:
129
- active_feedback = _active_feedback(project_root)
130
- except (ContextError, OSError, ValueError) as exc:
131
- message = f"SPEC_WORKFLOW_BLOCKED: active task is invalid: {exc}"
132
- payload = {"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": message}}
133
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
134
- return
135
- if active_feedback:
136
- payload = {"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": active_feedback}}
137
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
138
- return
139
111
  config = load_config(project_root)
140
112
  if not config:
141
113
  return
114
+ if resolve_enforcement(config) == "inject":
115
+ return
142
116
  quality_loop = resolve_quality_loop(config)
143
117
  if not quality_loop["post_check"]:
144
118
  return
@@ -218,3 +192,4 @@ def main() -> None:
218
192
 
219
193
  if __name__ == "__main__":
220
194
  main()
195
+ timing_log("cf_post_hook")
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- """PreToolUse hook: enforce active Context or inject deterministic path Specs."""
2
+ """PreToolUse hook: advisory Context/scope guidance on Edit/Write/MultiEdit.
3
+
4
+ Hot-path policy: never deny the tool; inject context or guidance instead.
5
+ Hard enforcement lives at the checkpoints (start / done / stop), not here.
6
+ """
3
7
 
4
8
  import json
5
9
  import os
@@ -7,11 +11,14 @@ from pathlib import Path
7
11
  import sys
8
12
 
9
13
  import cf_log
10
- from cf_core import _log, ensure_utf8_io, normalize_path, resolve_session_id
14
+ from cf_core import _log, ensure_utf8_io, load_config, normalize_path, resolve_enforcement, resolve_session_id, timing_log
15
+ from cf_session_state import load_session_state, save_session_state
11
16
  from cf_spec_context import load_active_task, load_context
12
- from cf_spec_resolver import resolve_candidates
17
+ from cf_spec_resolver import resolve_candidate_headers
13
18
  from cf_spec_router import RouterError, route_prompt
14
19
 
20
+ _WARN_TEXT = "⚠ Spec Workflow 校验未通过(warn 模式):{message} — 建议运行 cf-spec doctor 检查。"
21
+
15
22
 
16
23
  def _active_expansion(root: str, relative: str) -> tuple[str, ...]:
17
24
  marker = Path(root) / ".code-flow/.active-task.json"
@@ -21,13 +28,13 @@ def _active_expansion(root: str, relative: str) -> tuple[str, ...]:
21
28
  context = load_context(str(Path(root) / active.task_dir / "spec-context.yml"))
22
29
  bound = {item.spec_id for item in context.bindings}
23
30
  return tuple(
24
- item.spec_id for item in resolve_candidates(root, "code", (relative,))
25
- if item.spec_id not in bound and item.metadata.enforcement == "required"
31
+ item.spec_id for item in resolve_candidate_headers(root, "code", (relative,))
32
+ if item.spec_id not in bound and item.enforcement == "required"
26
33
  )
27
34
 
28
35
 
29
- def _deny(reason: str) -> dict[str, object]:
30
- return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": reason}}
36
+ def _inject(text: str) -> dict[str, object]:
37
+ return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": text}}
31
38
 
32
39
 
33
40
  def main() -> None:
@@ -42,24 +49,41 @@ def main() -> None:
42
49
  if tool_name not in {"Edit", "Write", "MultiEdit"} or not isinstance(file_path, str) or not file_path:
43
50
  return
44
51
  root = os.getcwd()
52
+ sid = resolve_session_id(data)
53
+ config = load_config(root)
54
+ enforcement = resolve_enforcement(config) if config else "required"
45
55
  absolute = file_path if os.path.isabs(file_path) else os.path.join(root, file_path)
46
56
  relative = normalize_path(os.path.relpath(absolute, root))
47
- expanded = _active_expansion(root, relative)
57
+ try:
58
+ expanded = _active_expansion(root, relative)
59
+ result = route_prompt(root, (relative,), sid)
60
+ except RouterError as exc:
61
+ if enforcement == "required":
62
+ sys.stdout.write(json.dumps(_inject(f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}. Run cf-spec doctor before continuing."), ensure_ascii=False))
63
+ elif enforcement == "warn":
64
+ sys.stdout.write(json.dumps(_inject(_WARN_TEXT.format(message=str(exc))), ensure_ascii=False))
65
+ return
48
66
  if expanded:
49
- reason = f"SPEC_WORKFLOW_BLOCKED: path introduces required Specs {', '.join(expanded)}; refresh Context and Plan first."
50
- sys.stdout.write(json.dumps(_deny(reason), ensure_ascii=False))
67
+ message = f"⚠ 该路径引入未绑定 required Spec:{', '.join(expanded)} — 建议先 refresh Context / Plan 再继续编辑。"
68
+ sys.stdout.write(json.dumps(_inject(message), ensure_ascii=False))
51
69
  return
52
- sid = resolve_session_id(data)
53
- result = route_prompt(root, (relative,), sid)
54
- if result.text:
55
- payload = {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": result.text}}
56
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
70
+ text = result.text
71
+ if result.mode == "task" and result.context_sha256:
72
+ # The projection is already in context from the prompt route unless
73
+ # the bound rules changed; skip per-edit re-injection.
74
+ state = load_session_state(root)
75
+ if state.get("injected_sha256") == result.context_sha256:
76
+ text = ""
77
+ else:
78
+ state["injected_sha256"] = result.context_sha256
79
+ save_session_state(root, state)
80
+ if text:
81
+ sys.stdout.write(json.dumps(_inject(text), ensure_ascii=False))
57
82
  cf_log.append_event(root, "edit_intent", {"file": relative, "tool": tool_name, "specs": list(result.specs)}, sid)
58
- except RouterError as exc:
59
- sys.stdout.write(json.dumps(_deny(f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}"), ensure_ascii=False))
60
83
  except (json.JSONDecodeError, OSError, ValueError) as exc:
61
84
  _log(f"cf_pre_tool_hook error: {exc}")
62
85
 
63
86
 
64
87
  if __name__ == "__main__":
65
88
  main()
89
+ timing_log("cf_pre_tool_hook")
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ """Shared per-session state for injection/reminder cadence.
3
+
4
+ Stores one JSON file per project root (`.code-flow/.session-state.json`),
5
+ keyed by session id. Both read and write are best-effort: a corrupt or
6
+ unwritable state degrades to no-op rather than breaking hook protocols.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Mapping
13
+
14
+
15
+ _STATE_PATH = ".code-flow/.session-state.json"
16
+
17
+
18
+ def load_session_state(root: str) -> dict:
19
+ """Return the persisted state dict; {} on any failure (never raises)."""
20
+ try:
21
+ value = json.loads((Path(root) / _STATE_PATH).read_text(encoding="utf-8"))
22
+ except (OSError, json.JSONDecodeError, UnicodeError):
23
+ return {}
24
+ return value if isinstance(value, dict) else {}
25
+
26
+
27
+ def save_session_state(root: str, state: Mapping) -> bool:
28
+ """Atomically persist state; False on failure (never raises)."""
29
+ path = Path(root) / _STATE_PATH
30
+ try:
31
+ path.parent.mkdir(parents=True, exist_ok=True)
32
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
33
+ temporary.write_text(json.dumps(dict(state), ensure_ascii=False), encoding="utf-8")
34
+ os.replace(temporary, path)
35
+ return True
36
+ except (OSError, UnicodeError, TypeError):
37
+ return False