@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.
- package/package.json +1 -1
- package/src/adapters/claude/commands/cf-spec.md +13 -0
- package/src/adapters/claude/commands/cf-sync.md +25 -0
- package/src/adapters/claude/commands/cf-task/plan.md +40 -0
- package/src/adapters/claude/commands/cf-task/start.md +6 -7
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +111 -0
- package/src/adapters/codex/skills/cf-spec/SKILL.md +1 -0
- package/src/adapters/codex/skills/cf-sync/SKILL.md +30 -0
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +6 -7
- package/src/adapters/costrict/commands/cf-spec.md +13 -0
- package/src/adapters/costrict/commands/cf-sync.md +25 -0
- package/src/adapters/costrict/commands/cf-task/plan.md +40 -0
- package/src/adapters/costrict/commands/cf-task/start.md +6 -7
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +111 -0
- package/src/adapters/opencode/commands/cf-spec.md +1 -0
- package/src/adapters/opencode/commands/cf-sync.md +29 -0
- package/src/adapters/opencode/commands/cf-task/plan.md +40 -5
- package/src/adapters/opencode/commands/cf-task/start.md +6 -7
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +111 -0
- package/src/adapters/opencode/plugins/code-flow/index.js +3 -3
- package/src/core/code-flow/config.yml +2 -1
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +199 -0
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +107 -0
- package/src/core/code-flow/scripts/cf_core.py +44 -1
- package/src/core/code-flow/scripts/cf_post_hook.py +5 -30
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +41 -17
- package/src/core/code-flow/scripts/cf_session_state.py +37 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +381 -29
- package/src/core/code-flow/scripts/cf_spec_gate.py +22 -6
- package/src/core/code-flow/scripts/cf_spec_metadata.py +60 -11
- package/src/core/code-flow/scripts/cf_spec_resolver.py +78 -7
- package/src/core/code-flow/scripts/cf_spec_router.py +107 -10
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -9
- package/src/core/code-flow/scripts/cf_spec_verify.py +166 -9
- package/src/core/code-flow/scripts/cf_stop_hook.py +35 -12
- package/src/core/code-flow/scripts/cf_sync.py +140 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +95 -7
- 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:
|
|
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
|
|
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
|
|
25
|
-
if item.spec_id not in bound and item.
|
|
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
|
|
30
|
-
return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
sys.stdout.write(json.dumps(
|
|
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
|
-
|
|
53
|
-
result
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|