@michaelxwb/code-flow 0.6.1 → 0.6.3

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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/adapters/claude/commands/cf-task/archive.md +5 -5
  3. package/src/adapters/claude/commands/cf-task/block.md +15 -9
  4. package/src/adapters/claude/commands/cf-task/graph.md +8 -1
  5. package/src/adapters/claude/commands/cf-task/note.md +1 -4
  6. package/src/adapters/claude/commands/cf-task/plan.md +40 -0
  7. package/src/adapters/claude/commands/cf-task/start.md +22 -22
  8. package/src/adapters/claude/commands/cf-task/verify-e2e.md +110 -0
  9. package/src/adapters/claude/commands/cf-validate.md +12 -13
  10. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +5 -5
  11. package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -9
  12. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +8 -1
  13. package/src/adapters/codex/skills/cf-task-note/SKILL.md +1 -4
  14. package/src/adapters/codex/skills/cf-task-start/SKILL.md +22 -22
  15. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +106 -0
  16. package/src/adapters/codex/skills/cf-validate/SKILL.md +12 -13
  17. package/src/adapters/costrict/commands/cf-task/archive.md +5 -5
  18. package/src/adapters/costrict/commands/cf-task/block.md +15 -9
  19. package/src/adapters/costrict/commands/cf-task/graph.md +8 -1
  20. package/src/adapters/costrict/commands/cf-task/note.md +1 -4
  21. package/src/adapters/costrict/commands/cf-task/plan.md +40 -0
  22. package/src/adapters/costrict/commands/cf-task/start.md +22 -22
  23. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +110 -0
  24. package/src/adapters/costrict/commands/cf-validate.md +12 -13
  25. package/src/adapters/opencode/commands/cf-task/archive.md +5 -5
  26. package/src/adapters/opencode/commands/cf-task/block.md +15 -9
  27. package/src/adapters/opencode/commands/cf-task/graph.md +8 -1
  28. package/src/adapters/opencode/commands/cf-task/note.md +1 -4
  29. package/src/adapters/opencode/commands/cf-task/plan.md +40 -5
  30. package/src/adapters/opencode/commands/cf-task/start.md +22 -22
  31. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +110 -0
  32. package/src/adapters/opencode/commands/cf-validate.md +12 -13
  33. package/src/adapters/opencode/plugins/code-flow/index.js +50 -28
  34. package/src/cli.js +177 -63
  35. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +100 -0
  36. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +248 -0
  37. package/src/core/code-flow/scripts/cf_acceptance_runner.py +192 -0
  38. package/src/core/code-flow/scripts/cf_acceptance_schema.py +83 -0
  39. package/src/core/code-flow/scripts/cf_core.py +26 -0
  40. package/src/core/code-flow/scripts/cf_exec_base.py +112 -0
  41. package/src/core/code-flow/scripts/cf_post_hook.py +2 -0
  42. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +13 -8
  43. package/src/core/code-flow/scripts/cf_scan.py +8 -6
  44. package/src/core/code-flow/scripts/cf_spec_context.py +267 -28
  45. package/src/core/code-flow/scripts/cf_spec_gate.py +10 -0
  46. package/src/core/code-flow/scripts/cf_spec_metadata.py +60 -11
  47. package/src/core/code-flow/scripts/cf_spec_resolver.py +85 -9
  48. package/src/core/code-flow/scripts/cf_spec_router.py +104 -9
  49. package/src/core/code-flow/scripts/cf_spec_session.py +3 -2
  50. package/src/core/code-flow/scripts/cf_spec_verify.py +159 -38
  51. package/src/core/code-flow/scripts/cf_stats.py +41 -9
  52. package/src/core/code-flow/scripts/cf_stop_hook.py +100 -59
  53. package/src/core/code-flow/scripts/cf_task_index.py +147 -0
  54. package/src/core/code-flow/scripts/cf_task_runtime.py +84 -3
  55. package/src/core/code-flow/scripts/cf_task_state.py +3 -0
  56. package/src/core/code-flow/scripts/cf_task_workflow.py +107 -0
  57. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +11 -5
  58. package/src/core/code-flow/scripts/cf_validation.py +81 -0
  59. package/src/core/code-flow/scripts/cf_workflow_service.py +366 -0
  60. package/src/core/code-flow/scripts/cf_workflow_transaction.py +96 -0
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """Shared execution substrate: deadline-aware argv subprocess runner.
3
+
4
+ All check executors (acceptance scenarios, Spec verifiers, session
5
+ validators) funnel through here so one entry deadline constrains the whole
6
+ chain, commands never pass through a shell, and unrun work is reported as
7
+ `incomplete` instead of silently skipped or falsely passed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shlex
13
+ import json
14
+ import subprocess
15
+ import time
16
+ from contextlib import contextmanager
17
+ from contextvars import ContextVar
18
+ from pathlib import Path
19
+ from typing import Iterator, Mapping, Optional, Sequence
20
+
21
+ _EXECUTIONS: ContextVar[Optional[dict[str, dict[str, object]]]] = ContextVar("cf_executions", default=None)
22
+
23
+
24
+ @contextmanager
25
+ def execution_session() -> Iterator[None]:
26
+ """Share results only inside one validation operation, never across turns."""
27
+ if _EXECUTIONS.get() is not None:
28
+ yield
29
+ return
30
+ token = _EXECUTIONS.set({})
31
+ try:
32
+ yield
33
+ finally:
34
+ _EXECUTIONS.reset(token)
35
+
36
+
37
+ def invalidate_executions() -> None:
38
+ cache = _EXECUTIONS.get()
39
+ if cache is not None:
40
+ cache.clear()
41
+
42
+
43
+ def execution_key(argv: Sequence[str], cwd: str, timeout: float) -> str:
44
+ """Identity of a command under the current process environment."""
45
+ return json.dumps([list(argv), str(Path(cwd).resolve()), float(timeout)], ensure_ascii=False)
46
+
47
+
48
+ def remaining_seconds(deadline: Optional[float]) -> Optional[float]:
49
+ """Seconds left until the absolute monotonic deadline (None = unbounded)."""
50
+ if deadline is None:
51
+ return None
52
+ return deadline - time.monotonic()
53
+
54
+
55
+ def build_argv(template: str, files: Sequence[str]) -> list[str]:
56
+ """Expand a validator command template into argv without a shell.
57
+
58
+ `{files}` must be a standalone token and expands to one argv entry per
59
+ file (spaces/metachars safe). Embedded usage is a config error — refusing
60
+ is safer than reintroducing shell splitting.
61
+ """
62
+ tokens = shlex.split(template or "")
63
+ if not tokens:
64
+ return []
65
+ argv: list[str] = []
66
+ for token in tokens:
67
+ if token == "{files}":
68
+ argv.extend(files)
69
+ elif "{files}" in token:
70
+ raise ValueError(f"validator command must keep {{files}} as a standalone token: {template!r}")
71
+ else:
72
+ argv.append(token)
73
+ return argv
74
+
75
+
76
+ def run_command(
77
+ argv: Sequence[str],
78
+ cwd: str,
79
+ timeout: float,
80
+ deadline: Optional[float] = None,
81
+ ) -> dict[str, object]:
82
+ """Run argv synchronously under timeout+deadline. Never uses a shell."""
83
+ key = execution_key(argv, cwd, timeout)
84
+ cache = _EXECUTIONS.get()
85
+ if cache is not None and key in cache:
86
+ return dict(cache[key])
87
+ result = _execute(argv, cwd, timeout, deadline)
88
+ if cache is not None and result["status"] == "ok":
89
+ cache[key] = result
90
+ return result
91
+
92
+
93
+ def _execute(argv: Sequence[str], cwd: str, timeout: float, deadline: Optional[float]) -> dict[str, object]:
94
+ remaining = remaining_seconds(deadline)
95
+ if remaining is not None and remaining <= 0:
96
+ return {"status": "deadline_exceeded", "returncode": None, "stdout": "", "stderr": ""}
97
+ effective = min(timeout, remaining) if remaining is not None else timeout
98
+ try:
99
+ proc = subprocess.run(
100
+ list(argv), cwd=cwd, capture_output=True, text=True, timeout=max(effective, 0.01)
101
+ )
102
+ except subprocess.TimeoutExpired as exc:
103
+ out = exc.stdout
104
+ return {
105
+ "status": "timeout",
106
+ "returncode": None,
107
+ "stdout": (out.decode() if isinstance(out, bytes) else (out or ""))[-4000:],
108
+ "stderr": "",
109
+ }
110
+ except OSError as exc:
111
+ return {"status": "spawn_error", "returncode": None, "stdout": "", "stderr": str(exc)}
112
+ return {"status": "ok", "returncode": proc.returncode, "stdout": proc.stdout or "", "stderr": proc.stderr or ""}
@@ -40,6 +40,7 @@ from cf_core import (
40
40
  resolve_enforcement,
41
41
  resolve_quality_loop,
42
42
  resolve_session_id,
43
+ timing_log,
43
44
  )
44
45
 
45
46
  FEEDBACK_HINT = '如认为误报,直接告诉我"这是误报",我会标记忽略。'
@@ -191,3 +192,4 @@ def main() -> None:
191
192
 
192
193
  if __name__ == "__main__":
193
194
  main()
195
+ timing_log("cf_post_hook")
@@ -11,10 +11,10 @@ from pathlib import Path
11
11
  import sys
12
12
 
13
13
  import cf_log
14
- from cf_core import _log, ensure_utf8_io, load_config, normalize_path, resolve_enforcement, resolve_session_id
14
+ from cf_core import _log, ensure_utf8_io, load_config, normalize_path, resolve_enforcement, resolve_session_id, timing_log
15
15
  from cf_session_state import load_session_state, save_session_state
16
- from cf_spec_context import load_active_task, load_context
17
- from cf_spec_resolver import resolve_candidates
16
+ from cf_spec_context import injection_version, load_active_task, load_context
17
+ from cf_spec_resolver import resolve_candidate_headers
18
18
  from cf_spec_router import RouterError, route_prompt
19
19
 
20
20
  _WARN_TEXT = "⚠ Spec Workflow 校验未通过(warn 模式):{message} — 建议运行 cf-spec doctor 检查。"
@@ -28,8 +28,9 @@ def _active_expansion(root: str, relative: str) -> tuple[str, ...]:
28
28
  context = load_context(str(Path(root) / active.task_dir / "spec-context.yml"))
29
29
  bound = {item.spec_id for item in context.bindings}
30
30
  return tuple(
31
- item.spec_id for item in resolve_candidates(root, "code", (relative,))
32
- 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"
33
+ and getattr(item, "scope", "path") != "unmatched"
33
34
  )
34
35
 
35
36
 
@@ -70,12 +71,15 @@ def main() -> None:
70
71
  text = result.text
71
72
  if result.mode == "task" and result.context_sha256:
72
73
  # The projection is already in context from the prompt route unless
73
- # the bound rules changed; skip per-edit re-injection.
74
+ # the session/task/contract version changed; skip per-edit
75
+ # re-injection. Same version key as the prompt hook so a TASK
76
+ # switch re-injects here too.
74
77
  state = load_session_state(root)
75
- if state.get("injected_sha256") == result.context_sha256:
78
+ version = injection_version(root, sid, result.context_sha256)
79
+ if state.get("injected_version") == version:
76
80
  text = ""
77
81
  else:
78
- state["injected_sha256"] = result.context_sha256
82
+ state["injected_version"] = version
79
83
  save_session_state(root, state)
80
84
  if text:
81
85
  sys.stdout.write(json.dumps(_inject(text), ensure_ascii=False))
@@ -86,3 +90,4 @@ def main() -> None:
86
90
 
87
91
  if __name__ == "__main__":
88
92
  main()
93
+ timing_log("cf_pre_tool_hook")
@@ -11,6 +11,7 @@ from cf_core import (
11
11
  build_spec_catalog,
12
12
  estimate_tokens,
13
13
  load_config,
14
+ project_instruction_file,
14
15
  non_injectable_specs,
15
16
  parse_spec_frontmatter,
16
17
  )
@@ -120,19 +121,20 @@ def find_missing_paths(text: str, project_root: str, base_dir: str = "") -> list
120
121
  return sorted(missing)
121
122
 
122
123
 
123
- def build_report(project_root: str) -> dict:
124
+ def build_report(project_root: str, platform: str = "") -> dict:
124
125
  """规范质量审计数据(cf-scan 引擎,亦供 cf-stats --audit 复用)。
