@michaelxwb/code-flow 0.8.1 → 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 +73 -152
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +156 -1417
- 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 +18 -21
- 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 +12 -12
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
- 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/acceptance-contract.md +0 -217
- package/docs/ci/acceptance-evidence.md +0 -77
- package/docs/migrations/runtime-0.7.0.md +0 -41
- package/docs/migrations/runtime-0.8.0.md +0 -154
- package/docs/migrations/runtime-0.9.0.md +0 -40
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/docs/spec-injection-hardening.md +0 -54
- package/docs/spec-injection-performance.json +0 -73
- package/docs/spec-optimization.md +0 -12
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -262
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -87
- package/src/adapters/claude/commands/cf-stats.md +0 -96
- package/src/adapters/claude/commands/cf-sync.md +0 -39
- package/src/adapters/claude/commands/cf-task/align.md +0 -331
- package/src/adapters/claude/commands/cf-task/archive.md +0 -155
- package/src/adapters/claude/commands/cf-task/block.md +0 -49
- package/src/adapters/claude/commands/cf-task/graph.md +0 -92
- package/src/adapters/claude/commands/cf-task/note.md +0 -248
- package/src/adapters/claude/commands/cf-task/plan.md +0 -477
- package/src/adapters/claude/commands/cf-task/prd.md +0 -173
- package/src/adapters/claude/commands/cf-task/start.md +0 -317
- package/src/adapters/claude/commands/cf-task/status.md +0 -71
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/claude/commands/cf-validate.md +0 -100
- 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 -54
- 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 -88
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
- package/src/adapters/commands.json +0 -115
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -262
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -87
- package/src/adapters/costrict/commands/cf-stats.md +0 -96
- package/src/adapters/costrict/commands/cf-sync.md +0 -39
- package/src/adapters/costrict/commands/cf-task/align.md +0 -331
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
- package/src/adapters/costrict/commands/cf-task/block.md +0 -49
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
- package/src/adapters/costrict/commands/cf-task/note.md +0 -248
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
- package/src/adapters/costrict/commands/cf-task/start.md +0 -317
- package/src/adapters/costrict/commands/cf-task/status.md +0 -71
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/costrict/commands/cf-validate.md +0 -100
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/install-plan.json +0 -100
- 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 -91
- package/src/adapters/opencode/commands/cf-stats.md +0 -100
- package/src/adapters/opencode/commands/cf-sync.md +0 -44
- package/src/adapters/opencode/commands/cf-task/align.md +0 -331
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
- package/src/adapters/opencode/commands/cf-task/block.md +0 -54
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
- package/src/adapters/opencode/commands/cf-task/note.md +0 -253
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
- package/src/adapters/opencode/commands/cf-task/start.md +0 -317
- package/src/adapters/opencode/commands/cf-task/status.md +0 -76
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/opencode/commands/cf-validate.md +0 -105
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/adapters/platform-bindings.json +0 -370
- package/src/cli.js +0 -1076
- package/src/core/code-flow/config.yml +0 -161
- package/src/core/code-flow/runtime-commands.json +0 -259
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
- package/src/core/code-flow/scripts/cf_audit.py +0 -10
- package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
- package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
- package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
- package/src/core/code-flow/scripts/cf_feedback.py +0 -10
- package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
- package/src/core/code-flow/scripts/cf_stats.py +0 -10
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_sync.py +0 -10
- package/src/core/code-flow/scripts/cf_task_index.py +0 -10
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_validation.py +0 -10
- 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 +0 -347
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
- package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
- package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
- package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
- package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
- package/src/core/code-flow/scripts/runtime-resources.json +0 -29
- 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/specs/shared/scenarios-template.md +0 -55
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -59
- package/src/migrate/runtime.js +0 -213
- package/src/migrate/spec-workflow.js +0 -253
- package/src/runtime.js +0 -219
|
@@ -1,609 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import fnmatch
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
import sys
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
|
-
from codeflow.core import cf_log
|
|
10
|
-
from codeflow.core.cf_markdown import parse_frontmatter
|
|
11
|
-
|
|
12
|
-
# --- Config cache (fix #3: avoid re-parsing YAML on every hook call) ---
|
|
13
|
-
|
|
14
|
-
_config_cache: dict = {}
|
|
15
|
-
_spec_domains_cache: dict = {}
|
|
16
|
-
_effective_mapping_cache: dict = {}
|
|
17
|
-
_ext_set_cache: dict = {}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ConfigError(ValueError):
|
|
21
|
-
"""An existing project configuration cannot be read or validated."""
|
|
22
|
-
|
|
23
|
-
code = "invalid_config"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def project_instruction_file(project_root: str, platform: str = "") -> str:
|
|
27
|
-
"""Select the installed platform's instruction file, with legacy fallback."""
|
|
28
|
-
names = ("CLAUDE.md", "AGENTS.md") if platform in ("claude", "costrict") else ("AGENTS.md", "CLAUDE.md")
|
|
29
|
-
return next((name for name in names if os.path.isfile(os.path.join(project_root, name))), names[0])
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def load_config(project_root: str) -> dict:
|
|
33
|
-
config_path = os.path.join(project_root, ".code-flow", "config.yml")
|
|
34
|
-
if not os.path.exists(config_path):
|
|
35
|
-
return {}
|
|
36
|
-
try:
|
|
37
|
-
info = os.stat(config_path)
|
|
38
|
-
signature = (info.st_mtime_ns, info.st_ctime_ns, info.st_size)
|
|
39
|
-
except OSError as exc:
|
|
40
|
-
raise ConfigError(f"{config_path}: {exc}") from exc
|
|
41
|
-
cached = _config_cache.get(config_path)
|
|
42
|
-
if cached and cached["mtime"] == signature:
|
|
43
|
-
return cached["data"]
|
|
44
|
-
try:
|
|
45
|
-
import yaml
|
|
46
|
-
except ImportError as exc:
|
|
47
|
-
raise ConfigError("PyYAML is required to read code-flow configuration") from exc
|
|
48
|
-
try:
|
|
49
|
-
with open(config_path, "r", encoding="utf-8") as file:
|
|
50
|
-
data = yaml.safe_load(file)
|
|
51
|
-
if not isinstance(data, dict):
|
|
52
|
-
raise ConfigError(f"{config_path}: configuration must be a YAML mapping")
|
|
53
|
-
result = data
|
|
54
|
-
_config_cache[config_path] = {"mtime": signature, "data": result}
|
|
55
|
-
return result
|
|
56
|
-
except (OSError, UnicodeError, yaml.YAMLError) as exc:
|
|
57
|
-
raise ConfigError(f"{config_path}: {exc}") from exc
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def estimate_tokens(text: str) -> int:
|
|
61
|
-
return len(text) // 4
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def normalize_path(path: str) -> str:
|
|
65
|
-
# Unconditional backslash replacement, not `os.sep`, so paths pasted from
|
|
66
|
-
# a Windows machine into a prompt on macOS/Linux still normalize correctly.
|
|
67
|
-
# On Windows `os.sep == '\\'`, so behavior there is unchanged.
|
|
68
|
-
return path.replace("\\", "/")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def effective_project_root(project_root: str, absolute_path: str) -> str:
|
|
72
|
-
"""Resolve the project root that owns an edited file.
|
|
73
|
-
|
|
74
|
-
Parallel subagents edit `.code-flow/worktrees/<run>/<TASK>/...`; the nearest
|
|
75
|
-
`.code-flow` ancestor is that worktree's root, not the session root. Files
|
|
76
|
-
outside the session root and unresolvable paths fall back to `project_root`.
|
|
77
|
-
Lexical `abspath` (not `realpath`) keeps caller/returned paths in the same
|
|
78
|
-
coordinates (macOS /var ↔ /private/var symlink).
|
|
79
|
-
"""
|
|
80
|
-
root = os.path.abspath(project_root)
|
|
81
|
-
target = os.path.abspath(absolute_path) if absolute_path else root
|
|
82
|
-
if target != root and not target.startswith(root + os.sep):
|
|
83
|
-
return root
|
|
84
|
-
current = target
|
|
85
|
-
while True:
|
|
86
|
-
if os.path.isdir(os.path.join(current, ".code-flow")):
|
|
87
|
-
return current
|
|
88
|
-
if current == root:
|
|
89
|
-
return root
|
|
90
|
-
parent = os.path.dirname(current)
|
|
91
|
-
if parent == current:
|
|
92
|
-
return root
|
|
93
|
-
current = parent
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _spec_path_from_entry(entry) -> str:
|
|
97
|
-
cfg = normalize_spec_entry(entry)
|
|
98
|
-
return normalize_path(cfg.get("path", ""))
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def discover_spec_domains(project_root: str) -> dict:
|
|
102
|
-
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
103
|
-
discovered = {}
|
|
104
|
-
if not os.path.isdir(specs_root):
|
|
105
|
-
_spec_domains_cache[project_root] = discovered
|
|
106
|
-
return discovered
|
|
107
|
-
|
|
108
|
-
for root, _, files in os.walk(specs_root):
|
|
109
|
-
for filename in files:
|
|
110
|
-
if not filename.endswith(".md"):
|
|
111
|
-
continue
|
|
112
|
-
full_path = os.path.join(root, filename)
|
|
113
|
-
rel = normalize_path(os.path.relpath(full_path, specs_root))
|
|
114
|
-
parts = rel.split("/", 1)
|
|
115
|
-
if len(parts) < 2:
|
|
116
|
-
continue
|
|
117
|
-
domain = parts[0]
|
|
118
|
-
discovered.setdefault(domain, []).append(rel)
|
|
119
|
-
|
|
120
|
-
for domain in discovered:
|
|
121
|
-
discovered[domain] = sorted(set(discovered[domain]))
|
|
122
|
-
_spec_domains_cache[project_root] = discovered
|
|
123
|
-
return discovered
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _default_spec_entry(rel: str) -> dict:
|
|
127
|
-
tier = 0 if rel.endswith("/_map.md") else 1
|
|
128
|
-
# shared/ 下非 _map.md 的文件是供 cf-task:align / cf-task:prd 命令
|
|
129
|
-
# 显式 Read 的模板,不是约束规范,禁止通配符自动注入。
|
|
130
|
-
if rel.startswith("shared/") and not rel.endswith("/_map.md"):
|
|
131
|
-
return {"path": rel, "tags": [], "tier": tier}
|
|
132
|
-
return {"path": rel, "tags": ["*"], "tier": tier}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _mapping_signature(mapping: dict) -> str:
|
|
136
|
-
"""Content signature for the mapping cache.
|
|
137
|
-
|
|
138
|
-
`id(mapping)` keys were reused after GC, potentially serving a mapping
|
|
139
|
-
built from stale config contents. Hash the JSON projection instead.
|
|
140
|
-
"""
|
|
141
|
-
try:
|
|
142
|
-
return json.dumps(mapping or {}, ensure_ascii=False, sort_keys=True, default=str)
|
|
143
|
-
except (TypeError, ValueError):
|
|
144
|
-
return ""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def build_effective_mapping(project_root: str, mapping: dict) -> dict:
|
|
148
|
-
discovered = discover_spec_domains(project_root)
|
|
149
|
-
cache_key = (project_root, _mapping_signature(mapping), _mapping_signature(discovered))
|
|
150
|
-
cached = _effective_mapping_cache.get(cache_key)
|
|
151
|
-
if cached is not None:
|
|
152
|
-
return cached
|
|
153
|
-
effective = {}
|
|
154
|
-
|
|
155
|
-
for domain, rel_paths in discovered.items():
|
|
156
|
-
source_cfg = mapping.get(domain) or {}
|
|
157
|
-
normalized_specs = []
|
|
158
|
-
seen = set()
|
|
159
|
-
|
|
160
|
-
for entry in source_cfg.get("specs") or []:
|
|
161
|
-
rel = _spec_path_from_entry(entry)
|
|
162
|
-
if not rel or rel in seen:
|
|
163
|
-
continue
|
|
164
|
-
normalized_specs.append(normalize_spec_entry(entry))
|
|
165
|
-
seen.add(rel)
|
|
166
|
-
|
|
167
|
-
for rel in rel_paths:
|
|
168
|
-
if rel in seen:
|
|
169
|
-
continue
|
|
170
|
-
normalized_specs.append(_default_spec_entry(rel))
|
|
171
|
-
seen.add(rel)
|
|
172
|
-
|
|
173
|
-
effective[domain] = {
|
|
174
|
-
"patterns": source_cfg.get("patterns") or [],
|
|
175
|
-
"specs": normalized_specs,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for domain, source_cfg in (mapping or {}).items():
|
|
179
|
-
if domain in effective:
|
|
180
|
-
continue
|
|
181
|
-
normalized_specs = []
|
|
182
|
-
seen = set()
|
|
183
|
-
for entry in source_cfg.get("specs") or []:
|
|
184
|
-
rel = _spec_path_from_entry(entry)
|
|
185
|
-
if not rel or rel in seen:
|
|
186
|
-
continue
|
|
187
|
-
normalized_specs.append(normalize_spec_entry(entry))
|
|
188
|
-
seen.add(rel)
|
|
189
|
-
effective[domain] = {
|
|
190
|
-
"patterns": source_cfg.get("patterns") or [],
|
|
191
|
-
"specs": normalized_specs,
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
_effective_mapping_cache[cache_key] = effective
|
|
195
|
-
return effective
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def is_code_file(rel_path: str, runtime_config: dict) -> bool:
|
|
199
|
-
rel_path = normalize_path(rel_path)
|
|
200
|
-
for pattern in runtime_config.get("skip_paths") or []:
|
|
201
|
-
if fnmatch.fnmatch(rel_path, pattern):
|
|
202
|
-
return False
|
|
203
|
-
_, ext = os.path.splitext(rel_path)
|
|
204
|
-
if ext in (runtime_config.get("skip_extensions") or []):
|
|
205
|
-
return False
|
|
206
|
-
code_exts = runtime_config.get("code_extensions") or []
|
|
207
|
-
# Content key: id() reuse after GC could serve a stale extension set.
|
|
208
|
-
ext_key = tuple(code_exts)
|
|
209
|
-
ext_set = _ext_set_cache.get(ext_key)
|
|
210
|
-
if ext_set is None:
|
|
211
|
-
ext_set = frozenset(code_exts)
|
|
212
|
-
_ext_set_cache[ext_key] = ext_set
|
|
213
|
-
return ext in ext_set
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def match_domains(rel_path: str, mapping: dict) -> list:
|
|
217
|
-
rel_path = normalize_path(rel_path)
|
|
218
|
-
domains = []
|
|
219
|
-
for domain, cfg in (mapping or {}).items():
|
|
220
|
-
patterns = cfg.get("patterns") or []
|
|
221
|
-
for pattern in patterns:
|
|
222
|
-
if fnmatch.fnmatch(rel_path, pattern):
|
|
223
|
-
domains.append(domain)
|
|
224
|
-
break
|
|
225
|
-
return domains
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def normalize_spec_entry(entry) -> dict:
|
|
229
|
-
"""Normalize spec config entry. Supports both old (string) and new (dict) format."""
|
|
230
|
-
if isinstance(entry, str):
|
|
231
|
-
return {"path": entry, "tags": ["*"], "tier": 1}
|
|
232
|
-
if isinstance(entry, dict):
|
|
233
|
-
return {
|
|
234
|
-
"path": entry.get("path", ""),
|
|
235
|
-
"tags": entry.get("tags", ["*"]),
|
|
236
|
-
"tier": entry.get("tier", 1),
|
|
237
|
-
}
|
|
238
|
-
return {}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
_BULLET_PREFIXES = ("- ", "* ", "+ ")
|
|
242
|
-
_COMPRESS_HTML_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
243
|
-
_COMPRESS_TRAILING_WS_RE = re.compile(r"[ \t]+$", re.MULTILINE)
|
|
244
|
-
_COMPRESS_BLANK_LINES_RE = re.compile(r"\n{3,}")
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def compress_content(text: str) -> str:
|
|
248
|
-
"""Lossless compression for spec content injected at Hook time.
|
|
249
|
-
|
|
250
|
-
Five conservative, semantics-preserving transforms:
|
|
251
|
-
1. Strip multi-line HTML comments ``<!-- ... -->``.
|
|
252
|
-
2. Strip trailing whitespace on every line.
|
|
253
|
-
3. Collapse runs of 2+ blank lines into a single blank line.
|
|
254
|
-
4. Drop a bullet line ( ``-`` / ``*`` / ``+`` ) equal to the previous line.
|
|
255
|
-
5. Strip leading/trailing blank lines of the whole text.
|
|
256
|
-
|
|
257
|
-
Idempotent: ``compress_content(compress_content(x)) == compress_content(x)``.
|
|
258
|
-
On any exception, log to stderr and return the original text so Hook
|
|
259
|
-
injection is never broken by a compression bug.
|
|
260
|
-
"""
|
|
261
|
-
if not isinstance(text, str) or not text:
|
|
262
|
-
return text
|
|
263
|
-
try:
|
|
264
|
-
result = _COMPRESS_HTML_RE.sub("", text)
|
|
265
|
-
result = _COMPRESS_TRAILING_WS_RE.sub("", result)
|
|
266
|
-
result = _COMPRESS_BLANK_LINES_RE.sub("\n\n", result)
|
|
267
|
-
lines = result.split("\n")
|
|
268
|
-
out_lines: list = []
|
|
269
|
-
prev_line: str = ""
|
|
270
|
-
in_fence = False
|
|
271
|
-
for line in lines:
|
|
272
|
-
stripped = line.lstrip()
|
|
273
|
-
if stripped.startswith("```"):
|
|
274
|
-
in_fence = not in_fence
|
|
275
|
-
# never dedup inside fenced code blocks — ✅/❌ example snippets
|
|
276
|
-
# may legitimately repeat identical bullet-looking lines (FEAT-07)
|
|
277
|
-
is_bullet = (not in_fence) and stripped.startswith(_BULLET_PREFIXES)
|
|
278
|
-
if is_bullet and out_lines and line == prev_line:
|
|
279
|
-
continue
|
|
280
|
-
out_lines.append(line)
|
|
281
|
-
prev_line = line
|
|
282
|
-
result = "\n".join(out_lines)
|
|
283
|
-
return result.strip()
|
|
284
|
-
except Exception as exc:
|
|
285
|
-
_log(f"compress_content error: {exc}")
|
|
286
|
-
return text
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def non_injectable_specs(effective_mapping: dict) -> set:
|
|
290
|
-
"""tags: [] 的命令专用模板(如 shared 的 PRD/设计模板)——永不自动注入,
|
|
291
|
-
预算统计(cf-stats/cf-scan)必须排除,避免利用率虚高。"""
|
|
292
|
-
excluded = set()
|
|
293
|
-
for domain_cfg in (effective_mapping or {}).values():
|
|
294
|
-
for entry in (domain_cfg or {}).get("specs") or []:
|
|
295
|
-
cfg = normalize_spec_entry(entry)
|
|
296
|
-
if cfg.get("path") and not cfg.get("tags"):
|
|
297
|
-
excluded.add(cfg["path"])
|
|
298
|
-
return excluded
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def resolve_quality_loop(config: dict) -> dict:
|
|
302
|
-
"""Resolve quality_loop feature switches with safe defaults (RULE-06).
|
|
303
|
-
|
|
304
|
-
Master `enabled`: only literal True enables — missing/other keeps the
|
|
305
|
-
pre-0.6 behavior for upgraded users; new inits opt in via the config
|
|
306
|
-
template. Sub-switches follow the master and only literal False disables
|
|
307
|
-
them (resolve_compress philosophy).
|
|
308
|
-
"""
|
|
309
|
-
if not isinstance(config, dict):
|
|
310
|
-
return {"enabled": False, "post_check": False,
|
|
311
|
-
"stop_check": False, "finish_check": False,
|
|
312
|
-
"correction_capture": False,
|
|
313
|
-
"compress_reminder": False,
|
|
314
|
-
"heavy_at_finish": False,
|
|
315
|
-
"finish_verifier_budget": 300.0}
|
|
316
|
-
cfg = config.get("quality_loop")
|
|
317
|
-
cfg = cfg if isinstance(cfg, dict) else {}
|
|
318
|
-
enabled = cfg.get("enabled") is True
|
|
319
|
-
|
|
320
|
-
def _sub(key: str) -> bool:
|
|
321
|
-
return enabled and cfg.get(key) is not False
|
|
322
|
-
|
|
323
|
-
# 任务级 verifier 执行预算(秒):超预算的 command/test verifier 延后到
|
|
324
|
-
# 需求级 verify-e2e;0 = 不限制(恢复旧行为)。非法值回落默认 300。
|
|
325
|
-
budget = cfg.get("finish_verifier_budget", 300)
|
|
326
|
-
if isinstance(budget, bool) or not isinstance(budget, (int, float)) or budget < 0 or budget != budget:
|
|
327
|
-
budget = 300.0
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
"enabled": enabled,
|
|
331
|
-
"post_check": _sub("post_check"),
|
|
332
|
-
"stop_check": _sub("stop_check"),
|
|
333
|
-
"finish_check": _sub("finish_check"),
|
|
334
|
-
"correction_capture": _sub("correction_capture"),
|
|
335
|
-
"compress_reminder": _sub("compress_reminder"),
|
|
336
|
-
# heavy validator 默认只在归档 cf_validation / /cf-validate 全量执行
|
|
337
|
-
"heavy_at_finish": enabled and cfg.get("heavy_at_finish") is True,
|
|
338
|
-
"finish_verifier_budget": float(budget),
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
ENFORCEMENT_MODES = frozenset(("required", "warn", "inject"))
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def resolve_enforcement(config: dict) -> str:
|
|
346
|
-
"""Project-wide Spec Workflow enforcement tier (light-mode ladder).
|
|
347
|
-
|
|
348
|
-
required — full fail-closed gates (default; upgraded projects keep it);
|
|
349
|
-
warn — gates run but only log/report, never block the session;
|
|
350
|
-
inject — routing/injection only; gates and blocks are disabled.
|
|
351
|
-
Missing or invalid values fail closed to "required".
|
|
352
|
-
"""
|
|
353
|
-
if not isinstance(config, dict):
|
|
354
|
-
return "required"
|
|
355
|
-
workflow = config.get("spec_workflow")
|
|
356
|
-
workflow = workflow if isinstance(workflow, dict) else {}
|
|
357
|
-
value = workflow.get("enforcement")
|
|
358
|
-
if isinstance(value, str) and value in ENFORCEMENT_MODES:
|
|
359
|
-
return value
|
|
360
|
-
return "required"
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def parse_spec_frontmatter(content: str) -> tuple:
|
|
364
|
-
"""Split optional YAML frontmatter from spec content → (meta, body).
|
|
365
|
-
|
|
366
|
-
Flat `key: value` parsing keeps the hook hot path free of pyyaml; content
|
|
367
|
-
without frontmatter returns ({}, content) unchanged.
|
|
368
|
-
"""
|
|
369
|
-
return parse_frontmatter(content)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
def spec_description(content: str) -> str:
|
|
373
|
-
"""One-line applicability summary for the spec catalog.
|
|
374
|
-
|
|
375
|
-
Priority: frontmatter `description:` → first blockquote line → H1 text.
|
|
376
|
-
Returns "" when none is found (cf-scan flags those specs).
|
|
377
|
-
"""
|
|
378
|
-
meta, body = parse_spec_frontmatter(content)
|
|
379
|
-
desc = (meta.get("description") or "").strip()
|
|
380
|
-
if desc:
|
|
381
|
-
return desc
|
|
382
|
-
h1 = ""
|
|
383
|
-
for line in body.splitlines():
|
|
384
|
-
stripped = line.strip()
|
|
385
|
-
if stripped.startswith(">"):
|
|
386
|
-
return stripped.lstrip("> ").strip()
|
|
387
|
-
if not h1 and stripped.startswith("# "):
|
|
388
|
-
h1 = stripped[2:].strip()
|
|
389
|
-
return h1
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
_catalog_cache: dict = {}
|
|
393
|
-
|
|
394
|
-
CATALOG_HEADER = (
|
|
395
|
-
"## Spec Catalog (code-flow)\n\n"
|
|
396
|
-
"以下规范文件位于 `.code-flow/specs/`。开始编码或修改文件前,"
|
|
397
|
-
"必须先 Read 与当前任务场景匹配的 spec 全文;与场景无关的不要读取。"
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
class CatalogBudgetError(ConfigError):
|
|
402
|
-
"""Catalog cannot fit its mandatory header and complete-list notice."""
|
|
403
|
-
|
|
404
|
-
def __init__(self, minimum: int) -> None:
|
|
405
|
-
self.minimum = minimum
|
|
406
|
-
super().__init__(f"catalog_max is too small; minimum is {minimum} estimated tokens")
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def _catalog_entries(project_root: str, effective_mapping: dict) -> list:
|
|
410
|
-
"""Collect injectable specs as (tier, rel_path, abs_path) sorted tier1-first."""
|
|
411
|
-
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
412
|
-
entries = []
|
|
413
|
-
for domain in sorted(effective_mapping.keys()):
|
|
414
|
-
for entry in (effective_mapping[domain] or {}).get("specs") or []:
|
|
415
|
-
cfg = normalize_spec_entry(entry)
|
|
416
|
-
rel = cfg.get("path", "")
|
|
417
|
-
# tags: [] marks command-only templates — never advertised either
|
|
418
|
-
if not rel or rel.startswith("_session/") or not cfg.get("tags"):
|
|
419
|
-
continue
|
|
420
|
-
entries.append((cfg.get("tier", 1), rel, os.path.join(specs_root, rel)))
|
|
421
|
-
# Tier 1 constraints first so budget truncation drops navigation maps first
|
|
422
|
-
entries.sort(key=lambda item: (item[0] != 1, item[1]))
|
|
423
|
-
return entries
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
def _catalog_lines(entries: list[tuple[int, str, str]]) -> list[str]:
|
|
427
|
-
lines = []
|
|
428
|
-
for tier, rel, abs_path in entries:
|
|
429
|
-
try:
|
|
430
|
-
with open(abs_path, "r", encoding="utf-8") as f:
|
|
431
|
-
desc = spec_description(f.read())
|
|
432
|
-
except (OSError, UnicodeError) as exc:
|
|
433
|
-
_log(f"Spec Catalog read failed: {rel}: {exc}")
|
|
434
|
-
continue
|
|
435
|
-
suffix = "(导航地图)" if tier == 0 else ""
|
|
436
|
-
lines.append(f"- `{rel}` — {desc or '(no description)'}{suffix}")
|
|
437
|
-
return lines
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def _bounded_catalog(lines: list[str], maximum: int) -> str:
|
|
441
|
-
catalog = CATALOG_HEADER
|
|
442
|
-
included = 0
|
|
443
|
-
def footer(count: int) -> str:
|
|
444
|
-
if count == len(lines):
|
|
445
|
-
return ""
|
|
446
|
-
return (f"\n已展示 {count}/{len(lines)} 项;其余规范请运行 "
|
|
447
|
-
"`code-flow spec catalog --root . --stage code --json` 获取。")
|
|
448
|
-
for line in lines:
|
|
449
|
-
candidate = catalog + "\n" + line
|
|
450
|
-
if estimate_tokens(candidate + footer(included + 1)) > maximum:
|
|
451
|
-
continue
|
|
452
|
-
catalog = candidate
|
|
453
|
-
included += 1
|
|
454
|
-
text = catalog + footer(included)
|
|
455
|
-
if estimate_tokens(text) > maximum:
|
|
456
|
-
raise CatalogBudgetError(estimate_tokens(text))
|
|
457
|
-
return text
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
def build_spec_catalog(project_root: str, effective_mapping: dict, catalog_max: int = 200) -> str:
|
|
461
|
-
"""Publish bounded navigation with an explicit complete-catalog entry point."""
|
|
462
|
-
entries = _catalog_entries(project_root, effective_mapping)
|
|
463
|
-
if not entries:
|
|
464
|
-
return ""
|
|
465
|
-
signature = []
|
|
466
|
-
for _, _, path in entries:
|
|
467
|
-
try:
|
|
468
|
-
info = os.stat(path)
|
|
469
|
-
signature.append((path, info.st_mtime_ns, info.st_ctime_ns, info.st_size))
|
|
470
|
-
except OSError:
|
|
471
|
-
signature.append((path, 0, 0, 0))
|
|
472
|
-
cached = _catalog_cache.get(project_root)
|
|
473
|
-
if cached and cached["signature"] == signature and cached["catalog_max"] == catalog_max:
|
|
474
|
-
return cached["catalog"]
|
|
475
|
-
lines = _catalog_lines(entries)
|
|
476
|
-
catalog = _bounded_catalog(lines, catalog_max) if lines else ""
|
|
477
|
-
_catalog_cache[project_root] = {"signature": signature, "catalog_max": catalog_max, "catalog": catalog}
|
|
478
|
-
return catalog
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
def catalog_omissions(project_root: str, effective_mapping: dict, catalog_max: int) -> dict[str, object]:
|
|
482
|
-
"""Compute omissions from the actual bounded catalog without changing its ordering."""
|
|
483
|
-
from collections import Counter
|
|
484
|
-
lines = _catalog_lines(_catalog_entries(project_root, effective_mapping))
|
|
485
|
-
text = build_spec_catalog(project_root, effective_mapping, catalog_max)
|
|
486
|
-
remaining = Counter(line for line in text.splitlines() if line.startswith('- `'))
|
|
487
|
-
dropped: list[str] = []
|
|
488
|
-
for line in lines:
|
|
489
|
-
if remaining[line]:
|
|
490
|
-
remaining[line] -= 1
|
|
491
|
-
else:
|
|
492
|
-
dropped.append(line.split('`', 2)[1])
|
|
493
|
-
return {"dropped_count": len(dropped),
|
|
494
|
-
"dropped_domains": sorted({path.split('/', 1)[0] for path in dropped})}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
def assemble_context(specs: list, heading: str) -> str:
|
|
498
|
-
parts = [heading]
|
|
499
|
-
parts.append("以上规范是本次开发的约束条件,生成代码必须遵循。")
|
|
500
|
-
parts.append("---")
|
|
501
|
-
tier0 = [s for s in specs if s.get("tier", 1) == 0]
|
|
502
|
-
tier1 = [s for s in specs if s.get("tier", 1) != 0]
|
|
503
|
-
|
|
504
|
-
if tier0:
|
|
505
|
-
parts.append("### Navigation (Retrieval Map)")
|
|
506
|
-
for spec in tier0:
|
|
507
|
-
parts.append(f"#### {spec['path']}")
|
|
508
|
-
parts.append(spec["content"])
|
|
509
|
-
|
|
510
|
-
if tier1:
|
|
511
|
-
parts.append("### Constraints (matched by file context)")
|
|
512
|
-
for spec in tier1:
|
|
513
|
-
parts.append(f"#### {spec['path']}")
|
|
514
|
-
parts.append(spec["content"])
|
|
515
|
-
|
|
516
|
-
return "\n\n".join(parts)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
def _log(msg: str) -> None:
|
|
520
|
-
"""Diagnostics go to stderr through the shared L0 writer (fix #9)."""
|
|
521
|
-
cf_log.warn(msg)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
_PROCESS_START = time.monotonic()
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
def timing_log(name: str) -> None:
|
|
528
|
-
"""Emit process-elapsed ms to stderr only when CF_DEBUG=1 (diagnostics).
|
|
529
|
-
|
|
530
|
-
Never touches stdout, so the hook JSON protocol is unaffected; a default
|
|
531
|
-
run pays one env lookup and no IO.
|
|
532
|
-
"""
|
|
533
|
-
if os.environ.get("CF_DEBUG") == "1":
|
|
534
|
-
_log(f"{name}: {int((time.monotonic() - _PROCESS_START) * 1000)}ms")
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
def phase_timing(name: str, started: float) -> None:
|
|
538
|
-
"""Emit a named phase duration to stderr when CF_DEBUG=1."""
|
|
539
|
-
if os.environ.get("CF_DEBUG") == "1":
|
|
540
|
-
_log(f"cf_phase {name}: {int((time.monotonic() - started) * 1000)}ms")
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def ensure_utf8_io() -> None:
|
|
544
|
-
"""Force stdin/stdout/stderr to UTF-8 so Windows hooks don't mojibake.
|
|
545
|
-
|
|
546
|
-
Claude Code/Codex pass UTF-8 JSON over stdin and expect UTF-8 over stdout,
|
|
547
|
-
but Python on Windows defaults streams to the system codepage (cp936 on
|
|
548
|
-
zh-CN locales). That corrupts CJK content end-to-end — both the parsed
|
|
549
|
-
prompt and anything written back, including the CF_DEBUG=1 .debug.log.
|
|
550
|
-
``reconfigure`` is a TextIOWrapper-only method, so test doubles built on
|
|
551
|
-
``io.StringIO`` are silently skipped.
|
|
552
|
-
"""
|
|
553
|
-
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
554
|
-
reconfigure = getattr(stream, "reconfigure", None)
|
|
555
|
-
if callable(reconfigure):
|
|
556
|
-
try:
|
|
557
|
-
reconfigure(encoding="utf-8")
|
|
558
|
-
except Exception:
|
|
559
|
-
pass
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
def resolve_compress(quality_config: dict) -> bool:
|
|
563
|
-
"""Return whether Hook-time spec compression is enabled.
|
|
564
|
-
|
|
565
|
-
Only a literal ``False`` turns it off; missing, ``None``, or any
|
|
566
|
-
non-bool value falls back to ``True`` so upgrades pick up compression
|
|
567
|
-
without requiring a config edit.
|
|
568
|
-
"""
|
|
569
|
-
if not isinstance(quality_config, dict):
|
|
570
|
-
return True
|
|
571
|
-
value = quality_config.get("compress")
|
|
572
|
-
if value is False:
|
|
573
|
-
return False
|
|
574
|
-
return True
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
def resolve_session_id(hook_data: dict) -> str:
|
|
578
|
-
"""Derive a session id consistent across PreToolUse / UserPromptSubmit hooks.
|
|
579
|
-
|
|
580
|
-
Prefer the platform-provided id so prompt, edit, feedback, and stop events
|
|
581
|
-
share one trace. Fall back to the PID for older runtimes and unit tests.
|
|
582
|
-
"""
|
|
583
|
-
if isinstance(hook_data, dict):
|
|
584
|
-
sid = hook_data.get("session_id")
|
|
585
|
-
if sid:
|
|
586
|
-
return str(sid)
|
|
587
|
-
return str(os.getpid())
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def debug_log(msg: str, project_root: str = None) -> None:
|
|
591
|
-
"""Append a debug line to .code-flow/.debug.log when CF_DEBUG=1.
|
|
592
|
-
Silent no-op unless CF_DEBUG=1 so default runs don't pay any IO cost.
|
|
593
|
-
Writes to a dotfile so cf_scan.py skips it and processDir upgrades don't overwrite it.
|
|
594
|
-
Failures (missing dir, unwritable fs) are swallowed on purpose
|
|
595
|
-
— we must never break the hook JSON protocol over logging.
|
|
596
|
-
"""
|
|
597
|
-
if os.environ.get("CF_DEBUG") != "1":
|
|
598
|
-
return
|
|
599
|
-
root = project_root or os.getcwd()
|
|
600
|
-
debug_dir = os.path.join(root, ".code-flow")
|
|
601
|
-
log_path = os.path.join(debug_dir, ".debug.log")
|
|
602
|
-
try:
|
|
603
|
-
os.makedirs(debug_dir, exist_ok=True)
|
|
604
|
-
from datetime import datetime
|
|
605
|
-
ts = datetime.now().isoformat(timespec="seconds")
|
|
606
|
-
with open(log_path, "a", encoding="utf-8") as f:
|
|
607
|
-
f.write(f"{ts} {msg}\n")
|
|
608
|
-
except Exception:
|
|
609
|
-
return
|