@michaelxwb/code-flow 0.6.5 → 1.0.0

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 (177) hide show
  1. package/README.md +75 -110
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +268 -0
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +21 -9
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  54. package/src/adapters/claude/CLAUDE.md +0 -45
  55. package/src/adapters/claude/commands/cf-init.md +0 -264
  56. package/src/adapters/claude/commands/cf-learn.md +0 -291
  57. package/src/adapters/claude/commands/cf-spec.md +0 -67
  58. package/src/adapters/claude/commands/cf-stats.md +0 -86
  59. package/src/adapters/claude/commands/cf-sync.md +0 -25
  60. package/src/adapters/claude/commands/cf-task/align.md +0 -309
  61. package/src/adapters/claude/commands/cf-task/archive.md +0 -161
  62. package/src/adapters/claude/commands/cf-task/block.md +0 -36
  63. package/src/adapters/claude/commands/cf-task/graph.md +0 -80
  64. package/src/adapters/claude/commands/cf-task/note.md +0 -236
  65. package/src/adapters/claude/commands/cf-task/plan.md +0 -363
  66. package/src/adapters/claude/commands/cf-task/prd.md +0 -162
  67. package/src/adapters/claude/commands/cf-task/start.md +0 -304
  68. package/src/adapters/claude/commands/cf-task/status.md +0 -59
  69. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
  70. package/src/adapters/claude/commands/cf-validate.md +0 -75
  71. package/src/adapters/claude/settings.local.json +0 -50
  72. package/src/adapters/codex/AGENTS.md +0 -45
  73. package/src/adapters/codex/config.toml +0 -3
  74. package/src/adapters/codex/hooks.json +0 -50
  75. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  76. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  77. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
  78. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
  79. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
  80. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
  81. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
  82. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
  83. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
  84. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
  85. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
  86. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
  87. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
  88. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
  89. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
  90. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
  91. package/src/adapters/costrict/CLAUDE.md +0 -45
  92. package/src/adapters/costrict/commands/cf-init.md +0 -264
  93. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  94. package/src/adapters/costrict/commands/cf-spec.md +0 -67
  95. package/src/adapters/costrict/commands/cf-stats.md +0 -86
  96. package/src/adapters/costrict/commands/cf-sync.md +0 -25
  97. package/src/adapters/costrict/commands/cf-task/align.md +0 -309
  98. package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
  99. package/src/adapters/costrict/commands/cf-task/block.md +0 -36
  100. package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
  101. package/src/adapters/costrict/commands/cf-task/note.md +0 -236
  102. package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
  103. package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
  104. package/src/adapters/costrict/commands/cf-task/start.md +0 -304
  105. package/src/adapters/costrict/commands/cf-task/status.md +0 -59
  106. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
  107. package/src/adapters/costrict/commands/cf-validate.md +0 -75
  108. package/src/adapters/costrict/settings.local.json +0 -50
  109. package/src/adapters/opencode/AGENTS.md +0 -45
  110. package/src/adapters/opencode/commands/cf-init.md +0 -281
  111. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  112. package/src/adapters/opencode/commands/cf-spec.md +0 -59
  113. package/src/adapters/opencode/commands/cf-stats.md +0 -90
  114. package/src/adapters/opencode/commands/cf-sync.md +0 -29
  115. package/src/adapters/opencode/commands/cf-task/align.md +0 -314
  116. package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
  117. package/src/adapters/opencode/commands/cf-task/block.md +0 -40
  118. package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
  119. package/src/adapters/opencode/commands/cf-task/note.md +0 -240
  120. package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
  121. package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
  122. package/src/adapters/opencode/commands/cf-task/start.md +0 -309
  123. package/src/adapters/opencode/commands/cf-task/status.md +0 -63
  124. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
  125. package/src/adapters/opencode/commands/cf-validate.md +0 -79
  126. package/src/adapters/opencode/opencode.json +0 -3
  127. package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
  128. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  129. package/src/cli.js +0 -1023
  130. package/src/core/code-flow/config.yml +0 -158
  131. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  132. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
  133. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
  134. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  135. package/src/core/code-flow/scripts/cf_checks.py +0 -336
  136. package/src/core/code-flow/scripts/cf_core.py +0 -787
  137. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  138. package/src/core/code-flow/scripts/cf_feedback.py +0 -56
  139. package/src/core/code-flow/scripts/cf_log.py +0 -154
  140. package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
  141. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
  142. package/src/core/code-flow/scripts/cf_scan.py +0 -295
  143. package/src/core/code-flow/scripts/cf_session_state.py +0 -37
  144. package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
  145. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
  146. package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
  147. package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
  148. package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
  149. package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
  150. package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
  151. package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
  152. package/src/core/code-flow/scripts/cf_stats.py +0 -506
  153. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
  154. package/src/core/code-flow/scripts/cf_sync.py +0 -140
  155. package/src/core/code-flow/scripts/cf_task_index.py +0 -148
  156. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
  157. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  158. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  159. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
  160. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
  161. package/src/core/code-flow/scripts/cf_validation.py +0 -81
  162. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  163. package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
  164. package/src/core/code-flow/specs/backend/_map.md +0 -56
  165. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  166. package/src/core/code-flow/specs/backend/database.md +0 -51
  167. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  168. package/src/core/code-flow/specs/backend/logging.md +0 -50
  169. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  170. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  171. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  172. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  173. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  174. package/src/core/code-flow/specs/shared/_map.md +0 -57
  175. package/src/core/code-flow/tasks/.gitkeep +0 -0
  176. package/src/core/code-flow/validation.yml +0 -53
  177. package/src/migrate/spec-workflow.js +0 -238
