@michaelxwb/code-flow 0.6.0 → 0.6.1
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-spec.md +13 -0
- package/src/adapters/claude/commands/cf-sync.md +25 -0
- package/src/adapters/codex/skills/cf-spec/SKILL.md +1 -0
- package/src/adapters/codex/skills/cf-sync/SKILL.md +30 -0
- package/src/adapters/costrict/commands/cf-spec.md +13 -0
- package/src/adapters/costrict/commands/cf-sync.md +25 -0
- package/src/adapters/opencode/commands/cf-spec.md +1 -0
- package/src/adapters/opencode/commands/cf-sync.md +29 -0
- package/src/adapters/opencode/plugins/code-flow/index.js +3 -3
- package/src/core/code-flow/config.yml +2 -1
- package/src/core/code-flow/scripts/cf_core.py +24 -1
- package/src/core/code-flow/scripts/cf_post_hook.py +3 -30
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +37 -14
- package/src/core/code-flow/scripts/cf_session_state.py +37 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +250 -13
- package/src/core/code-flow/scripts/cf_spec_gate.py +12 -6
- package/src/core/code-flow/scripts/cf_spec_router.py +6 -2
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -9
- package/src/core/code-flow/scripts/cf_spec_verify.py +51 -4
- package/src/core/code-flow/scripts/cf_stop_hook.py +33 -12
- package/src/core/code-flow/scripts/cf_sync.py +140 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +49 -7
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +104 -8
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- `cf-spec migrate --plan <migration-plan.yml>`
|
|
6
6
|
- `cf-spec context [需求目录]`
|
|
7
7
|
- `cf-spec refresh [需求目录]`
|
|
8
|
+
- `cf-spec status [需求目录]`
|
|
8
9
|
- `cf-spec doctor [需求目录]`
|
|
9
10
|
|
|
10
11
|
## 通用硬门禁
|
|
@@ -33,6 +34,18 @@
|
|
|
33
34
|
`python3 .code-flow/scripts/cf_spec_context.py validate --task-dir <目录> --json`
|
|
34
35
|
3. Context 缺失、schema/hash 无效时 fail-closed,不回退 Catalog。
|
|
35
36
|
|
|
37
|
+
|
|
38
|
+
## status
|
|
39
|
+
|
|
40
|
+
展示人话版 Context 状态:任务、marker hash 是否一致、code Gate 结果、各绑定 Rule 状态。
|
|
41
|
+
|
|
42
|
+
执行:
|
|
43
|
+
|
|
44
|
+
`python3 .code-flow/scripts/cf_spec_context.py status --task-dir <目录> --root "$PWD"`
|
|
45
|
+
|
|
46
|
+
- marker 漂移时输出下一步(cf-spec doctor resync)。
|
|
47
|
+
- 需要机器可读输出时加 `--json`。
|
|
48
|
+
|
|
36
49
|
## refresh
|
|
37
50
|
|
|
38
51
|
执行:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# cf-sync
|
|
2
|
+
|
|
3
|
+
一键同步双副本(canonical 源 → 部署副本),并检查漂移。用法:
|
|
4
|
+
|
|
5
|
+
- `python3 .code-flow/scripts/cf_sync.py check` — 检查全部配对是否一致(默认)
|
|
6
|
+
- `python3 .code-flow/scripts/cf_sync.py sync` — 把 canonical 源复制到部署副本
|
|
7
|
+
- `python3 .code-flow/scripts/cf_sync.py check --verbose` — 额外列出部署侧独有文件
|
|
8
|
+
|
|
9
|
+
## 同步配对
|
|
10
|
+
|
|
11
|
+
| canonical 源 | 部署副本 | 同步内容 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `src/core/code-flow` | `.code-flow` | `scripts/`、`.version`、`.gitignore` |
|
|
14
|
+
| `src/adapters/claude` | `.claude` | `commands/` |
|
|
15
|
+
| `src/adapters/codex` | `.codex` | `hooks.json` |
|
|
16
|
+
| `src/adapters/codex/skills` | `.agents/skills` | 全部 |
|
|
17
|
+
| `src/adapters/costrict` | `.costrict` | `commands/` |
|
|
18
|
+
| `src/adapters/opencode` | `.opencode` | `commands/`、`plugins/` |
|
|
19
|
+
|
|
20
|
+
## 规则
|
|
21
|
+
|
|
22
|
+
1. 项目自有内容(`specs/`、`tasks/`、`config.yml`、`validation.yml`、`settings.local.json`)不在同步范围内,不会被动覆盖。
|
|
23
|
+
2. 部署侧独有文件不会被删除(避免误删本地产物)。
|
|
24
|
+
3. 只改一侧 = 测试通过但 live 行为不变;改完 canonical 源后运行 `cf-sync sync` 部署,提交时双副本一起提交。
|
|
25
|
+
4. 每对 `check` 全绿后提交,保证四平台命令与运行副本同步。
|
|
@@ -10,6 +10,7 @@ description: Manage schema-v1 Spec Context, drift recovery, and prepared one-sho
|
|
|
10
10
|
- `cf-spec migrate --plan <migration-plan.yml>`
|
|
11
11
|
- `cf-spec context [需求目录]`
|
|
12
12
|
- `cf-spec refresh [需求目录]`
|
|
13
|
+
- `cf-spec status [需求目录]`
|
|
13
14
|
- `cf-spec doctor [需求目录]`
|
|
14
15
|
|
|
15
16
|
## 通用硬门禁
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cf-sync
|
|
3
|
+
description: One-command canonical → deploy sync for dual-copy artifacts, with drift check.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# cf-sync
|
|
7
|
+
|
|
8
|
+
一键同步双副本(canonical 源 → 部署副本),并检查漂移。用法:
|
|
9
|
+
|
|
10
|
+
- `python3 .code-flow/scripts/cf_sync.py check` — 检查全部配对是否一致(默认)
|
|
11
|
+
- `python3 .code-flow/scripts/cf_sync.py sync` — 把 canonical 源复制到部署副本
|
|
12
|
+
- `python3 .code-flow/scripts/cf_sync.py check --verbose` — 额外列出部署侧独有文件
|
|
13
|
+
|
|
14
|
+
## 同步配对
|
|
15
|
+
|
|
16
|
+
| canonical 源 | 部署副本 | 同步内容 |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `src/core/code-flow` | `.code-flow` | `scripts/`、`.version`、`.gitignore` |
|
|
19
|
+
| `src/adapters/claude` | `.claude` | `commands/` |
|
|
20
|
+
| `src/adapters/codex` | `.codex` | `hooks.json` |
|
|
21
|
+
| `src/adapters/codex/skills` | `.agents/skills` | 全部 |
|
|
22
|
+
| `src/adapters/costrict` | `.costrict` | `commands/` |
|
|
23
|
+
| `src/adapters/opencode` | `.opencode` | `commands/`、`plugins/` |
|
|
24
|
+
|
|
25
|
+
## 规则
|
|
26
|
+
|
|
27
|
+
1. 项目自有内容(`specs/`、`tasks/`、`config.yml`、`validation.yml`、`settings.local.json`)不在同步范围内,不会被动覆盖。
|
|
28
|
+
2. 部署侧独有文件不会被删除(避免误删本地产物)。
|
|
29
|
+
3. 只改一侧 = 测试通过但 live 行为不变;改完 canonical 源后运行 `cf-sync sync` 部署,提交时双副本一起提交。
|
|
30
|
+
4. 每对 `check` 全绿后提交,保证四平台命令与运行副本同步。
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- `cf-spec migrate --plan <migration-plan.yml>`
|
|
6
6
|
- `cf-spec context [需求目录]`
|
|
7
7
|
- `cf-spec refresh [需求目录]`
|
|
8
|
+
- `cf-spec status [需求目录]`
|
|
8
9
|
- `cf-spec doctor [需求目录]`
|
|
9
10
|
|
|
10
11
|
## 通用硬门禁
|
|
@@ -33,6 +34,18 @@
|
|
|
33
34
|
`python3 .code-flow/scripts/cf_spec_context.py validate --task-dir <目录> --json`
|
|
34
35
|
3. Context 缺失、schema/hash 无效时 fail-closed,不回退 Catalog。
|
|
35
36
|
|
|
37
|
+
|
|
38
|
+
## status
|
|
39
|
+
|
|
40
|
+
展示人话版 Context 状态:任务、marker hash 是否一致、code Gate 结果、各绑定 Rule 状态。
|
|
41
|
+
|
|
42
|
+
执行:
|
|
43
|
+
|
|
44
|
+
`python3 .code-flow/scripts/cf_spec_context.py status --task-dir <目录> --root "$PWD"`
|
|
45
|
+
|
|
46
|
+
- marker 漂移时输出下一步(cf-spec doctor resync)。
|
|
47
|
+
- 需要机器可读输出时加 `--json`。
|
|
48
|
+
|
|
36
49
|
## refresh
|
|
37
50
|
|
|
38
51
|
执行:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# cf-sync
|
|
2
|
+
|
|
3
|
+
一键同步双副本(canonical 源 → 部署副本),并检查漂移。用法:
|
|
4
|
+
|
|
5
|
+
- `python3 .code-flow/scripts/cf_sync.py check` — 检查全部配对是否一致(默认)
|
|
6
|
+
- `python3 .code-flow/scripts/cf_sync.py sync` — 把 canonical 源复制到部署副本
|
|
7
|
+
- `python3 .code-flow/scripts/cf_sync.py check --verbose` — 额外列出部署侧独有文件
|
|
8
|
+
|
|
9
|
+
## 同步配对
|
|
10
|
+
|
|
11
|
+
| canonical 源 | 部署副本 | 同步内容 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `src/core/code-flow` | `.code-flow` | `scripts/`、`.version`、`.gitignore` |
|
|
14
|
+
| `src/adapters/claude` | `.claude` | `commands/` |
|
|
15
|
+
| `src/adapters/codex` | `.codex` | `hooks.json` |
|
|
16
|
+
| `src/adapters/codex/skills` | `.agents/skills` | 全部 |
|
|
17
|
+
| `src/adapters/costrict` | `.costrict` | `commands/` |
|
|
18
|
+
| `src/adapters/opencode` | `.opencode` | `commands/`、`plugins/` |
|
|
19
|
+
|
|
20
|
+
## 规则
|
|
21
|
+
|
|
22
|
+
1. 项目自有内容(`specs/`、`tasks/`、`config.yml`、`validation.yml`、`settings.local.json`)不在同步范围内,不会被动覆盖。
|
|
23
|
+
2. 部署侧独有文件不会被删除(避免误删本地产物)。
|
|
24
|
+
3. 只改一侧 = 测试通过但 live 行为不变;改完 canonical 源后运行 `cf-sync sync` 部署,提交时双副本一起提交。
|
|
25
|
+
4. 每对 `check` 全绿后提交,保证四平台命令与运行副本同步。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 双副本同步:检查漂移并一键部署 canonical 源
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# cf-sync
|
|
6
|
+
|
|
7
|
+
一键同步双副本(canonical 源 → 部署副本),并检查漂移。用法:
|
|
8
|
+
|
|
9
|
+
- `python3 .code-flow/scripts/cf_sync.py check` — 检查全部配对是否一致(默认)
|
|
10
|
+
- `python3 .code-flow/scripts/cf_sync.py sync` — 把 canonical 源复制到部署副本
|
|
11
|
+
- `python3 .code-flow/scripts/cf_sync.py check --verbose` — 额外列出部署侧独有文件
|
|
12
|
+
|
|
13
|
+
## 同步配对
|
|
14
|
+
|
|
15
|
+
| canonical 源 | 部署副本 | 同步内容 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `src/core/code-flow` | `.code-flow` | `scripts/`、`.version`、`.gitignore` |
|
|
18
|
+
| `src/adapters/claude` | `.claude` | `commands/` |
|
|
19
|
+
| `src/adapters/codex` | `.codex` | `hooks.json` |
|
|
20
|
+
| `src/adapters/codex/skills` | `.agents/skills` | 全部 |
|
|
21
|
+
| `src/adapters/costrict` | `.costrict` | `commands/` |
|
|
22
|
+
| `src/adapters/opencode` | `.opencode` | `commands/`、`plugins/` |
|
|
23
|
+
|
|
24
|
+
## 规则
|
|
25
|
+
|
|
26
|
+
1. 项目自有内容(`specs/`、`tasks/`、`config.yml`、`validation.yml`、`settings.local.json`)不在同步范围内,不会被动覆盖。
|
|
27
|
+
2. 部署侧独有文件不会被删除(避免误删本地产物)。
|
|
28
|
+
3. 只改一侧 = 测试通过但 live 行为不变;改完 canonical 源后运行 `cf-sync sync` 部署,提交时双副本一起提交。
|
|
29
|
+
4. 每对 `check` 全绿后提交,保证四平台命令与运行副本同步。
|
|
@@ -21,13 +21,13 @@ function pythonPath(projectRoot, script) {
|
|
|
21
21
|
return join(projectRoot, SCRIPT_DIR, script);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function callHook(projectRoot, script, input) {
|
|
24
|
+
function callHook(projectRoot, script, input, timeout = 5000) {
|
|
25
25
|
try {
|
|
26
26
|
const proc = spawnSync("python3", [pythonPath(projectRoot, script)], {
|
|
27
27
|
cwd: projectRoot,
|
|
28
28
|
input: JSON.stringify(input),
|
|
29
29
|
encoding: "utf-8",
|
|
30
|
-
timeout
|
|
30
|
+
timeout,
|
|
31
31
|
maxBuffer: 1024 * 1024,
|
|
32
32
|
});
|
|
33
33
|
if (proc.error || proc.status !== 0) {
|
|
@@ -65,7 +65,7 @@ export const CodeFlow = async (ctx) => {
|
|
|
65
65
|
const sid =
|
|
66
66
|
input.event?.properties?.sessionID ||
|
|
67
67
|
input.event?.properties?.info?.id || "";
|
|
68
|
-
const result = callHook(projectRoot, "cf_stop_hook.py", { session_id: sid });
|
|
68
|
+
const result = callHook(projectRoot, "cf_stop_hook.py", { session_id: sid }, 35000);
|
|
69
69
|
if (result?.reason) {
|
|
70
70
|
// idle 无法阻断,校验失败排队到下一轮 system prompt
|
|
71
71
|
const pending = sessionContext.get(sid);
|
|
@@ -2,7 +2,7 @@ version: 1
|
|
|
2
2
|
|
|
3
3
|
spec_workflow:
|
|
4
4
|
schema_version: 1
|
|
5
|
-
enforcement: required
|
|
5
|
+
enforcement: required # required=全门禁 | warn=门禁仅记录不阻断 | inject=只注入不门禁
|
|
6
6
|
drift_check: true
|
|
7
7
|
scope_expansion_check: true
|
|
8
8
|
metrics: true
|
|
@@ -29,6 +29,7 @@ quality_loop:
|
|
|
29
29
|
# 注入时对 spec 内容做保守无损压缩(去行尾空白、折叠多空行、剥 HTML 注释、去重 bullet)
|
|
30
30
|
# 缺省视为 true;仅显式 false 关闭
|
|
31
31
|
compress: true
|
|
32
|
+
compress_reminder: true # 会话过长每 N 轮提醒 /compact(per-session 计数,缺省 true)
|
|
32
33
|
code_extensions:
|
|
33
34
|
- ".py"
|
|
34
35
|
- ".ts"
|
|
@@ -467,7 +467,8 @@ def resolve_quality_loop(config: dict) -> dict:
|
|
|
467
467
|
"""
|
|
468
468
|
if not isinstance(config, dict):
|
|
469
469
|
return {"enabled": False, "post_check": False,
|
|
470
|
-
"stop_check": False, "correction_capture": False
|
|
470
|
+
"stop_check": False, "correction_capture": False,
|
|
471
|
+
"compress_reminder": False}
|
|
471
472
|
cfg = config.get("quality_loop")
|
|
472
473
|
cfg = cfg if isinstance(cfg, dict) else {}
|
|
473
474
|
enabled = cfg.get("enabled") is True
|
|
@@ -480,9 +481,31 @@ def resolve_quality_loop(config: dict) -> dict:
|
|
|
480
481
|
"post_check": _sub("post_check"),
|
|
481
482
|
"stop_check": _sub("stop_check"),
|
|
482
483
|
"correction_capture": _sub("correction_capture"),
|
|
484
|
+
"compress_reminder": _sub("compress_reminder"),
|
|
483
485
|
}
|
|
484
486
|
|
|
485
487
|
|
|
488
|
+
ENFORCEMENT_MODES = frozenset(("required", "warn", "inject"))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def resolve_enforcement(config: dict) -> str:
|
|
492
|
+
"""Project-wide Spec Workflow enforcement tier (light-mode ladder).
|
|
493
|
+
|
|
494
|
+
required — full fail-closed gates (default; upgraded projects keep it);
|
|
495
|
+
warn — gates run but only log/report, never block the session;
|
|
496
|
+
inject — routing/injection only; gates and blocks are disabled.
|
|
497
|
+
Missing or invalid values fail closed to "required".
|
|
498
|
+
"""
|
|
499
|
+
if not isinstance(config, dict):
|
|
500
|
+
return "required"
|
|
501
|
+
workflow = config.get("spec_workflow")
|
|
502
|
+
workflow = workflow if isinstance(workflow, dict) else {}
|
|
503
|
+
value = workflow.get("enforcement")
|
|
504
|
+
if isinstance(value, str) and value in ENFORCEMENT_MODES:
|
|
505
|
+
return value
|
|
506
|
+
return "required"
|
|
507
|
+
|
|
508
|
+
|
|
486
509
|
_FRONTMATTER_RE = re.compile(r"\A---[ \t]*\n(.*?)\n---[ \t]*\n?", re.DOTALL)
|
|
487
510
|
|
|
488
511
|
|
|
@@ -21,8 +21,6 @@ import os
|
|
|
21
21
|
import sys
|
|
22
22
|
|
|
23
23
|
import cf_log
|
|
24
|
-
from cf_spec_context import ContextError, load_active_task
|
|
25
|
-
from cf_task_runtime import evaluate_scope, run_done_gate
|
|
26
24
|
from cf_checks import (
|
|
27
25
|
load_check_state,
|
|
28
26
|
load_spec_checks,
|
|
@@ -39,6 +37,7 @@ from cf_core import (
|
|
|
39
37
|
match_domains,
|
|
40
38
|
normalize_path,
|
|
41
39
|
normalize_spec_entry,
|
|
40
|
+
resolve_enforcement,
|
|
42
41
|
resolve_quality_loop,
|
|
43
42
|
resolve_session_id,
|
|
44
43
|
)
|
|
@@ -93,23 +92,6 @@ def _feedback_text(violations: list) -> str:
|
|
|
93
92
|
return "\n".join(lines)
|
|
94
93
|
|
|
95
94
|
|
|
96
|
-
def _active_feedback(project_root: str) -> str:
|
|
97
|
-
marker = os.path.join(project_root, ".code-flow", ".active-task.json")
|
|
98
|
-
if not os.path.exists(marker):
|
|
99
|
-
return ""
|
|
100
|
-
active = load_active_task(project_root)
|
|
101
|
-
task_dir = os.path.join(project_root, active.task_dir)
|
|
102
|
-
scope = evaluate_scope(project_root, task_dir)
|
|
103
|
-
if scope.decision == "pause":
|
|
104
|
-
return scope.message
|
|
105
|
-
result = run_done_gate(project_root, task_dir)
|
|
106
|
-
failed = [item for item in result.evidence if item.get("status") != "verified"]
|
|
107
|
-
if not failed:
|
|
108
|
-
return ""
|
|
109
|
-
refs = ", ".join(str(item.get("verifier_ref")) for item in failed)
|
|
110
|
-
return f"当前 TASK required verifier 未通过:{refs}。修复后再继续。"
|
|
111
|
-
|
|
112
|
-
|
|
113
95
|
def main() -> None:
|
|
114
96
|
try:
|
|
115
97
|
ensure_utf8_io()
|
|
@@ -125,20 +107,11 @@ def main() -> None:
|
|
|
125
107
|
return
|
|
126
108
|
|
|
127
109
|
project_root = os.getcwd()
|
|
128
|
-
try:
|
|
129
|
-
active_feedback = _active_feedback(project_root)
|
|
130
|
-
except (ContextError, OSError, ValueError) as exc:
|
|
131
|
-
message = f"SPEC_WORKFLOW_BLOCKED: active task is invalid: {exc}"
|
|
132
|
-
payload = {"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": message}}
|
|
133
|
-
sys.stdout.write(json.dumps(payload, ensure_ascii=False))
|
|
134
|
-
return
|
|
135
|
-
if active_feedback:
|
|
136
|
-
payload = {"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": active_feedback}}
|
|
137
|
-
sys.stdout.write(json.dumps(payload, ensure_ascii=False))
|
|
138
|
-
return
|
|
139
110
|
config = load_config(project_root)
|
|
140
111
|
if not config:
|
|
141
112
|
return
|
|
113
|
+
if resolve_enforcement(config) == "inject":
|
|
114
|
+
return
|
|
142
115
|
quality_loop = resolve_quality_loop(config)
|
|
143
116
|
if not quality_loop["post_check"]:
|
|
144
117
|
return
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook:
|
|
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
|
+
"""
|
|
3
7
|
|
|
4
8
|
import json
|
|
5
9
|
import os
|
|
@@ -7,11 +11,14 @@ from pathlib import Path
|
|
|
7
11
|
import sys
|
|
8
12
|
|
|
9
13
|
import cf_log
|
|
10
|
-
from cf_core import _log, ensure_utf8_io, normalize_path, resolve_session_id
|
|
14
|
+
from cf_core import _log, ensure_utf8_io, load_config, normalize_path, resolve_enforcement, resolve_session_id
|
|
15
|
+
from cf_session_state import load_session_state, save_session_state
|
|
11
16
|
from cf_spec_context import load_active_task, load_context
|
|
12
17
|
from cf_spec_resolver import resolve_candidates
|
|
13
18
|
from cf_spec_router import RouterError, route_prompt
|
|
14
19
|
|
|
20
|
+
_WARN_TEXT = "⚠ Spec Workflow 校验未通过(warn 模式):{message} — 建议运行 cf-spec doctor 检查。"
|
|
21
|
+
|
|
15
22
|
|
|
16
23
|
def _active_expansion(root: str, relative: str) -> tuple[str, ...]:
|
|
17
24
|
marker = Path(root) / ".code-flow/.active-task.json"
|
|
@@ -26,8 +33,8 @@ def _active_expansion(root: str, relative: str) -> tuple[str, ...]:
|
|
|
26
33
|
)
|
|
27
34
|
|
|
28
35
|
|
|
29
|
-
def
|
|
30
|
-
return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "
|
|
36
|
+
def _inject(text: str) -> dict[str, object]:
|
|
37
|
+
return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": text}}
|
|
31
38
|
|
|
32
39
|
|
|
33
40
|
def main() -> None:
|
|
@@ -42,21 +49,37 @@ def main() -> None:
|
|
|
42
49
|
if tool_name not in {"Edit", "Write", "MultiEdit"} or not isinstance(file_path, str) or not file_path:
|
|
43
50
|
return
|
|
44
51
|
root = os.getcwd()
|
|
52
|
+
sid = resolve_session_id(data)
|
|
53
|
+
config = load_config(root)
|
|
54
|
+
enforcement = resolve_enforcement(config) if config else "required"
|
|
45
55
|
absolute = file_path if os.path.isabs(file_path) else os.path.join(root, file_path)
|
|
46
56
|
relative = normalize_path(os.path.relpath(absolute, root))
|
|
47
|
-
|
|
57
|
+
try:
|
|
58
|
+
expanded = _active_expansion(root, relative)
|
|
59
|
+
result = route_prompt(root, (relative,), sid)
|
|
60
|
+
except RouterError as exc:
|
|
61
|
+
if enforcement == "required":
|
|
62
|
+
sys.stdout.write(json.dumps(_inject(f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}. Run cf-spec doctor before continuing."), ensure_ascii=False))
|
|
63
|
+
elif enforcement == "warn":
|
|
64
|
+
sys.stdout.write(json.dumps(_inject(_WARN_TEXT.format(message=str(exc))), ensure_ascii=False))
|
|
65
|
+
return
|
|
48
66
|
if expanded:
|
|
49
|
-
|
|
50
|
-
sys.stdout.write(json.dumps(
|
|
67
|
+
message = f"⚠ 该路径引入未绑定 required Spec:{', '.join(expanded)} — 建议先 refresh Context / Plan 再继续编辑。"
|
|
68
|
+
sys.stdout.write(json.dumps(_inject(message), ensure_ascii=False))
|
|
51
69
|
return
|
|
52
|
-
|
|
53
|
-
result
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
text = result.text
|
|
71
|
+
if result.mode == "task" and result.context_sha256:
|
|
72
|
+
# The projection is already in context from the prompt route unless
|
|
73
|
+
# the bound rules changed; skip per-edit re-injection.
|
|
74
|
+
state = load_session_state(root)
|
|
75
|
+
if state.get("injected_sha256") == result.context_sha256:
|
|
76
|
+
text = ""
|
|
77
|
+
else:
|
|
78
|
+
state["injected_sha256"] = result.context_sha256
|
|
79
|
+
save_session_state(root, state)
|
|
80
|
+
if text:
|
|
81
|
+
sys.stdout.write(json.dumps(_inject(text), ensure_ascii=False))
|
|
57
82
|
cf_log.append_event(root, "edit_intent", {"file": relative, "tool": tool_name, "specs": list(result.specs)}, sid)
|
|
58
|
-
except RouterError as exc:
|
|
59
|
-
sys.stdout.write(json.dumps(_deny(f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}"), ensure_ascii=False))
|
|
60
83
|
except (json.JSONDecodeError, OSError, ValueError) as exc:
|
|
61
84
|
_log(f"cf_pre_tool_hook error: {exc}")
|
|
62
85
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared per-session state for injection/reminder cadence.
|
|
3
|
+
|
|
4
|
+
Stores one JSON file per project root (`.code-flow/.session-state.json`),
|
|
5
|
+
keyed by session id. Both read and write are best-effort: a corrupt or
|
|
6
|
+
unwritable state degrades to no-op rather than breaking hook protocols.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Mapping
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_STATE_PATH = ".code-flow/.session-state.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_session_state(root: str) -> dict:
|
|
19
|
+
"""Return the persisted state dict; {} on any failure (never raises)."""
|
|
20
|
+
try:
|
|
21
|
+
value = json.loads((Path(root) / _STATE_PATH).read_text(encoding="utf-8"))
|
|
22
|
+
except (OSError, json.JSONDecodeError, UnicodeError):
|
|
23
|
+
return {}
|
|
24
|
+
return value if isinstance(value, dict) else {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def save_session_state(root: str, state: Mapping) -> bool:
|
|
28
|
+
"""Atomically persist state; False on failure (never raises)."""
|
|
29
|
+
path = Path(root) / _STATE_PATH
|
|
30
|
+
try:
|
|
31
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
33
|
+
temporary.write_text(json.dumps(dict(state), ensure_ascii=False), encoding="utf-8")
|
|
34
|
+
os.replace(temporary, path)
|
|
35
|
+
return True
|
|
36
|
+
except (OSError, UnicodeError, TypeError):
|
|
37
|
+
return False
|