@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
@@ -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 _log, ensure_utf8_io, normalize_path, resolve_quality_loop, resolve_session_id, load_config
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 = route_prompt(root, paths, sid)
64
- if not result.text:
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
- cf_log.append_event(root, "inject", {"specs": list(result.specs), "mode": result.mode, "source": result.mode}, sid)
67
- sys.stdout.write(json.dumps(_payload(result.text, result.mode), ensure_ascii=False))
68
- except RouterError as exc:
69
- message = f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}. Run cf-spec doctor before continuing."
70
- sys.stdout.write(json.dumps(_payload(message, "blocked"), ensure_ascii=False))
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")