@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,98 +0,0 @@
|
|
|
1
|
-
"""OpenCode v2 native event RPC; no Claude/Codex hook wire translation."""
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from codeflow.core import cf_log
|
|
7
|
-
from codeflow.core.cf_core import ensure_utf8_io
|
|
8
|
-
from codeflow.spec.cf_edit_service import after_edit, prepare_edits
|
|
9
|
-
from codeflow.spec.cf_injection import injection_output
|
|
10
|
-
from codeflow.core.cf_exec_base import execution_session
|
|
11
|
-
from codeflow.core.cf_native_paths import project_root, validated_paths
|
|
12
|
-
from codeflow.core.cf_patch import FileOperation, parse_patch
|
|
13
|
-
from codeflow.core.cf_runtime_install import verify_install
|
|
14
|
-
from codeflow.hooks.cf_stop_hook import build_stop_result
|
|
15
|
-
from codeflow.hooks.cf_user_prompt_hook import build_prompt_context
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def edit_paths(root: str, directory: str, event: dict[str, object]) -> tuple[tuple[str, bool], ...]:
|
|
19
|
-
tool, inputs = event.get("tool"), event.get("input")
|
|
20
|
-
if not isinstance(inputs, dict):
|
|
21
|
-
raise ValueError("native tool input must be an object")
|
|
22
|
-
if tool in {"edit", "write"}:
|
|
23
|
-
path = inputs.get("path")
|
|
24
|
-
if not isinstance(path, str) or not path or "\x00" in path:
|
|
25
|
-
raise ValueError(f"{tool}.input.path must be a nonempty path")
|
|
26
|
-
operations = (FileOperation("update", path),)
|
|
27
|
-
elif tool == "patch":
|
|
28
|
-
command = inputs.get("patchText")
|
|
29
|
-
if not isinstance(command, str):
|
|
30
|
-
raise ValueError("patch.input.patchText must be a string")
|
|
31
|
-
operations = parse_patch(command)
|
|
32
|
-
else:
|
|
33
|
-
return ()
|
|
34
|
-
return validated_paths(root, directory, operations)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def tool_result(root: str, directory: str, kind: str, event: dict[str, object], sid: str) -> dict[str, object]:
|
|
38
|
-
tool = event.get("tool")
|
|
39
|
-
if tool not in {"edit", "write", "patch"}:
|
|
40
|
-
return {"context": "", "blocked": False}
|
|
41
|
-
if kind == "tool.after" and event.get("status") != "completed":
|
|
42
|
-
cf_log.degrade(root, "opencode_tool", f"{tool}: edit not confirmed (status={event.get('status')})", sid)
|
|
43
|
-
return {"context": "", "blocked": False}
|
|
44
|
-
paths = edit_paths(root, directory, event)
|
|
45
|
-
if kind == "tool.before":
|
|
46
|
-
context, blocked = prepare_edits(root, tuple(path for path, _ in paths), sid, str(tool))
|
|
47
|
-
return {"context": context, "blocked": blocked}
|
|
48
|
-
text = []
|
|
49
|
-
blocked = False
|
|
50
|
-
for path, deleted in paths:
|
|
51
|
-
feedback = after_edit(root, path, sid, str(tool), deleted)
|
|
52
|
-
if feedback:
|
|
53
|
-
text.append(feedback)
|
|
54
|
-
context = "\n\n".join(dict.fromkeys(text))
|
|
55
|
-
return {"context": context, "blocked": blocked}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def dispatch(data: dict[str, object]) -> dict[str, object]:
|
|
59
|
-
kind, directory, event = data.get("kind"), data.get("directory"), data.get("event")
|
|
60
|
-
if not isinstance(directory, str) or not os.path.isabs(directory) or not isinstance(event, dict):
|
|
61
|
-
raise ValueError("native event requires absolute session directory and an event object")
|
|
62
|
-
sid = event.get("sessionID")
|
|
63
|
-
if not isinstance(sid, str) or not sid:
|
|
64
|
-
raise ValueError("native event requires sessionID")
|
|
65
|
-
root = project_root(directory)
|
|
66
|
-
if not root:
|
|
67
|
-
return {"context": "", "blocked": False}
|
|
68
|
-
verify_install(root)
|
|
69
|
-
if kind == "session.prompt":
|
|
70
|
-
prompt = event.get("prompt")
|
|
71
|
-
if not isinstance(prompt, dict) or not isinstance(prompt.get("text"), str):
|
|
72
|
-
raise ValueError("native prompt.text must be a string")
|
|
73
|
-
text, mode = build_prompt_context(prompt["text"], sid, root)
|
|
74
|
-
return {"context": text, "blocked": mode == "blocked"}
|
|
75
|
-
if kind in {"tool.before", "tool.after"}:
|
|
76
|
-
return tool_result(root, directory, kind, event, sid)
|
|
77
|
-
if kind == "session.idle":
|
|
78
|
-
with execution_session():
|
|
79
|
-
result = build_stop_result(root, sid)
|
|
80
|
-
return {"context": result.get("reason", ""), "blocked": result.get("decision") == "block"}
|
|
81
|
-
raise ValueError(f"unknown native event kind: {kind}")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def main() -> None:
|
|
85
|
-
ensure_utf8_io()
|
|
86
|
-
try:
|
|
87
|
-
data = json.loads(sys.stdin.read())
|
|
88
|
-
if not isinstance(data, dict):
|
|
89
|
-
raise ValueError("native RPC input must be an object")
|
|
90
|
-
with injection_output():
|
|
91
|
-
result = dispatch(data)
|
|
92
|
-
sys.stdout.write(json.dumps(result, ensure_ascii=False))
|
|
93
|
-
sys.stdout.flush()
|
|
94
|
-
return
|
|
95
|
-
except Exception as exc:
|
|
96
|
-
cf_log.warn(f"cf_opencode_event error: {exc}")
|
|
97
|
-
result = {"error": f"code-flow OpenCode runtime failed: {exc}. Run code-flow migrate --runtime --dry-run, then --apply."}
|
|
98
|
-
sys.stdout.write(json.dumps(result, ensure_ascii=False))
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Claude-style PostToolUse protocol entry; edit rules live in cf_edit_service."""
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
|
|
7
|
-
from codeflow.core.cf_core import ensure_utf8_io, resolve_session_id, timing_log
|
|
8
|
-
from codeflow.core.cf_log import warn
|
|
9
|
-
from codeflow.spec.cf_edit_service import after_edit
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main() -> None:
|
|
13
|
-
try:
|
|
14
|
-
ensure_utf8_io()
|
|
15
|
-
if sys.stdin.isatty():
|
|
16
|
-
warn("cf_post_hook.py: missing stdin event")
|
|
17
|
-
return
|
|
18
|
-
raw = sys.stdin.read()
|
|
19
|
-
if not raw.strip():
|
|
20
|
-
return
|
|
21
|
-
data = json.loads(raw)
|
|
22
|
-
tool = data.get("tool_name", "")
|
|
23
|
-
path = (data.get("tool_input") or {}).get("file_path", "")
|
|
24
|
-
if tool not in {"Edit", "Write", "MultiEdit"} or not isinstance(path, str) or not path:
|
|
25
|
-
return
|
|
26
|
-
text = after_edit(os.getcwd(), path, resolve_session_id(data), tool)
|
|
27
|
-
if text:
|
|
28
|
-
sys.stdout.write(json.dumps({"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": text}}, ensure_ascii=False))
|
|
29
|
-
except Exception as exc:
|
|
30
|
-
warn(f"cf_post_hook.py error: {exc}")
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Claude-style PreToolUse protocol entry; edit rules live in cf_edit_service."""
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
|
|
7
|
-
from codeflow.core.cf_core import ensure_utf8_io, resolve_session_id, timing_log
|
|
8
|
-
from codeflow.core.cf_log import warn
|
|
9
|
-
from codeflow.spec.cf_edit_service import prepare_edit
|
|
10
|
-
from codeflow.spec.cf_injection import injection_output, pre_tool_payload
|
|
11
|
-
from codeflow.core.cf_native_paths import project_root
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def main() -> None:
|
|
15
|
-
try:
|
|
16
|
-
ensure_utf8_io()
|
|
17
|
-
if sys.stdin.isatty():
|
|
18
|
-
warn("cf_pre_tool_hook.py: missing stdin event")
|
|
19
|
-
return
|
|
20
|
-
raw = sys.stdin.read()
|
|
21
|
-
if not raw.strip():
|
|
22
|
-
return
|
|
23
|
-
data = json.loads(raw)
|
|
24
|
-
tool = data.get("tool_name", "")
|
|
25
|
-
path = (data.get("tool_input") or {}).get("file_path", "")
|
|
26
|
-
if tool not in {"Edit", "Write", "MultiEdit"} or not isinstance(path, str) or not path:
|
|
27
|
-
return
|
|
28
|
-
directory = os.getcwd()
|
|
29
|
-
root = project_root(directory)
|
|
30
|
-
if not root:
|
|
31
|
-
return
|
|
32
|
-
absolute = path if os.path.isabs(path) else os.path.join(directory, path)
|
|
33
|
-
with injection_output():
|
|
34
|
-
text, blocked = prepare_edit(root, absolute, resolve_session_id(data), tool)
|
|
35
|
-
if text:
|
|
36
|
-
sys.stdout.write(json.dumps(pre_tool_payload(text, blocked), ensure_ascii=False))
|
|
37
|
-
sys.stdout.flush()
|
|
38
|
-
except Exception as exc:
|
|
39
|
-
warn(f"cf_pre_tool_hook.py error: {exc}")
|
|
40
|
-
text = f"SPEC_WORKFLOW_BLOCKED [pre_tool_error]: {exc}"
|
|
41
|
-
sys.stdout.write(json.dumps(pre_tool_payload(text, True), ensure_ascii=False))
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Stop Hook: run validation.yml checks on session-edited files (FEAT-03).
|
|
3
|
-
|
|
4
|
-
Flow: session edit events (cf_log) → match validator triggers → run commands
|
|
5
|
-
serially under a total budget → failures surface as {"decision": "block",
|
|
6
|
-
"reason": ...} so the agent fixes them before the session truly ends.
|
|
7
|
-
|
|
8
|
-
Protocol notes (deviation from the injection-hook contract, recorded in
|
|
9
|
-
TASK-010): the Stop event's feedback channel is decision/reason — there is no
|
|
10
|
-
additionalContext for Stop. `stop_hook_active` is respected so an already
|
|
11
|
-
continued session is never re-blocked (no loops). All-pass → silent exit 0.
|
|
12
|
-
"""
|
|
13
|
-
import json
|
|
14
|
-
import os
|
|
15
|
-
import re
|
|
16
|
-
import sys
|
|
17
|
-
import time
|
|
18
|
-
|
|
19
|
-
from codeflow.core import cf_log
|
|
20
|
-
from codeflow.core.cf_exec_base import execution_session
|
|
21
|
-
from codeflow.cli.cf_spec_context import load_active_task
|
|
22
|
-
from codeflow.task.cf_task_runtime import run_done_gate
|
|
23
|
-
from codeflow.core.cf_stop_guard import StopAlreadyRunning, stop_guard
|
|
24
|
-
from codeflow.acceptance.cf_acceptance_evidence import line_status, scenario_line
|
|
25
|
-
from codeflow.task.cf_task_doc import TaskDocumentError, parse_task_document
|
|
26
|
-
from codeflow.core.cf_validators import PARSE_ERROR, VALID, load_validators, run_validators
|
|
27
|
-
from codeflow.acceptance.cf_acceptance_manifest_core import coverage_execution
|
|
28
|
-
from codeflow.core.cf_core import (
|
|
29
|
-
ensure_utf8_io,
|
|
30
|
-
load_config,
|
|
31
|
-
normalize_path,
|
|
32
|
-
resolve_enforcement,
|
|
33
|
-
resolve_quality_loop,
|
|
34
|
-
resolve_session_id,
|
|
35
|
-
timing_log,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
TOTAL_BUDGET_SECONDS = 30.0
|
|
39
|
-
GATE_BUDGET_SECONDS = 25.0
|
|
40
|
-
_SCENARIO_RE = re.compile(r"\b[SEB]-\d+\b")
|
|
41
|
-
_UNVERIFIED_RE = re.compile(r"\b(?:planned|pending|tbd)\b", re.IGNORECASE)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def session_edited_files(project_root: str, sid: str) -> list:
|
|
45
|
-
files = []
|
|
46
|
-
seen = set()
|
|
47
|
-
for event in cf_log.read_events(project_root, days=2, events=("edit",)):
|
|
48
|
-
if event.get("sid") != sid:
|
|
49
|
-
continue
|
|
50
|
-
rel = (event.get("data") or {}).get("file")
|
|
51
|
-
if rel and rel not in seen:
|
|
52
|
-
seen.add(rel)
|
|
53
|
-
files.append(rel)
|
|
54
|
-
return files
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _root_scoped_files(project_root: str, files: list) -> list:
|
|
58
|
-
"""Drop worktree-internal / outside-root paths.
|
|
59
|
-
|
|
60
|
-
Parallel subagents edit another working copy (.code-flow/worktrees/<run>/<TASK>/);
|
|
61
|
-
those paths must not trigger this root's validators.
|
|
62
|
-
"""
|
|
63
|
-
root = os.path.realpath(project_root)
|
|
64
|
-
scoped = []
|
|
65
|
-
for rel in files:
|
|
66
|
-
path = normalize_path(rel)
|
|
67
|
-
if path.startswith(".code-flow/worktrees/"):
|
|
68
|
-
continue
|
|
69
|
-
absolute = os.path.realpath(os.path.join(root, path))
|
|
70
|
-
if absolute == root or absolute.startswith(root + os.sep):
|
|
71
|
-
scoped.append(rel)
|
|
72
|
-
return scoped
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _is_active_task_file(rel_path: str) -> bool:
|
|
76
|
-
path = normalize_path(rel_path)
|
|
77
|
-
if not path.startswith(".code-flow/tasks/") or "/archived/" in path:
|
|
78
|
-
return False
|
|
79
|
-
return path.endswith(".md") and not path.endswith((".design.md", ".prd.md", ".scenarios.md"))
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _subsection(section: str, heading: str) -> str:
|
|
83
|
-
match = re.search(
|
|
84
|
-
rf"(?ms)^### {re.escape(heading)}\s*$\n(.*?)(?=^### |^## |\Z)",
|
|
85
|
-
section,
|
|
86
|
-
)
|
|
87
|
-
return match.group(1) if match else ""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _acceptance_gap(task_id: str, section: str, coverage: str) -> str:
|
|
91
|
-
|
|
92
|
-
status = re.search(r"(?m)^- \*\*Status\*\*: ([^\n]+)", section)
|
|
93
|
-
# done = implementation finished; verified = E2E/final acceptance closed.
|
|
94
|
-
# Both enter the same contract checks; anything else is still in flight.
|
|
95
|
-
if not status or status.group(1).strip() not in ("done", "verified"):
|
|
96
|
-
return ""
|
|
97
|
-
refs_match = re.search(r"(?m)^- \*\*Acceptance-Refs\*\*: ([^\n]+)", section)
|
|
98
|
-
if not refs_match:
|
|
99
|
-
return f"{task_id} 缺少 Acceptance-Refs"
|
|
100
|
-
refs = refs_match.group(1).strip()
|
|
101
|
-
if refs.upper().startswith("N/A"):
|
|
102
|
-
return ""
|
|
103
|
-
scenarios = sorted(set(_SCENARIO_RE.findall(refs)))
|
|
104
|
-
if not scenarios:
|
|
105
|
-
return f"{task_id} Acceptance-Refs 未引用 S/E/B 场景"
|
|
106
|
-
contract = _subsection(section, "Acceptance Contract")
|
|
107
|
-
evidence = _subsection(section, "Acceptance Evidence")
|
|
108
|
-
if not contract or not evidence:
|
|
109
|
-
return f"{task_id} 缺少 Acceptance Contract 或 Acceptance Evidence"
|
|
110
|
-
for scenario in scenarios:
|
|
111
|
-
task_status = status.group(1).strip()
|
|
112
|
-
row_kind, row_exec = coverage_execution(coverage, scenario)
|
|
113
|
-
if task_status == "done" and (row_kind == "e2e" or row_exec == "deferred"):
|
|
114
|
-
allowed = {"verified", "e2e_deferred"}
|
|
115
|
-
elif task_status == "done" and row_kind == "manual":
|
|
116
|
-
# manual 人工验收延后到需求级 verify-e2e 由用户一次性确认(confirm-manual 写回 verified)
|
|
117
|
-
allowed = {"verified", "planned", "pending", "manual_pending"}
|
|
118
|
-
else:
|
|
119
|
-
allowed = {"verified"}
|
|
120
|
-
states = []
|
|
121
|
-
for body in (contract, evidence, coverage):
|
|
122
|
-
found = [line_status(line, scenario) for line in body.splitlines() if scenario_line(line, scenario)]
|
|
123
|
-
states.append(found[-1] if found else "")
|
|
124
|
-
if not all(state in allowed for state in states):
|
|
125
|
-
return f"{task_id} 的 {scenario} 状态未闭环(planned/pending/TBD/failed 或缺少 verified;仅实现阶段 E2E/显式 deferred 可 e2e_deferred、manual 可 manual_pending)"
|
|
126
|
-
return ""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def task_acceptance_failures(project_root: str, files: list) -> list:
|
|
130
|
-
"""Check new-format task files; legacy tasks without coverage are ignored."""
|
|
131
|
-
failures = []
|
|
132
|
-
for rel_path in files:
|
|
133
|
-
if not _is_active_task_file(rel_path):
|
|
134
|
-
continue
|
|
135
|
-
path = os.path.join(project_root, normalize_path(rel_path))
|
|
136
|
-
try:
|
|
137
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
138
|
-
text = f.read()
|
|
139
|
-
except FileNotFoundError:
|
|
140
|
-
# Session logs retain paths after a task has moved or been deleted.
|
|
141
|
-
continue
|
|
142
|
-
except OSError as exc:
|
|
143
|
-
cf_log.warn(f"cf_stop_hook task read failed: {exc}")
|
|
144
|
-
continue
|
|
145
|
-
if "## Acceptance Coverage" not in text:
|
|
146
|
-
continue
|
|
147
|
-
coverage = text.split("## Acceptance Coverage", 1)[1]
|
|
148
|
-
boundary = coverage.find("\n## ")
|
|
149
|
-
if boundary != -1:
|
|
150
|
-
coverage = coverage[:boundary]
|
|
151
|
-
try:
|
|
152
|
-
document = parse_task_document(text, source=rel_path)
|
|
153
|
-
except TaskDocumentError as exc:
|
|
154
|
-
if exc.code == "task_document_empty":
|
|
155
|
-
continue
|
|
156
|
-
failures.append({
|
|
157
|
-
"name": "任务段落解析",
|
|
158
|
-
"on_fail": "修复任务文件中重复或损坏的 TASK 段落后再结束",
|
|
159
|
-
"detail": str(exc),
|
|
160
|
-
})
|
|
161
|
-
continue
|
|
162
|
-
for task in document.tasks:
|
|
163
|
-
detail = _acceptance_gap(task.id, task.body, coverage)
|
|
164
|
-
if detail:
|
|
165
|
-
failures.append({
|
|
166
|
-
"name": "任务验收契约",
|
|
167
|
-
"on_fail": "补齐设计场景的测试映射与执行证据后再完成任务",
|
|
168
|
-
"detail": detail,
|
|
169
|
-
})
|
|
170
|
-
return failures
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _reason_text(failures: list, truncated: bool) -> str:
|
|
174
|
-
lines = ["收尾校验未通过(cf-stop):"]
|
|
175
|
-
for item in failures:
|
|
176
|
-
tag = "(未执行)" if item.get("incomplete") else ""
|
|
177
|
-
lines.append(f"✗ {item['name']}{tag}:{item['on_fail']}")
|
|
178
|
-
if item["detail"]:
|
|
179
|
-
lines.append(f" 输出片段: {item['detail'][:200]}")
|
|
180
|
-
if truncated:
|
|
181
|
-
lines.append("(总预算 30s 已用尽,以上为已完成部分)")
|
|
182
|
-
lines.append("请修复后再结束。")
|
|
183
|
-
return "\n".join(lines)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _active_stop_scope(root: str, sid: str, enforcement: str, deadline: float) -> tuple[list[str], dict[str, str]]:
|
|
187
|
-
try:
|
|
188
|
-
active = load_active_task(root)
|
|
189
|
-
remaining = deadline - time.monotonic()
|
|
190
|
-
done = run_done_gate(root, os.path.join(root, active.task_dir), cheap=True,
|
|
191
|
-
budget=min(GATE_BUDGET_SECONDS, max(remaining, 0.01)))
|
|
192
|
-
except (OSError, ValueError) as exc:
|
|
193
|
-
if enforcement == "required":
|
|
194
|
-
return [], {"decision": "block", "reason": f"SPEC_WORKFLOW_BLOCKED: active task is invalid: {exc}"}
|
|
195
|
-
cf_log.append_event(root, "stop_check", {"gate": "invalid_active_nonfatal", "error": str(exc)}, sid)
|
|
196
|
-
return [], {}
|
|
197
|
-
if done.decision != "pass":
|
|
198
|
-
reason = done.message or "当前 TASK required Spec verifier/Evidence 未通过;修复或重新对齐后再 Done。"
|
|
199
|
-
if any(item.get("error_code") == "verifier_budget_exhausted" for item in done.evidence):
|
|
200
|
-
reason += "(验证预算不足,部分 verifier 未运行,请拆分 TASK 或减少验证命令)"
|
|
201
|
-
if enforcement == "required":
|
|
202
|
-
return [], {"decision": "block", "reason": reason}
|
|
203
|
-
cf_log.append_event(root, "stop_check", {"gate": "blocked_nonfatal", "reason": reason}, sid)
|
|
204
|
-
return list(done.files), {}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _stop_feedback(root: str, sid: str, files: list[str], enforcement: str, deadline: float) -> dict[str, str]:
|
|
208
|
-
if not files:
|
|
209
|
-
return {}
|
|
210
|
-
failures = task_acceptance_failures(root, files)
|
|
211
|
-
truncated = False
|
|
212
|
-
config = load_validators(root)
|
|
213
|
-
if config.state == PARSE_ERROR:
|
|
214
|
-
failures.append({
|
|
215
|
-
"name": "validation.yml",
|
|
216
|
-
"on_fail": "修复 .code-flow/validation.yml 的 YAML/结构错误后再结束",
|
|
217
|
-
"detail": f"invalid_validation_config: {config.error}",
|
|
218
|
-
})
|
|
219
|
-
elif config.state == VALID:
|
|
220
|
-
filtered = config.without_heavy()
|
|
221
|
-
if filtered.validators:
|
|
222
|
-
outcome = run_validators(root, filtered, files, sid, deadline=deadline)
|
|
223
|
-
failures.extend(outcome.failures)
|
|
224
|
-
truncated = outcome.truncated
|
|
225
|
-
if not failures:
|
|
226
|
-
return {}
|
|
227
|
-
if enforcement == "warn":
|
|
228
|
-
cf_log.append_event(root, "stop_check", {"failures": [item["name"] for item in failures], "nonfatal": True}, sid)
|
|
229
|
-
return {}
|
|
230
|
-
return {"decision": "block", "reason": _reason_text(failures, truncated)}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def build_stop_result(project_root: str, sid: str) -> dict[str, str]:
|
|
234
|
-
enforcement = "required"
|
|
235
|
-
try:
|
|
236
|
-
config = load_config(project_root)
|
|
237
|
-
if not config:
|
|
238
|
-
return {}
|
|
239
|
-
enforcement = resolve_enforcement(config)
|
|
240
|
-
if enforcement == "inject":
|
|
241
|
-
return {}
|
|
242
|
-
active = os.path.exists(os.path.join(project_root, ".code-flow/.active-task.json"))
|
|
243
|
-
enabled = resolve_quality_loop(config)["stop_check"]
|
|
244
|
-
if not active and not enabled:
|
|
245
|
-
return {}
|
|
246
|
-
with stop_guard(project_root, sid):
|
|
247
|
-
return _guarded_stop_result(project_root, sid, enforcement, active, enabled)
|
|
248
|
-
except StopAlreadyRunning as exc:
|
|
249
|
-
return {"decision": "block", "reason": str(exc)}
|
|
250
|
-
except Exception as exc:
|
|
251
|
-
cf_log.warn(f"cf_stop_hook error: {exc}")
|
|
252
|
-
return {"decision": "block", "reason": f"SPEC_WORKFLOW_BLOCKED: {exc}"} if enforcement == "required" else {}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def _guarded_stop_result(root: str, sid: str, enforcement: str, active: bool, enabled: bool) -> dict[str, str]:
|
|
256
|
-
deadline = time.monotonic() + TOTAL_BUDGET_SECONDS
|
|
257
|
-
files, payload = _active_stop_scope(root, sid, enforcement, deadline) if active else ([], {})
|
|
258
|
-
if not payload and enabled:
|
|
259
|
-
if not active:
|
|
260
|
-
files = _root_scoped_files(root, session_edited_files(root, sid))
|
|
261
|
-
payload = _stop_feedback(root, sid, files, enforcement, deadline)
|
|
262
|
-
return payload
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def process_stop(data: dict[str, object], project_root: str) -> None:
|
|
266
|
-
try:
|
|
267
|
-
if data.get("stop_hook_active"):
|
|
268
|
-
return
|
|
269
|
-
payload = build_stop_result(project_root, resolve_session_id(data))
|
|
270
|
-
if payload:
|
|
271
|
-
sys.stdout.write(json.dumps(payload, ensure_ascii=False))
|
|
272
|
-
except Exception as exc:
|
|
273
|
-
cf_log.warn(f"cf_stop_hook error: {exc}")
|
|
274
|
-
sys.stdout.write(json.dumps({"decision": "block", "reason": f"SPEC_WORKFLOW_BLOCKED: {exc}"}, ensure_ascii=False))
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def _main() -> None:
|
|
278
|
-
try:
|
|
279
|
-
ensure_utf8_io()
|
|
280
|
-
if sys.stdin.isatty():
|
|
281
|
-
cf_log.warn("cf_stop_hook.py: missing stdin event")
|
|
282
|
-
return
|
|
283
|
-
raw = sys.stdin.read()
|
|
284
|
-
if raw.strip():
|
|
285
|
-
process_stop(json.loads(raw), os.getcwd())
|
|
286
|
-
except (json.JSONDecodeError, OSError, ValueError) as exc:
|
|
287
|
-
cf_log.warn(f"cf_stop_hook.py bad_stdin_json: {exc}")
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def main() -> None:
|
|
291
|
-
with execution_session():
|
|
292
|
-
_main()
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""UserPromptSubmit hook for the Context-first three-branch router."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import re
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
from codeflow.core import cf_log
|
|
13
|
-
from codeflow.acceptance.cf_checks import detect_correction
|
|
14
|
-
from codeflow.core.cf_core import (
|
|
15
|
-
ConfigError,
|
|
16
|
-
ensure_utf8_io,
|
|
17
|
-
load_config,
|
|
18
|
-
normalize_path,
|
|
19
|
-
resolve_quality_loop,
|
|
20
|
-
resolve_session_id,
|
|
21
|
-
timing_log,
|
|
22
|
-
)
|
|
23
|
-
from codeflow.core.cf_session_state import load_session_state, save_session_state
|
|
24
|
-
from codeflow.spec.cf_injection import injection_output, injection_text
|
|
25
|
-
from codeflow.spec.cf_spec_router import RouterError, RouteResult, route_prompt
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
_PATH_RE = re.compile(r'`([^`\n]+)`|@?((?:[a-zA-Z]:[\\/]|/)?[a-zA-Z0-9_.][a-zA-Z0-9_./\\\-]*\.[a-zA-Z]{1,8})(?![a-zA-Z0-9_])')
|
|
29
|
-
_EXT_RE = re.compile(r'\.(py|js|jsx|ts|tsx|vue|svelte|go|rs|java|rb|cs|cpp|c|h)$')
|
|
30
|
-
_URL_RE = re.compile(r'\b[a-zA-Z][a-zA-Z0-9+.-]*://[^\s`<>]+')
|
|
31
|
-
_HARNESS_RE = re.compile(
|
|
32
|
-
r"<(?P<tag>task-notification|output-file)\b[^>]*>.*?</(?P=tag)>",
|
|
33
|
-
re.IGNORECASE | re.DOTALL,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
_COMPRESS_INTERVAL = 25
|
|
37
|
-
_REMINDER_TEXT = (
|
|
38
|
-
"⚠ 本会话已处理较多轮次,上下文较长;批量任务建议在任务间压缩上下文,或拆到新会话继续。"
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def extract_paths_from_prompt(prompt: str, root: str | None = None) -> list[str]:
|
|
43
|
-
"""Extract explicit file paths without semantic keyword inference.
|
|
44
|
-
|
|
45
|
-
Paths outside the project are ignored: harness metadata (for example the
|
|
46
|
-
`<output-file>` in a background-agent notification) and pasted logs often
|
|
47
|
-
contain absolute paths that are not routable project files. Only in-project
|
|
48
|
-
paths participate in spec routing.
|
|
49
|
-
"""
|
|
50
|
-
paths = []
|
|
51
|
-
seen = set()
|
|
52
|
-
text = _HARNESS_RE.sub("", prompt)
|
|
53
|
-
for match in _PATH_RE.finditer(_URL_RE.sub("", text)):
|
|
54
|
-
candidate = normalize_path((match.group(1) or match.group(2)).lstrip('@'))
|
|
55
|
-
if '/' not in candidate and not _EXT_RE.search(candidate):
|
|
56
|
-
continue
|
|
57
|
-
if root is not None:
|
|
58
|
-
try:
|
|
59
|
-
target = Path(candidate) if Path(candidate).is_absolute() else Path(root) / candidate
|
|
60
|
-
candidate = target.resolve().relative_to(Path(root).resolve()).as_posix()
|
|
61
|
-
except ValueError:
|
|
62
|
-
continue
|
|
63
|
-
if candidate not in seen and ('/' in candidate or _EXT_RE.search(candidate)):
|
|
64
|
-
paths.append(candidate)
|
|
65
|
-
seen.add(candidate)
|
|
66
|
-
return paths
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _payload(text: str, mode: str) -> dict[str, object]:
|
|
70
|
-
payload: dict[str, object] = {
|
|
71
|
-
"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": text}
|
|
72
|
-
}
|
|
73
|
-
if mode == "blocked":
|
|
74
|
-
payload.update(decision="block", reason=text)
|
|
75
|
-
if os.environ.get("CF_DEBUG") == "1":
|
|
76
|
-
payload["debug"] = {"mode": mode}
|
|
77
|
-
return payload
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _record_correction(root: str, prompt: str, paths: list[str], sid: str) -> None:
|
|
81
|
-
config = load_config(root)
|
|
82
|
-
if not config or not resolve_quality_loop(config)["correction_capture"]:
|
|
83
|
-
return
|
|
84
|
-
correction = detect_correction(prompt)
|
|
85
|
-
if correction:
|
|
86
|
-
cf_log.append_event(root, "correction", {"phrase": correction["phrase"], "prompt_head": prompt[:200], "files": paths}, sid)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _session_reminder(root: str, sid: str) -> str:
|
|
90
|
-
"""One-line context-compress nudge once per COMPRESS_INTERVAL prompts per session.
|
|
91
|
-
|
|
92
|
-
Gated by quality_loop.compress_reminder; disabled → zero IO. Never raises: a
|
|
93
|
-
corrupt state file degrades to no reminder rather than breaking the hook.
|
|
94
|
-
"""
|
|
95
|
-
config = load_config(root)
|
|
96
|
-
if not config or not resolve_quality_loop(config)["compress_reminder"]:
|
|
97
|
-
return ""
|
|
98
|
-
state = load_session_state(root)
|
|
99
|
-
try:
|
|
100
|
-
if state.get("session_id") != sid:
|
|
101
|
-
state = {"session_id": sid, "prompt_count": 0, "next_remind_at": _COMPRESS_INTERVAL}
|
|
102
|
-
state["prompt_count"] = int(state.get("prompt_count", 0)) + 1
|
|
103
|
-
next_at = int(state.get("next_remind_at", _COMPRESS_INTERVAL))
|
|
104
|
-
reminder = ""
|
|
105
|
-
if state["prompt_count"] >= next_at:
|
|
106
|
-
reminder = _REMINDER_TEXT
|
|
107
|
-
state["next_remind_at"] = next_at + _COMPRESS_INTERVAL
|
|
108
|
-
except (ValueError, TypeError, KeyError):
|
|
109
|
-
return ""
|
|
110
|
-
save_session_state(root, state)
|
|
111
|
-
return reminder
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def build_prompt_context(prompt: str, sid: str, root: str) -> tuple[str, str]:
|
|
115
|
-
try:
|
|
116
|
-
return _build_prompt_context(prompt, sid, root)
|
|
117
|
-
except (ConfigError, RouterError, OSError, UnicodeError, ValueError, TypeError) as exc:
|
|
118
|
-
return f"SPEC_WORKFLOW_BLOCKED [{getattr(exc, 'code', 'invalid_prompt_context')}]: {exc}. Run cf-spec doctor before continuing.", "blocked"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _build_prompt_context(prompt: str, sid: str, root: str) -> tuple[str, str]:
|
|
122
|
-
if not prompt.strip():
|
|
123
|
-
return "", ""
|
|
124
|
-
config = load_config(root)
|
|
125
|
-
active = (Path(root) / ".code-flow/.active-task.json").exists()
|
|
126
|
-
paths = extract_paths_from_prompt(prompt, None if active else root)
|
|
127
|
-
_record_correction(root, prompt, paths, sid)
|
|
128
|
-
result = route_prompt(root, paths, sid)
|
|
129
|
-
reminder = _session_reminder(root, sid) if config and resolve_quality_loop(config)["compress_reminder"] else ""
|
|
130
|
-
text = injection_text(root, sid, result, prompt=True)
|
|
131
|
-
if text and result.mode == "catalog":
|
|
132
|
-
cf_log.append_event(root, "inject", {"specs": list(result.specs), "mode": result.mode, "source": result.mode}, sid)
|
|
133
|
-
mode = result.mode if text else "reminder"
|
|
134
|
-
return "\n\n".join(part.rstrip() for part in (text, reminder) if part), mode
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def process_prompt(data: dict[str, object], root: str) -> None:
|
|
138
|
-
try:
|
|
139
|
-
prompt = data.get("prompt", "")
|
|
140
|
-
if not isinstance(prompt, str) or not prompt.strip():
|
|
141
|
-
return
|
|
142
|
-
with injection_output():
|
|
143
|
-
text, mode = build_prompt_context(prompt, resolve_session_id(data), root)
|
|
144
|
-
if text:
|
|
145
|
-
sys.stdout.write(json.dumps(_payload(text, mode), ensure_ascii=False))
|
|
146
|
-
sys.stdout.flush()
|
|
147
|
-
except (json.JSONDecodeError, UnicodeError, OSError, ValueError) as exc:
|
|
148
|
-
cf_log.warn(f"cf_user_prompt_hook error: {exc}")
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def main() -> None:
|
|
152
|
-
try:
|
|
153
|
-
ensure_utf8_io()
|
|
154
|
-
if sys.stdin.isatty():
|
|
155
|
-
cf_log.warn("cf_user_prompt_hook.py: missing stdin event")
|
|
156
|
-
return
|
|
157
|
-
raw = sys.stdin.read()
|
|
158
|
-
if raw.strip():
|
|
159
|
-
process_prompt(json.loads(raw), os.getcwd())
|
|
160
|
-
except (json.JSONDecodeError, OSError, ValueError) as exc:
|
|
161
|
-
cf_log.warn(f"cf_user_prompt_hook.py bad_stdin_json: {exc}")
|
|
File without changes
|