@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.
- package/README.md +75 -110
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +268 -0
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +21 -9
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -264
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -67
- package/src/adapters/claude/commands/cf-stats.md +0 -86
- package/src/adapters/claude/commands/cf-sync.md +0 -25
- package/src/adapters/claude/commands/cf-task/align.md +0 -309
- package/src/adapters/claude/commands/cf-task/archive.md +0 -161
- package/src/adapters/claude/commands/cf-task/block.md +0 -36
- package/src/adapters/claude/commands/cf-task/graph.md +0 -80
- package/src/adapters/claude/commands/cf-task/note.md +0 -236
- package/src/adapters/claude/commands/cf-task/plan.md +0 -363
- package/src/adapters/claude/commands/cf-task/prd.md +0 -162
- package/src/adapters/claude/commands/cf-task/start.md +0 -304
- package/src/adapters/claude/commands/cf-task/status.md +0 -59
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/claude/commands/cf-validate.md +0 -75
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -50
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -264
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -67
- package/src/adapters/costrict/commands/cf-stats.md +0 -86
- package/src/adapters/costrict/commands/cf-sync.md +0 -25
- package/src/adapters/costrict/commands/cf-task/align.md +0 -309
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
- package/src/adapters/costrict/commands/cf-task/block.md +0 -36
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
- package/src/adapters/costrict/commands/cf-task/note.md +0 -236
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
- package/src/adapters/costrict/commands/cf-task/start.md +0 -304
- package/src/adapters/costrict/commands/cf-task/status.md +0 -59
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/costrict/commands/cf-validate.md +0 -75
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -59
- package/src/adapters/opencode/commands/cf-stats.md +0 -90
- package/src/adapters/opencode/commands/cf-sync.md +0 -29
- package/src/adapters/opencode/commands/cf-task/align.md +0 -314
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
- package/src/adapters/opencode/commands/cf-task/block.md +0 -40
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
- package/src/adapters/opencode/commands/cf-task/note.md +0 -240
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
- package/src/adapters/opencode/commands/cf-task/start.md +0 -309
- package/src/adapters/opencode/commands/cf-task/status.md +0 -63
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/opencode/commands/cf-validate.md +0 -79
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/cli.js +0 -1023
- package/src/core/code-flow/config.yml +0 -158
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_checks.py +0 -336
- package/src/core/code-flow/scripts/cf_core.py +0 -787
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_feedback.py +0 -56
- package/src/core/code-flow/scripts/cf_log.py +0 -154
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
- package/src/core/code-flow/scripts/cf_scan.py +0 -295
- package/src/core/code-flow/scripts/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
- package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
- package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
- package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
- package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
- package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
- package/src/core/code-flow/scripts/cf_stats.py +0 -506
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
- package/src/core/code-flow/scripts/cf_sync.py +0 -140
- package/src/core/code-flow/scripts/cf_task_index.py +0 -148
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
- package/src/core/code-flow/scripts/cf_validation.py +0 -81
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -53
- 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")
|