@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
|
@@ -8,13 +8,30 @@ import sys
|
|
|
8
8
|
|
|
9
9
|
import cf_log
|
|
10
10
|
from cf_checks import detect_correction
|
|
11
|
-
from cf_core import
|
|
11
|
+
from cf_core import (
|
|
12
|
+
_log,
|
|
13
|
+
ensure_utf8_io,
|
|
14
|
+
load_config,
|
|
15
|
+
normalize_path,
|
|
16
|
+
resolve_enforcement,
|
|
17
|
+
resolve_quality_loop,
|
|
18
|
+
resolve_session_id,
|
|
19
|
+
timing_log,
|
|
20
|
+
)
|
|
21
|
+
from cf_session_state import load_session_state, save_session_state
|
|
12
22
|
from cf_spec_router import RouterError, route_prompt
|
|
13
23
|
|
|
14
24
|
|
|
15
25
|
_PATH_RE = re.compile(r'[@`]?([a-zA-Z0-9_.][a-zA-Z0-9_./\\\-]*\.[a-zA-Z]{1,6})(?![a-zA-Z0-9_])')
|
|
16
26
|
_EXT_RE = re.compile(r'\.(py|js|ts|go|rs|java|rb|cs|cpp|c|h)$')
|
|
17
27
|
|
|
28
|
+
_COMPRESS_INTERVAL = 25
|
|
29
|
+
_REINJECT_INTERVAL = 25
|
|
30
|
+
_REMINDER_TEXT = (
|
|
31
|
+
"⚠ 本会话已处理较多轮次,上下文较长;批量任务建议在任务间压缩上下文,或拆到新会话继续。"
|
|
32
|
+
)
|
|
33
|
+
_WARN_TEXT = "⚠ Spec Workflow 校验未通过(warn 模式):{message} — 建议运行 cf-spec doctor 检查。"
|
|
34
|
+
|
|
18
35
|
|
|
19
36
|
def extract_paths_from_prompt(prompt: str) -> list[str]:
|
|
20
37
|
"""Extract explicit file paths without semantic keyword inference."""
|
|
@@ -46,6 +63,49 @@ def _record_correction(root: str, prompt: str, paths: list[str], sid: str) -> No
|
|
|
46
63
|
cf_log.append_event(root, "correction", {"phrase": correction["phrase"], "prompt_head": prompt[:200], "files": paths}, sid)
|
|
47
64
|
|
|
48
65
|
|
|
66
|
+
def _session_reminder(root: str, sid: str) -> str:
|
|
67
|
+
"""One-line context-compress nudge once per COMPRESS_INTERVAL prompts per session.
|
|
68
|
+
|
|
69
|
+
Gated by quality_loop.compress_reminder; disabled → zero IO. Never raises: a
|
|
70
|
+
corrupt state file degrades to no reminder rather than breaking the hook.
|
|
71
|
+
"""
|
|
72
|
+
config = load_config(root)
|
|
73
|
+
if not config or not resolve_quality_loop(config)["compress_reminder"]:
|
|
74
|
+
return ""
|
|
75
|
+
state = load_session_state(root)
|
|
76
|
+
try:
|
|
77
|
+
if state.get("session_id") != sid:
|
|
78
|
+
state = {"session_id": sid, "prompt_count": 0, "next_remind_at": _COMPRESS_INTERVAL}
|
|
79
|
+
state["prompt_count"] = int(state.get("prompt_count", 0)) + 1
|
|
80
|
+
reminder = ""
|
|
81
|
+
if state["prompt_count"] >= int(state.get("next_remind_at", _COMPRESS_INTERVAL)):
|
|
82
|
+
reminder = _REMINDER_TEXT
|
|
83
|
+
state["next_remind_at"] = int(state["next_remind_at"]) + _COMPRESS_INTERVAL
|
|
84
|
+
except (ValueError, TypeError):
|
|
85
|
+
return ""
|
|
86
|
+
save_session_state(root, state)
|
|
87
|
+
return reminder
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _route_with_enforcement(root: str, paths: list[str], sid: str, enforcement: str) -> tuple[object, object]:
|
|
91
|
+
"""Route with enforcement-aware failure handling.
|
|
92
|
+
|
|
93
|
+
required — fail closed with the SPEC_WORKFLOW_BLOCKED message;
|
|
94
|
+
warn — degrade to a short advisory message;
|
|
95
|
+
inject — degrade silently.
|
|
96
|
+
Returns (result, error_payload); exactly one is not None.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
return route_prompt(root, paths, sid), None
|
|
100
|
+
except RouterError as exc:
|
|
101
|
+
if enforcement == "required":
|
|
102
|
+
message = f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}. Run cf-spec doctor before continuing."
|
|
103
|
+
return None, _payload(message, "blocked")
|
|
104
|
+
if enforcement == "warn":
|
|
105
|
+
return None, _payload(_WARN_TEXT.format(message=str(exc)), "warn")
|
|
106
|
+
return None, None
|
|
107
|
+
|
|
108
|
+
|
|
49
109
|
def main() -> None:
|
|
50
110
|
try:
|
|
51
111
|
ensure_utf8_io()
|
|
@@ -58,19 +118,57 @@ def main() -> None:
|
|
|
58
118
|
return
|
|
59
119
|
root = os.getcwd()
|
|
60
120
|
sid = resolve_session_id(data)
|
|
121
|
+
config = load_config(root)
|
|
122
|
+
enforcement = resolve_enforcement(config) if config else "required"
|
|
61
123
|
paths = extract_paths_from_prompt(prompt)
|
|
62
124
|
_record_correction(root, prompt, paths, sid)
|
|
63
|
-
result =
|
|
64
|
-
if not
|
|
125
|
+
result, error_payload = _route_with_enforcement(root, paths, sid, enforcement)
|
|
126
|
+
if error_payload is not None:
|
|
127
|
+
sys.stdout.write(json.dumps(error_payload, ensure_ascii=False))
|
|
65
128
|
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
129
|
+
if result is None:
|
|
130
|
+
return
|
|
131
|
+
reminder = ""
|
|
132
|
+
if config and resolve_quality_loop(config)["compress_reminder"]:
|
|
133
|
+
reminder = _session_reminder(root, sid)
|
|
134
|
+
text = result.text
|
|
135
|
+
if result.mode == "task" and result.context_sha256:
|
|
136
|
+
# Dedup: inject the projection once per session/context, refreshing
|
|
137
|
+
# at the compress cadence so long sessions do not re-pay 12k chars
|
|
138
|
+
# on every prompt.
|
|
139
|
+
state = load_session_state(root)
|
|
140
|
+
count = int(state.get("prompt_count", 0))
|
|
141
|
+
dirty = False
|
|
142
|
+
if not (config and resolve_quality_loop(config)["compress_reminder"]):
|
|
143
|
+
# compress_reminder off → _session_reminder never bumps the
|
|
144
|
+
# counter; maintain the cadence here so the periodic refresh
|
|
145
|
+
# still fires after compaction.
|
|
146
|
+
count += 1
|
|
147
|
+
state["prompt_count"] = count
|
|
148
|
+
dirty = True
|
|
149
|
+
injected = state.get("injected_sha256")
|
|
150
|
+
last_inject = int(state.get("last_inject_prompt", 0))
|
|
151
|
+
if injected == result.context_sha256 and count - last_inject < _REINJECT_INTERVAL:
|
|
152
|
+
text = ""
|
|
153
|
+
else:
|
|
154
|
+
state["injected_sha256"] = result.context_sha256
|
|
155
|
+
state["last_inject_prompt"] = count
|
|
156
|
+
dirty = True
|
|
157
|
+
if dirty:
|
|
158
|
+
save_session_state(root, state)
|
|
159
|
+
if not text and not reminder:
|
|
160
|
+
return
|
|
161
|
+
if text:
|
|
162
|
+
cf_log.append_event(root, "inject", {"specs": list(result.specs), "mode": result.mode, "source": result.mode}, sid)
|
|
163
|
+
if text and reminder:
|
|
164
|
+
text = text.rstrip() + "\n\n" + reminder
|
|
165
|
+
elif reminder:
|
|
166
|
+
text = reminder
|
|
167
|
+
sys.stdout.write(json.dumps(_payload(text, result.mode if text else "reminder"), ensure_ascii=False))
|
|
71
168
|
except (json.JSONDecodeError, UnicodeError, OSError, ValueError) as exc:
|
|
72
169
|
_log(f"cf_user_prompt_hook error: {exc}")
|
|
73
170
|
|
|
74
171
|
|
|
75
172
|
if __name__ == "__main__":
|
|
76
173
|
main()
|
|
174
|
+
timing_log("cf_user_prompt_hook")
|