@michaelxwb/code-flow 0.6.5 → 0.8.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/README.md +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -42
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +52 -39
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +52 -39
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +53 -45
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- 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_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
|
@@ -1,192 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import json
|
|
8
|
-
from pathlib import Path
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.cli.cf_acceptance_runner."""
|
|
9
3
|
import sys
|
|
10
|
-
from typing import IO, Mapping, Optional, Sequence
|
|
11
|
-
|
|
12
|
-
from cf_exec_base import execution_key, run_command, invalidate_executions
|
|
13
|
-
from cf_acceptance_schema import load_manifest, validate_execution_baseline
|
|
14
|
-
from cf_acceptance_evidence import persist_results
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _run(
|
|
18
|
-
item: Mapping[str, object], root: Path, include_e2e: bool = False, deadline: Optional[float] = None,
|
|
19
|
-
only_e2e: bool = False,
|
|
20
|
-
) -> dict[str, object]:
|
|
21
|
-
scenario_id = str(item.get("id", "unknown"))
|
|
22
|
-
kind = str(item.get("kind", "functional"))
|
|
23
|
-
if only_e2e and kind != "e2e":
|
|
24
|
-
return {"id": scenario_id, "kind": kind, "status": "not_included"}
|
|
25
|
-
if kind == "manual":
|
|
26
|
-
return {"id": scenario_id, "kind": kind, "status": "manual_pending"}
|
|
27
|
-
if kind == "e2e" and not include_e2e and not only_e2e:
|
|
28
|
-
return {"id": scenario_id, "kind": kind, "status": "e2e_deferred"}
|
|
29
|
-
command = item.get("command")
|
|
30
|
-
if not isinstance(command, list) or not command or not all(isinstance(value, str) for value in command):
|
|
31
|
-
return {"id": scenario_id, "kind": kind, "status": "not_configured"}
|
|
32
|
-
timeout = item.get("timeout", 60)
|
|
33
|
-
timeout_value = float(timeout) if isinstance(timeout, (int, float)) else 60.0
|
|
34
|
-
cwd = item.get("cwd", ".") if isinstance(item.get("cwd", "."), str) else "."
|
|
35
|
-
outcome = run_command(command, str(root / cwd), timeout_value, deadline)
|
|
36
|
-
if outcome["status"] == "deadline_exceeded":
|
|
37
|
-
return {"id": scenario_id, "kind": kind, "status": "incomplete", "error": "deadline_exceeded"}
|
|
38
|
-
if outcome["status"] == "timeout":
|
|
39
|
-
return {"id": scenario_id, "kind": kind, "status": "failed", "error": "timeout",
|
|
40
|
-
"output": str(outcome.get("stdout", ""))}
|
|
41
|
-
if outcome["status"] == "spawn_error":
|
|
42
|
-
return {"id": scenario_id, "kind": kind, "status": "failed", "error": str(outcome.get("stderr", ""))}
|
|
43
|
-
if outcome["returncode"] == 0:
|
|
44
|
-
return {"id": scenario_id, "kind": kind, "status": "passed", "exit_code": 0}
|
|
45
|
-
output = (str(outcome.get("stdout", "")) + str(outcome.get("stderr", ""))).strip()[-4000:]
|
|
46
|
-
return {"id": scenario_id, "kind": kind, "status": "failed", "exit_code": outcome["returncode"], "output": output}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _ordered(items: list[Mapping[str, object]]) -> list[Mapping[str, object]]:
|
|
50
|
-
by_id = {str(item.get("id")): item for item in items}
|
|
51
|
-
pending = set(by_id)
|
|
52
|
-
ordered: list[Mapping[str, object]] = []
|
|
53
|
-
while pending:
|
|
54
|
-
ready: list[str] = []
|
|
55
|
-
for item_id in by_id:
|
|
56
|
-
if item_id not in pending:
|
|
57
|
-
continue
|
|
58
|
-
deps = by_id[item_id].get("depends_on", [])
|
|
59
|
-
dep_ids = [str(dep) for dep in deps] if isinstance(deps, list) else []
|
|
60
|
-
unknown = [dep for dep in dep_ids if dep not in by_id]
|
|
61
|
-
if unknown:
|
|
62
|
-
raise ValueError(f"acceptance scenario dependency missing: {', '.join(unknown)}")
|
|
63
|
-
if all(dep not in pending for dep in dep_ids):
|
|
64
|
-
ready.append(item_id)
|
|
65
|
-
if not ready:
|
|
66
|
-
raise ValueError("acceptance scenario dependency cycle")
|
|
67
|
-
ordered.extend(by_id[item_id] for item_id in ready)
|
|
68
|
-
pending.difference_update(ready)
|
|
69
|
-
return ordered
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _executable(items: list[Mapping[str, object]]) -> bool:
|
|
73
|
-
"""Draft completeness vs execution gate: at least one scenario must be
|
|
74
|
-
executable — a functional scenario with a registered command, or a
|
|
75
|
-
manual / e2e scenario with its own confirmation/deferral path. Zero
|
|
76
|
-
executable scenarios must block execution acceptance, never pass it."""
|
|
77
|
-
for item in items:
|
|
78
|
-
command = item.get("command")
|
|
79
|
-
if isinstance(command, list) and command and all(isinstance(value, str) for value in command):
|
|
80
|
-
return True
|
|
81
|
-
if str(item.get("kind", "")) in ("manual", "e2e"):
|
|
82
|
-
return True
|
|
83
|
-
return False
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _owned(items: list[Mapping[str, object]], owner: str) -> list[Mapping[str, object]]:
|
|
87
|
-
"""Keep scenarios owned by the active TASK; ownerless legacy scenarios stay
|
|
88
|
-
included so old manifests keep working. Cross-TASK depends_on edges are
|
|
89
|
-
pruned: single-TASK ordering is enforced here, whole-demand ordering at
|
|
90
|
-
the final (archive/E2E) gate."""
|
|
91
|
-
if not owner:
|
|
92
|
-
return items
|
|
93
|
-
kept = [
|
|
94
|
-
item for item in items
|
|
95
|
-
if not isinstance(item.get("owner"), str) or not item.get("owner") or item.get("owner") == owner
|
|
96
|
-
]
|
|
97
|
-
kept_ids = {str(item.get("id")) for item in kept}
|
|
98
|
-
pruned: list[Mapping[str, object]] = []
|
|
99
|
-
for item in kept:
|
|
100
|
-
if not isinstance(item, dict):
|
|
101
|
-
pruned.append(item)
|
|
102
|
-
continue
|
|
103
|
-
deps = item.get("depends_on")
|
|
104
|
-
if isinstance(deps, list):
|
|
105
|
-
item = {**item, "depends_on": [dep for dep in deps if str(dep) in kept_ids]}
|
|
106
|
-
pruned.append(item)
|
|
107
|
-
return pruned
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _run_unique(
|
|
111
|
-
ordered: list[Mapping[str, object]], root: Path, include_e2e: bool, deadline: Optional[float] = None,
|
|
112
|
-
only_e2e: bool = False,
|
|
113
|
-
) -> list[dict[str, object]]:
|
|
114
|
-
"""Reuse identical execution semantics within a dependency-free batch."""
|
|
115
|
-
cache: dict[str, Mapping[str, object]] = {}
|
|
116
|
-
results: list[dict[str, object]] = []
|
|
117
|
-
for item in ordered:
|
|
118
|
-
scenario_id = str(item.get("id", "unknown"))
|
|
119
|
-
command = item.get("command")
|
|
120
|
-
key: Optional[str] = None
|
|
121
|
-
if isinstance(command, list) and command and all(isinstance(value, str) for value in command):
|
|
122
|
-
key = execution_key(command, str(root / str(item.get("cwd", "."))), float(item.get("timeout", 60)))
|
|
123
|
-
key += json.dumps([item.get("kind", "functional"), include_e2e, only_e2e])
|
|
124
|
-
if item.get("depends_on"):
|
|
125
|
-
cache.clear()
|
|
126
|
-
invalidate_executions()
|
|
127
|
-
if key is not None and key in cache:
|
|
128
|
-
shared = dict(cache[key])
|
|
129
|
-
shared["id"] = scenario_id
|
|
130
|
-
results.append(shared)
|
|
131
|
-
continue
|
|
132
|
-
result = _run(item, root, include_e2e, deadline, only_e2e)
|
|
133
|
-
if key is not None:
|
|
134
|
-
cache[key] = result
|
|
135
|
-
results.append(result)
|
|
136
|
-
return results
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def run_manifest(
|
|
140
|
-
manifest_file: str,
|
|
141
|
-
root: str,
|
|
142
|
-
write_evidence: bool = False,
|
|
143
|
-
include_e2e: bool = False,
|
|
144
|
-
owner: str = "",
|
|
145
|
-
deadline: Optional[float] = None,
|
|
146
|
-
only_e2e: bool = False,
|
|
147
|
-
) -> dict[str, object]:
|
|
148
|
-
data = load_manifest(Path(manifest_file))
|
|
149
|
-
validate_execution_baseline(Path(manifest_file), data)
|
|
150
|
-
scenarios = data["scenarios"]
|
|
151
|
-
_ordered(scenarios) # Validate the complete DAG before owner filtering.
|
|
152
|
-
items = _owned(scenarios, owner)
|
|
153
|
-
ordered = _ordered(items)
|
|
154
|
-
results = _run_unique(ordered, Path(root), include_e2e or only_e2e, deadline, only_e2e)
|
|
155
|
-
if write_evidence:
|
|
156
|
-
persist_results(Path(manifest_file), data, results)
|
|
157
|
-
if not _executable(items):
|
|
158
|
-
return {"decision": "block", "results": results, "error": "no_executable_scenarios"}
|
|
159
|
-
allowed = ("passed", "manual_pending", "e2e_deferred", "not_included")
|
|
160
|
-
decision = "pass" if all(item["status"] in allowed for item in results) else "block"
|
|
161
|
-
error = ""
|
|
162
|
-
if decision == "block":
|
|
163
|
-
pending = sorted({str(item["id"]) for item in results if item["status"] == "incomplete"})
|
|
164
|
-
failed = sorted({str(item["id"]) for item in results if item["status"] not in (*allowed, "incomplete")})
|
|
165
|
-
error = ",".join((["incomplete:" + ",".join(pending)] if pending else []) + (["failed:" + ",".join(failed)] if failed else [])) or "acceptance scenario failed"
|
|
166
|
-
outcome: dict[str, object] = {"decision": decision, "results": results}
|
|
167
|
-
if error:
|
|
168
|
-
outcome["error"] = error
|
|
169
|
-
return outcome
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
173
|
-
parser = argparse.ArgumentParser()
|
|
174
|
-
parser.add_argument("--manifest", required=True)
|
|
175
|
-
parser.add_argument("--root", required=True)
|
|
176
|
-
parser.add_argument("--write-evidence", action="store_true")
|
|
177
|
-
parser.add_argument("--include-e2e", action="store_true", help="Execute E2E scenarios (deferred by default)")
|
|
178
|
-
parser.add_argument("--only-e2e", action="store_true", help="Execute only E2E scenarios; functional/manual are reported not_included")
|
|
179
|
-
parser.add_argument("--owner", default="", help="Only execute scenarios owned by this TASK (plus ownerless legacy ones)")
|
|
180
|
-
parser.add_argument("--deadline", type=float, default=0.0, help="Absolute monotonic deadline propagated from the entry gate (0 = unbounded)")
|
|
181
|
-
args = parser.parse_args(argv)
|
|
182
|
-
try:
|
|
183
|
-
result = run_manifest(args.manifest, args.root, args.write_evidence, args.include_e2e, args.owner, args.deadline or None, args.only_e2e)
|
|
184
|
-
stdout.write(json.dumps(result, ensure_ascii=False))
|
|
185
|
-
return 0 if result["decision"] == "pass" else 3
|
|
186
|
-
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
187
|
-
stdout.write(json.dumps({"decision": "block", "error": str(exc)}, ensure_ascii=False))
|
|
188
|
-
return 2
|
|
189
4
|
|
|
5
|
+
from codeflow.cli import cf_acceptance_runner as _impl
|
|
190
6
|
|
|
191
7
|
if __name__ == "__main__":
|
|
192
|
-
raise SystemExit(main())
|
|
8
|
+
raise SystemExit(_impl.main())
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.cli.cf_audit."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from codeflow.cli import cf_audit as _impl
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
raise SystemExit(_impl.main())
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.hooks.cf_codex_hook."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from codeflow.hooks import cf_codex_hook as _impl
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
_impl.main()
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.cli.cf_design_lint."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from codeflow.cli import cf_design_lint as _impl
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
raise SystemExit(_impl.main())
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.cli.cf_evidence_import."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from codeflow.cli import cf_evidence_import as _impl
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
raise SystemExit(_impl.main())
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|
|
@@ -1,56 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
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
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.cli.cf_feedback."""
|
|
13
3
|
import sys
|
|
14
4
|
|
|
15
|
-
from
|
|
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
|
-
|
|
5
|
+
from codeflow.cli import cf_feedback as _impl
|
|
54
6
|
|
|
55
7
|
if __name__ == "__main__":
|
|
56
|
-
sys.exit(main(sys.argv[1:]))
|
|
8
|
+
sys.exit(_impl.main(sys.argv[1:]))
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.hooks.cf_opencode_event."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from codeflow.hooks import cf_opencode_event as _impl
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
_impl.main()
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|
|
@@ -1,196 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
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
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.hooks.cf_post_hook."""
|
|
21
3
|
import sys
|
|
22
4
|
|
|
23
|
-
import
|
|
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
|
-
|
|
5
|
+
from codeflow.hooks import cf_post_hook as _impl
|
|
193
6
|
|
|
194
7
|
if __name__ == "__main__":
|
|
195
|
-
main()
|
|
196
|
-
timing_log("cf_post_hook")
|
|
8
|
+
_impl.main()
|
|
9
|
+
_impl.timing_log("cf_post_hook")
|
|
10
|
+
else:
|
|
11
|
+
sys.modules[__name__] = _impl
|
|
@@ -1,104 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
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
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.hooks.cf_pre_tool_hook."""
|
|
11
3
|
import sys
|
|
12
4
|
|
|
13
|
-
import
|
|
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
|
-
|
|
5
|
+
from codeflow.hooks import cf_pre_tool_hook as _impl
|
|
101
6
|
|
|
102
7
|
if __name__ == "__main__":
|
|
103
|
-
main()
|
|
104
|
-
timing_log("cf_pre_tool_hook")
|
|
8
|
+
_impl.main()
|
|
9
|
+
_impl.timing_log("cf_pre_tool_hook")
|
|
10
|
+
else:
|
|
11
|
+
sys.modules[__name__] = _impl
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Path-frozen entry shim: implementation lives in codeflow.cli.cf_scenarios."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from codeflow.cli import cf_scenarios as _impl
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
raise SystemExit(_impl.main())
|
|
9
|
+
else:
|
|
10
|
+
sys.modules[__name__] = _impl
|