@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.
- package/package.json +1 -1
- package/src/adapters/claude/commands/cf-task/archive.md +5 -5
- package/src/adapters/claude/commands/cf-task/block.md +15 -9
- package/src/adapters/claude/commands/cf-task/graph.md +8 -1
- package/src/adapters/claude/commands/cf-task/note.md +1 -4
- package/src/adapters/claude/commands/cf-task/plan.md +40 -0
- package/src/adapters/claude/commands/cf-task/start.md +22 -22
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +110 -0
- package/src/adapters/claude/commands/cf-validate.md +12 -13
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +5 -5
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -9
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +8 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +1 -4
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +22 -22
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +106 -0
- package/src/adapters/codex/skills/cf-validate/SKILL.md +12 -13
- package/src/adapters/costrict/commands/cf-task/archive.md +5 -5
- package/src/adapters/costrict/commands/cf-task/block.md +15 -9
- package/src/adapters/costrict/commands/cf-task/graph.md +8 -1
- package/src/adapters/costrict/commands/cf-task/note.md +1 -4
- package/src/adapters/costrict/commands/cf-task/plan.md +40 -0
- package/src/adapters/costrict/commands/cf-task/start.md +22 -22
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +110 -0
- package/src/adapters/costrict/commands/cf-validate.md +12 -13
- package/src/adapters/opencode/commands/cf-task/archive.md +5 -5
- package/src/adapters/opencode/commands/cf-task/block.md +15 -9
- package/src/adapters/opencode/commands/cf-task/graph.md +8 -1
- package/src/adapters/opencode/commands/cf-task/note.md +1 -4
- package/src/adapters/opencode/commands/cf-task/plan.md +40 -5
- package/src/adapters/opencode/commands/cf-task/start.md +22 -22
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +110 -0
- package/src/adapters/opencode/commands/cf-validate.md +12 -13
- package/src/adapters/opencode/plugins/code-flow/index.js +50 -28
- package/src/cli.js +177 -63
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +100 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +248 -0
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +192 -0
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +83 -0
- package/src/core/code-flow/scripts/cf_core.py +26 -0
- package/src/core/code-flow/scripts/cf_exec_base.py +112 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +2 -0
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +13 -8
- package/src/core/code-flow/scripts/cf_scan.py +8 -6
- package/src/core/code-flow/scripts/cf_spec_context.py +267 -28
- package/src/core/code-flow/scripts/cf_spec_gate.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_metadata.py +60 -11
- package/src/core/code-flow/scripts/cf_spec_resolver.py +85 -9
- package/src/core/code-flow/scripts/cf_spec_router.py +104 -9
- package/src/core/code-flow/scripts/cf_spec_session.py +3 -2
- package/src/core/code-flow/scripts/cf_spec_verify.py +159 -38
- package/src/core/code-flow/scripts/cf_stats.py +41 -9
- package/src/core/code-flow/scripts/cf_stop_hook.py +100 -59
- package/src/core/code-flow/scripts/cf_task_index.py +147 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +84 -3
- package/src/core/code-flow/scripts/cf_task_state.py +3 -0
- package/src/core/code-flow/scripts/cf_task_workflow.py +107 -0
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +11 -5
- package/src/core/code-flow/scripts/cf_validation.py +81 -0
- package/src/core/code-flow/scripts/cf_workflow_service.py +366 -0
- 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
|
|
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
|
|
32
|
-
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"
|
|
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
|
|
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
|
-
|
|
78
|
+
version = injection_version(root, sid, result.context_sha256)
|
|
79
|
+
if state.get("injected_version") == version:
|
|
76
80
|
text = ""
|
|
77
81
|
else:
|
|
78
|
-
state["
|
|
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
|
-
|
|
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":
|
|
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,
|