125
126
 
126
127
  files 含 issues / template 标记,review 为待复审清单;展示层负责过滤与渲染。"""
127
128
  files = []
128
129
  total_tokens = 0
129
130
 
130
- claude_path = os.path.join(project_root, "CLAUDE.md")
131
+ instruction_file = project_instruction_file(project_root, platform)
132
+ claude_path = os.path.join(project_root, instruction_file)
131
133
  if os.path.exists(claude_path):
132
134
  content = read_text(claude_path)
133
135
  tokens = estimate_tokens(content)
134
136
  total_tokens += tokens
135
- files.append({"path": "CLAUDE.md", "tokens": tokens, "issues": []})
137
+ files.append({"path": instruction_file, "tokens": tokens, "issues": []})
136
138
 
137
139
  specs_root = os.path.join(project_root, ".code-flow", "specs")
138
140
  specs = []
@@ -142,14 +144,14 @@ def build_report(project_root: str) -> dict:
142
144
  if not name.endswith(".md"):
143
145
  continue
144
146
  full_path = os.path.join(root, name)
147
+ rel = os.path.relpath(full_path, specs_root)
148
+ if rel.replace(os.sep, "/").startswith("_session/"):
149
+ continue
145
150
  content = read_text(full_path)
146
151
  if not content:
147
152
  continue
148
153
  tokens = estimate_tokens(content)
149
154
  total_tokens += tokens
150
- rel = os.path.relpath(full_path, specs_root)
151
- if rel.replace(os.sep, "/").startswith("_session/"):
152
- continue # 会话级临时约束(FEAT-08)不参与审计与预算
153
155
  rel_path = os.path.join("specs", rel).replace(os.sep, "/")
154
156
  spec_entry = {
155
157
  "path": rel_path,