@@ -1,112 +0,0 @@
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 ""}
@@ -1,56 +0,0 @@
1
- #!/usr/bin/env python3
2
- """cf_feedback: mark a check as false positive (FEAT-02 对话即反馈).
3
-
4
- The violation feedback text tells the user "误报可直接告诉我"; the agent then
5
- runs this CLI on the user's behalf — no new slash command, no config editing.
6
-
7
- Usage: python3 cf_feedback.py ignore <check-id>
8
- Exit codes: 0 = recorded / 1 = usage error / 2 = unknown check id.
9
- stdout: one JSON line {"check_id", "fp_count", "hit_count", "disabled"}.
10
- """
11
- import json
12
- import os
13
- import sys
14
-
15
- from cf_checks import load_check_state, load_spec_checks, record_false_positive
16
- from cf_core import build_effective_mapping, load_config, normalize_spec_entry
17
- from cf_log import append_event
18
-
19
-
20
- def known_check_ids(project_root: str) -> set:
21
- """All check ids: declared in any configured spec, or already in state."""
22
- ids = set(load_check_state(project_root).keys())
23
- config = load_config(project_root)
24
- mapping = build_effective_mapping(
25
- project_root, (config or {}).get("path_mapping") or {}
26
- )
27
- specs_root = os.path.join(project_root, ".code-flow", "specs")
28
- for domain_cfg in mapping.values():
29
- for entry in (domain_cfg or {}).get("specs") or []:
30
- rel = normalize_spec_entry(entry).get("path")
31
- if not rel:
32
- continue
33
- checks, _ = load_spec_checks(os.path.join(specs_root, rel))
34
- ids.update(check["id"] for check in checks)
35
- return ids
36
-
37
-
38
- def main(argv: list) -> int:
39
- if len(argv) != 2 or argv[0] != "ignore":
40
- sys.stderr.write("usage: cf_feedback.py ignore <check-id>\n")
41
- return 1
42
- check_id = argv[1]
43
- project_root = os.getcwd()
44
- if check_id not in known_check_ids(project_root):
45
- sys.stderr.write(f"unknown check id: {check_id}\n")
46
- return 2
47
- append_event(
48
- project_root, "false_positive", {"check_id": check_id}, "cf-feedback"
49
- )
50
- result = record_false_positive(project_root, check_id)
51
- sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
52
- return 0
53
-
54
-
55
- if __name__ == "__main__":
56
- sys.exit(main(sys.argv[1:]))
@@ -1,154 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Session activity log: JSONL event stream for the quality-loop feature.
3
-
4
- Design contract (spec-quality-loop.design.md §3.3):
5
- - One event per line: {"v", "ts", "sid", "event", "data"}.
6
- - append is O(1) single-write; failures are swallowed (stderr only) so the
7
- hook protocol is never broken by logging (RULE-01 / E-03).
8
- - Reads are line-tolerant: corrupt lines are skipped, never raised (RULE-05).
9
- - Rotation: when the live file reaches MAX_BYTES it is archived to
10
- sessions/YYYY-MM/session-log-<ts>.jsonl and a fresh file starts (B-02).
11
- """
12
- import json
13
- import os
14
- import sys
15
- from datetime import datetime, timedelta
16
-
17
- LOG_NAME = ".session-log.jsonl"
18
- SESSIONS_DIR = "sessions"
19
- MAX_BYTES = 5 * 1024 * 1024
20
- SCHEMA_VERSION = 1
21
-
22
- KNOWN_EVENTS = (
23
- "inject",
24
- "edit",
25
- "violation",
26
- "correction",
27
- "false_positive",
28
- "degrade",
29
- "stop_check",
30
- "spec_candidate",
31
- "spec_bound",
32
- "spec_applied",
33
- "spec_read",
34
- "spec_drift",
35
- "spec_gate",
36
- "late_violation",
37
- "metrics_degraded",
38
- )
39
-
40
-
41
- def _log(msg: str) -> None:
42
- sys.stderr.write(f"[cf-log] {msg}\n")
43
-
44
-
45
- def log_path(project_root: str) -> str:
46
- return os.path.join(project_root, ".code-flow", LOG_NAME)
47
-
48
-
49
- def _archive_dir(project_root: str, month: str) -> str:
50
- return os.path.join(project_root, ".code-flow", SESSIONS_DIR, month)
51
-
52
-
53
- def _rotate_if_needed(project_root: str, path: str) -> None:
54
- try:
55
- if os.path.getsize(path) < MAX_BYTES:
56
- return
57
- except OSError:
58
- return
59
- now = datetime.now()
60
- month_dir = _archive_dir(project_root, now.strftime("%Y-%m"))
61
- os.makedirs(month_dir, exist_ok=True)
62
- target = os.path.join(
63
- month_dir, f"session-log-{now.strftime('%Y%m%dT%H%M%S')}.jsonl"
64
- )
65
- os.replace(path, target)
66
-
67
-
68
- def append_event(project_root: str, event: str, data: dict, sid: str) -> bool:
69
- """Append one event line. Never raises; returns False on failure (E-03)."""
70
- try:
71
- path = log_path(project_root)
72
- os.makedirs(os.path.dirname(path), exist_ok=True)
73
- _rotate_if_needed(project_root, path)
74
- line = json.dumps(
75
- {
76
- "v": SCHEMA_VERSION,
77
- "ts": datetime.now().isoformat(timespec="seconds"),
78
- "sid": sid,
79
- "event": event,
80
- "data": data or {},
81
- },
82
- ensure_ascii=False,
83
- )
84
- with open(path, "a", encoding="utf-8") as f:
85
- f.write(line + "\n")
86
- return True
87
- except Exception as exc:
88
- _log(f"append_event failed: {exc}")
89
- return False
90
-
91
-
92
- def degrade(project_root: str, component: str, error: object, sid: str = "") -> None:
93
- """Record a degrade event (FEAT-02). Never raises — degrading the
94
- degrade-logger itself must stay invisible (RULE-01)."""
95
- append_event(
96
- project_root,
97
- "degrade",
98
- {"component": component, "error": str(error)[:200]},
99
- sid,
100
- )
101
-
102
-
103
- def _candidate_files(project_root: str, days: int) -> list:
104
- """Live log + archived months overlapping the [now-days, now] window."""
105
- paths = []
106
- base = os.path.join(project_root, ".code-flow", SESSIONS_DIR)
107
- if os.path.isdir(base):
108
- cutoff = datetime.now() - timedelta(days=days)
109
- for month in sorted(os.listdir(base)):
110
- try:
111
- month_start = datetime.strptime(month, "%Y-%m")
112
- except ValueError:
113
- continue
114
- # keep a month if any part of it can fall inside the window
115
- if month_start >= cutoff.replace(day=1):
116
- month_dir = os.path.join(base, month)
117
- paths.extend(
118
- os.path.join(month_dir, name)
119
- for name in sorted(os.listdir(month_dir))
120
- if name.endswith(".jsonl")
121
- )
122
- live = log_path(project_root)
123
- if os.path.exists(live):
124
- paths.append(live)
125
- return paths
126
-
127
-
128
- def read_events(project_root: str, days: int = 30, events: tuple = ()) -> list:
129
- """Read events within the window, oldest first. Corrupt lines skipped."""
130
- cutoff = (datetime.now() - timedelta(days=days)).isoformat(timespec="seconds")
131
- wanted = set(events) if events else None
132
- out = []
133
- for path in _candidate_files(project_root, days):
134
- try:
135
- with open(path, "r", encoding="utf-8") as f:
136
- for raw in f:
137
- raw = raw.strip()
138
- if not raw:
139
- continue
140
- try:
141
- item = json.loads(raw)
142
- except Exception:
143
- continue
144
- if not isinstance(item, dict) or "event" not in item:
145
- continue
146
- if wanted is not None and item["event"] not in wanted:
147
- continue
148
- if str(item.get("ts", "")) < cutoff:
149
- continue
150
- out.append(item)
151
- except Exception as exc:
152
- _log(f"read_events skipped {path}: {exc}")
153
- continue
154
- return out
@@ -1,196 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PostToolUse Hook: compliance feedback after Edit/Write (FEAT-01).
3
-
4
- Flow (spec-quality-loop.design.md §3.4):
5
- edited file → matched domains → aggregate frontmatter checks of their
6
- constraint specs → run against on-disk content → feedback via
7
- additionalContext (advice only, never blocking) → violation events.
8
-
9
- Protocol invariants (RULE-01/02):
10
- - always exit 0, stdout is JSON-only or empty (no-op);
11
- - violations only warn — there is no error severity;
12
- - same check+file reported once per session (RISK-05);
13
- - any internal failure degrades silently (stderr + degrade event).
14
-
15
- Input (stdin): {"tool_name", "tool_input": {"file_path"}, "session_id"}
16
- Output: {"hookSpecificOutput": {"hookEventName": "PostToolUse",
17
- "additionalContext": "..."}} or empty.
18
- """
19
- import json
20
- import os
21
- import sys
22
-
23
- import cf_log
24
- from cf_checks import (
25
- load_check_state,
26
- load_spec_checks,
27
- record_hits,
28
- run_checks,
29
- save_check_state,
30
- )
31
- from cf_core import (
32
- _log,
33
- build_effective_mapping,
34
- effective_project_root,
35
- ensure_utf8_io,
36
- is_code_file,
37
- load_config,
38
- match_domains,
39
- normalize_path,
40
- normalize_spec_entry,
41
- resolve_enforcement,
42
- resolve_quality_loop,
43
- resolve_session_id,
44
- timing_log,
45
- )
46
-
47
- FEEDBACK_HINT = '如认为误报,直接告诉我"这是误报",我会标记忽略。'
48
- _REPORTED_KEY = "_reported"
49
-
50
-
51
- def _collect_checks(project_root: str, domains: list, mapping: dict) -> list:
52
- """Aggregate checks of all constraint specs in matched domains.
53
-
54
- Each check is annotated with its source spec path. Checks apply whenever
55
- their own `files` glob matches — independent of injection tag gating.
56
- """
57
- specs_root = os.path.join(project_root, ".code-flow", "specs")
58
- collected = []
59
- seen_specs = set()
60
- for domain in domains:
61
- for entry in (mapping.get(domain) or {}).get("specs") or []:
62
- cfg = normalize_spec_entry(entry)
63
- rel = cfg.get("path")
64
- if not rel or rel in seen_specs or not cfg.get("tags"):
65
- continue
66
- seen_specs.add(rel)
67
- checks, _ = load_spec_checks(os.path.join(specs_root, rel))
68
- for check in checks:
69
- item = dict(check)
70
- item["spec"] = rel
71
- collected.append(item)
72
- return collected
73
-
74
-
75
- def _session_reported(state: dict, sid: str) -> set:
76
- session = state.get(_REPORTED_KEY) or {}
77
- if session.get("sid") != sid:
78
- return set()
79
- return set(session.get("keys") or [])
80
-
81
-
82
- def _save_reported(project_root: str, state: dict, sid: str, keys: set) -> None:
83
- state[_REPORTED_KEY] = {"sid": sid, "keys": sorted(keys)}
84
- save_check_state(project_root, state)
85
-
86
-
87
- def _feedback_text(violations: list) -> str:
88
- lines = ["## Spec 合规反馈 (auto-check)", ""]
89
- for v in violations:
90
- lines.append(f"⚠ {v['message']}(规则: {v['spec']}#{v['check_id']})")
91
- lines.append(f" 违规行 {v['line_no']}: {v['line']}")
92
- lines.append("")
93
- lines.append("请修正以上违规后再继续。" + FEEDBACK_HINT)
94
- return "\n".join(lines)
95
-
96
-
97
- def main() -> None:
98
- try:
99
- ensure_utf8_io()
100
- raw = sys.stdin.read()
101
- if not raw.strip():
102
- return
103
- data = json.loads(raw)
104
- tool_name = data.get("tool_name", "")
105
- file_path = (data.get("tool_input") or {}).get("file_path", "")
106
- if tool_name not in {"Edit", "Write", "MultiEdit"}:
107
- return
108
- if not isinstance(file_path, str) or not file_path:
109
- return
110
-
111
- session_root = os.getcwd()
112
- abs_path = file_path if os.path.isabs(file_path) else os.path.join(session_root, file_path)
113
- # 并行子 agent 在 .code-flow/worktrees/... 内编辑时路由到该 worktree 的规格与状态
114
- project_root = effective_project_root(session_root, abs_path)
115
- config = load_config(project_root)
116
- if not config:
117
- return
118
- if resolve_enforcement(config) == "inject":
119
- return
120
- quality_loop = resolve_quality_loop(config)
121
- if not quality_loop["post_check"]:
122
- return
123
-
124
- rel_path = normalize_path(os.path.relpath(abs_path, project_root))
125
- sid = resolve_session_id(data)
126
- cf_log.append_event(project_root, "edit", {"file": rel_path, "tool": tool_name}, sid)
127
- if not is_code_file(rel_path, config.get("quality_loop") or {}):
128
- return
129
- mapping = build_effective_mapping(
130
- project_root, config.get("path_mapping") or {}
131
- )
132
- domains = match_domains(rel_path, mapping)
133
- if not domains:
134
- return
135
-
136
- checks = _collect_checks(project_root, domains, mapping)
137
- if not checks:
138
- return
139
-
140
- try:
141
- with open(abs_path, "r", encoding="utf-8") as f:
142
- content = f.read()
143
- except Exception:
144
- return
145
-
146
- state = load_check_state(project_root)
147
- violations, skipped = run_checks(checks, rel_path, content, state)
148
- for item in skipped:
149
- cf_log.degrade(
150
- project_root, "post_check",
151
- f"{item['check_id']}:{item['reason']}", sid,
152
- )
153
- if not violations:
154
- return
155
-
156
- spec_by_check = {c["id"]: c["spec"] for c in checks}
157
- reported = _session_reported(state, sid)
158
- fresh = []
159
- for v in violations:
160
- key = f"{v['check_id']}|{v['file']}"
161
- if key in reported:
162
- continue
163
- v["spec"] = spec_by_check.get(v["check_id"], "")
164
- fresh.append(v)
165
- reported.add(key)
166
- if not fresh:
167
- return
168
-
169
- # hit_count feeds the false-positive rate denominator (RULE-04)
170
- record_hits(project_root, sorted({v["check_id"] for v in fresh}))
171
- state = load_check_state(project_root)
172
- _save_reported(project_root, state, sid, reported)
173
-
174
- for v in fresh:
175
- cf_log.append_event(
176
- project_root, "violation",
177
- {"check_id": v["check_id"], "spec": v["spec"],
178
- "file": v["file"], "severity": v["severity"]},
179
- sid,
180
- )
181
-
182
- payload = {
183
- "hookSpecificOutput": {
184
- "hookEventName": "PostToolUse",
185
- "additionalContext": _feedback_text(fresh),
186
- }
187
- }
188
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
189
- except Exception as exc:
190
- _log(f"cf_post_hook error: {exc}")
191
- return
192
-
193
-
194
- if __name__ == "__main__":
195
- main()
196
- timing_log("cf_post_hook")
@@ -1,104 +0,0 @@
1
- #!/usr/bin/env python3
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
- """
7
-
8
- import json
9
- import os
10
- from pathlib import Path
11
- import sys
12
-
13
- import cf_log
14
- from cf_core import (
15
- _log,
16
- effective_project_root,
17
- ensure_utf8_io,
18
- load_config,
19
- normalize_path,
20
- resolve_enforcement,
21
- resolve_session_id,
22
- timing_log,
23
- )
24
- from cf_session_state import load_session_state, save_session_state
25
- from cf_spec_context import injection_version, load_active_task, load_context
26
- from cf_spec_resolver import resolve_candidate_headers
27
- from cf_spec_router import RouterError, route_prompt
28
-
29
- _WARN_TEXT = "⚠ Spec Workflow 校验未通过(warn 模式):{message} — 建议运行 cf-spec doctor 检查。"
30
-
31
-
32
- def _active_expansion(root: str, relative: str) -> tuple[str, ...]:
33
- marker = Path(root) / ".code-flow/.active-task.json"
34
- if not marker.exists():
35
- return ()
36
- active = load_active_task(root)
37
- context = load_context(str(Path(root) / active.task_dir / "spec-context.yml"))
38
- bound = {item.spec_id for item in context.bindings}
39
- return tuple(
40
- item.spec_id for item in resolve_candidate_headers(root, "code", (relative,))
41
- if item.spec_id not in bound and item.enforcement == "required"
42
- and getattr(item, "scope", "path") != "unmatched"
43
- )
44
-
45
-
46
- def _inject(text: str) -> dict[str, object]:
47
- return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": text}}
48
-
49
-
50
- def main() -> None:
51
- try:
52
- ensure_utf8_io()
53
- raw = sys.stdin.read()
54
- if not raw.strip():
55
- return
56
- data = json.loads(raw)
57
- tool_name = data.get("tool_name", "")
58
- file_path = (data.get("tool_input") or {}).get("file_path", "")
59
- if tool_name not in {"Edit", "Write", "MultiEdit"} or not isinstance(file_path, str) or not file_path:
60
- return
61
- session_root = os.getcwd()
62
- absolute = file_path if os.path.isabs(file_path) else os.path.join(session_root, file_path)
63
- # 并行子 agent 在 .code-flow/worktrees/... 内编辑时路由到该 worktree 的上下文
64
- root = effective_project_root(session_root, absolute)
65
- sid = resolve_session_id(data)
66
- config = load_config(root)
67
- enforcement = resolve_enforcement(config) if config else "required"
68
- relative = normalize_path(os.path.relpath(absolute, root))
69
- try:
70
- expanded = _active_expansion(root, relative)
71
- result = route_prompt(root, (relative,), sid)
72
- except RouterError as exc:
73
- if enforcement == "required":
74
- sys.stdout.write(json.dumps(_inject(f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}. Run cf-spec doctor before continuing."), ensure_ascii=False))
75
- elif enforcement == "warn":
76
- sys.stdout.write(json.dumps(_inject(_WARN_TEXT.format(message=str(exc))), ensure_ascii=False))
77
- return
78
- if expanded:
79
- message = f"⚠ 该路径引入未绑定 required Spec:{', '.join(expanded)} — 建议先 refresh Context / Plan 再继续编辑。"
80
- sys.stdout.write(json.dumps(_inject(message), ensure_ascii=False))
81
- return
82
- text = result.text
83
- if result.mode == "task" and result.context_sha256:
84
- # The projection is already in context from the prompt route unless
85
- # the session/task/contract version changed; skip per-edit
86
- # re-injection. Same version key as the prompt hook so a TASK
87
- # switch re-injects here too.
88
- state = load_session_state(root)
89
- version = injection_version(root, sid, result.context_sha256)
90
- if state.get("injected_version") == version:
91
- text = ""
92
- else:
93
- state["injected_version"] = version
94
- save_session_state(root, state)
95
- if text:
96
- sys.stdout.write(json.dumps(_inject(text), ensure_ascii=False))
97
- cf_log.append_event(root, "edit_intent", {"file": relative, "tool": tool_name, "specs": list(result.specs)}, sid)
98
- except (json.JSONDecodeError, OSError, ValueError) as exc:
99
- _log(f"cf_pre_tool_hook error: {exc}")
100
-
101
-
102
- if __name__ == "__main__":
103
- main()
104
- timing_log("cf_pre_tool_hook")