@michaelxwb/code-flow 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/migrations/spec-workflow-0.6.0.md +42 -0
- package/package.json +4 -2
- package/src/adapters/claude/CLAUDE.md +11 -17
- package/src/adapters/claude/commands/cf-init.md +5 -4
- package/src/adapters/claude/commands/cf-spec.md +63 -0
- package/src/adapters/claude/commands/cf-sync.md +25 -0
- package/src/adapters/claude/commands/cf-task/align.md +23 -1
- package/src/adapters/claude/commands/cf-task/archive.md +7 -0
- package/src/adapters/claude/commands/cf-task/plan.md +16 -0
- package/src/adapters/claude/commands/cf-task/prd.md +24 -0
- package/src/adapters/claude/commands/cf-task/start.md +13 -10
- package/src/adapters/claude/settings.local.json +1 -11
- package/src/adapters/codex/AGENTS.md +11 -17
- package/src/adapters/codex/hooks.json +6 -4
- package/src/adapters/codex/skills/cf-init/SKILL.md +5 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +56 -0
- package/src/adapters/codex/skills/cf-sync/SKILL.md +30 -0
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +23 -1
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +7 -0
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +16 -0
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +24 -0
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +13 -10
- package/src/adapters/costrict/CLAUDE.md +11 -17
- package/src/adapters/costrict/commands/cf-init.md +5 -4
- package/src/adapters/costrict/commands/cf-spec.md +63 -0
- package/src/adapters/costrict/commands/cf-sync.md +25 -0
- package/src/adapters/costrict/commands/cf-task/align.md +23 -1
- package/src/adapters/costrict/commands/cf-task/archive.md +7 -0
- package/src/adapters/costrict/commands/cf-task/plan.md +16 -0
- package/src/adapters/costrict/commands/cf-task/prd.md +24 -0
- package/src/adapters/costrict/commands/cf-task/start.md +13 -10
- package/src/adapters/costrict/settings.local.json +1 -11
- package/src/adapters/opencode/AGENTS.md +11 -17
- package/src/adapters/opencode/commands/cf-init.md +4 -3
- package/src/adapters/opencode/commands/cf-spec.md +55 -0
- package/src/adapters/opencode/commands/cf-sync.md +29 -0
- package/src/adapters/opencode/commands/cf-task/align.md +24 -1
- package/src/adapters/opencode/commands/cf-task/archive.md +8 -0
- package/src/adapters/opencode/commands/cf-task/plan.md +17 -0
- package/src/adapters/opencode/commands/cf-task/prd.md +25 -0
- package/src/adapters/opencode/commands/cf-task/start.md +14 -10
- package/src/adapters/opencode/plugins/code-flow/index.js +3 -4
- package/src/cli.js +51 -2
- package/src/core/code-flow/config.yml +14 -16
- package/src/core/code-flow/scripts/cf_checks.py +30 -0
- package/src/core/code-flow/scripts/cf_core.py +36 -256
- package/src/core/code-flow/scripts/cf_log.py +8 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -9
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +88 -0
- package/src/core/code-flow/scripts/cf_session_state.py +37 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +1470 -0
- package/src/core/code-flow/scripts/cf_spec_gate.py +230 -0
- package/src/core/code-flow/scripts/cf_spec_metadata.py +304 -0
- package/src/core/code-flow/scripts/cf_spec_migrate.py +512 -0
- package/src/core/code-flow/scripts/cf_spec_resolver.py +271 -0
- package/src/core/code-flow/scripts/cf_spec_router.py +149 -0
- package/src/core/code-flow/scripts/cf_spec_session.py +118 -0
- package/src/core/code-flow/scripts/cf_spec_verify.py +340 -0
- package/src/core/code-flow/scripts/cf_stats.py +40 -5
- package/src/core/code-flow/scripts/cf_stop_hook.py +43 -2
- package/src/core/code-flow/scripts/cf_sync.py +140 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +184 -0
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +115 -345
- package/src/core/code-flow/specs/backend/code-quality-performance.md +13 -1
- package/src/core/code-flow/specs/backend/database.md +13 -1
- package/src/core/code-flow/specs/backend/directory-structure.md +13 -1
- package/src/core/code-flow/specs/backend/logging.md +13 -1
- package/src/core/code-flow/specs/backend/platform-rules.md +13 -1
- package/src/core/code-flow/specs/frontend/component-specs.md +13 -1
- package/src/core/code-flow/specs/frontend/directory-structure.md +13 -1
- package/src/core/code-flow/specs/frontend/quality-standards.md +13 -1
- package/src/core/code-flow/specs/shared/design/design-frontend.md +10 -0
- package/src/core/code-flow/specs/shared/design/design-full.md +10 -0
- package/src/core/code-flow/specs/shared/design/design-lite.md +10 -0
- package/src/core/code-flow/specs/shared/prd-template.md +14 -3
- package/src/migrate/spec-workflow.js +238 -0
- package/src/adapters/claude/commands/cf-inject.md +0 -55
- package/src/adapters/codex/skills/cf-inject/SKILL.md +0 -56
- package/src/adapters/costrict/commands/cf-inject.md +0 -55
- package/src/adapters/opencode/commands/cf-inject.md +0 -59
- package/src/core/code-flow/scripts/cf_inject_hook.py +0 -178
- package/src/core/code-flow/scripts/cf_session_hook.py +0 -42
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Active-task scope expansion and hash-bound Done verification."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
import hashlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import time
|
|
10
|
+
from typing import Mapping, Optional
|
|
11
|
+
|
|
12
|
+
from cf_spec_context import (
|
|
13
|
+
BindingInput,
|
|
14
|
+
RuleBinding,
|
|
15
|
+
RuleStageStatus,
|
|
16
|
+
SpecBinding,
|
|
17
|
+
SpecContext,
|
|
18
|
+
bind_specs,
|
|
19
|
+
current_owned_paths,
|
|
20
|
+
load_active_task,
|
|
21
|
+
load_context,
|
|
22
|
+
pause_active_task,
|
|
23
|
+
resync_active_hash,
|
|
24
|
+
save_context,
|
|
25
|
+
)
|
|
26
|
+
from cf_spec_gate import validate_stage
|
|
27
|
+
from cf_spec_metadata import load_spec_metadata
|
|
28
|
+
from cf_spec_resolver import resolve_candidates, SpecCandidate
|
|
29
|
+
from cf_spec_session import context_sha256
|
|
30
|
+
from cf_spec_verify import VerificationEvidence, VerificationScope, run_all_verifiers
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ScopeResult:
|
|
35
|
+
decision: str
|
|
36
|
+
files: tuple[str, ...]
|
|
37
|
+
new_specs: tuple[str, ...]
|
|
38
|
+
message: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class DoneResult:
|
|
43
|
+
decision: str
|
|
44
|
+
files: tuple[str, ...]
|
|
45
|
+
evidence: tuple[Mapping[str, object], ...]
|
|
46
|
+
message: str = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _context_path(task_dir: str) -> str:
|
|
50
|
+
return str(Path(task_dir) / "spec-context.yml")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _required_candidate(candidate: SpecCandidate) -> bool:
|
|
54
|
+
metadata = candidate.metadata
|
|
55
|
+
return metadata.enforcement == "required" and any(
|
|
56
|
+
rule.enforcement == "required" for rule in metadata.rules
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def evaluate_scope(root: str, task_dir: str) -> ScopeResult:
|
|
61
|
+
active = load_active_task(root)
|
|
62
|
+
files = current_owned_paths(root, active)
|
|
63
|
+
context = load_context(_context_path(task_dir))
|
|
64
|
+
candidates = resolve_candidates(root, "code", files)
|
|
65
|
+
existing = {binding.spec_id for binding in context.bindings}
|
|
66
|
+
new = tuple(
|
|
67
|
+
candidate
|
|
68
|
+
for candidate in candidates
|
|
69
|
+
if candidate.scope == "path" and candidate.spec_id not in existing
|
|
70
|
+
)
|
|
71
|
+
if not new:
|
|
72
|
+
return ScopeResult("continue", files, (), "scope unchanged")
|
|
73
|
+
selections = tuple(
|
|
74
|
+
BindingInput(candidate, "scope:path", f"Git diff matched {','.join(candidate.matched_paths)}")
|
|
75
|
+
for candidate in new
|
|
76
|
+
)
|
|
77
|
+
save_context(_context_path(task_dir), bind_specs(context, selections))
|
|
78
|
+
# The toolchain expanded the context itself; re-sync the marker so this
|
|
79
|
+
# automatic change does not surface as active_context_drift with no recovery.
|
|
80
|
+
resync_active_hash(root, task_dir, context_sha256(load_context(_context_path(task_dir))))
|
|
81
|
+
required = tuple(candidate.spec_id for candidate in new if _required_candidate(candidate))
|
|
82
|
+
if required:
|
|
83
|
+
pause_active_task(root)
|
|
84
|
+
message = "新增 required Spec 已暂停 TASK;请选择局部 Plan 或回 Align 更新设计"
|
|
85
|
+
return ScopeResult("pause", files, required, message)
|
|
86
|
+
return ScopeResult("continue", files, tuple(candidate.spec_id for candidate in new), "advisory scope expanded")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _diff_hash(root: str, files: tuple[str, ...]) -> str:
|
|
90
|
+
digest = hashlib.sha256()
|
|
91
|
+
for relative in files:
|
|
92
|
+
path = Path(root) / relative
|
|
93
|
+
digest.update(relative.encode())
|
|
94
|
+
digest.update(path.read_bytes() if path.is_file() else b"<deleted>")
|
|
95
|
+
return digest.hexdigest()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _evidence_data(evidence: VerificationEvidence) -> Mapping[str, object]:
|
|
99
|
+
return {
|
|
100
|
+
"verifier_ref": evidence.verifier_ref,
|
|
101
|
+
"executed_at": evidence.executed_at,
|
|
102
|
+
"status": evidence.status,
|
|
103
|
+
"rule_text_sha256": evidence.rule_text_sha256,
|
|
104
|
+
"artifact_sha256": evidence.artifact_sha256,
|
|
105
|
+
"diff_sha256": evidence.diff_sha256,
|
|
106
|
+
"result_sha256": evidence.result_sha256,
|
|
107
|
+
"error_code": evidence.error_code,
|
|
108
|
+
"details": evidence.details,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _update_rule(rule: RuleBinding, evidence: Mapping[str, object]) -> RuleBinding:
|
|
113
|
+
if "code" not in rule.stage_status:
|
|
114
|
+
return rule
|
|
115
|
+
statuses = dict(rule.stage_status)
|
|
116
|
+
current = statuses["code"]
|
|
117
|
+
if current.status in ("not_applicable", "waived"):
|
|
118
|
+
return rule
|
|
119
|
+
status = "verified" if evidence.get("status") == "verified" else "unverified"
|
|
120
|
+
if evidence.get("error_code") == "skipped_in_cheap_gate":
|
|
121
|
+
signature = (evidence.get("verifier_ref"), evidence.get("result_sha256"), None)
|
|
122
|
+
else:
|
|
123
|
+
signature = (evidence.get("verifier_ref"), evidence.get("result_sha256"), evidence.get("diff_sha256"))
|
|
124
|
+
if any(
|
|
125
|
+
(item.get("verifier_ref"), item.get("result_sha256"), item.get("diff_sha256")) == signature
|
|
126
|
+
for item in current.evidence
|
|
127
|
+
):
|
|
128
|
+
statuses["code"] = replace(current, status=status)
|
|
129
|
+
else:
|
|
130
|
+
statuses["code"] = replace(current, status=status, evidence=(*current.evidence, evidence))
|
|
131
|
+
return replace(rule, stage_status=statuses)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _apply_evidence(context: SpecContext, evidence: tuple[Mapping[str, object], ...]) -> SpecContext:
|
|
135
|
+
by_ref = {str(item["verifier_ref"]): item for item in evidence}
|
|
136
|
+
bindings: list[SpecBinding] = []
|
|
137
|
+
for binding in context.bindings:
|
|
138
|
+
rules = tuple(
|
|
139
|
+
_update_rule(rule, by_ref[f"{binding.spec_id}#{rule.ref}"])
|
|
140
|
+
if f"{binding.spec_id}#{rule.ref}" in by_ref else rule
|
|
141
|
+
for rule in binding.rules
|
|
142
|
+
)
|
|
143
|
+
bindings.append(replace(binding, rules=rules))
|
|
144
|
+
return replace(context, bindings=tuple(bindings))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _rule_manual_confirmation(rule: RuleBinding) -> Optional[Mapping[str, object]]:
|
|
148
|
+
status = rule.stage_status.get("code")
|
|
149
|
+
if status is None or status.decision is None or status.decision.kind != "manual_verification":
|
|
150
|
+
return None
|
|
151
|
+
decision = status.decision
|
|
152
|
+
return {
|
|
153
|
+
"reason": decision.reason,
|
|
154
|
+
"confirmed_by": decision.confirmed_by,
|
|
155
|
+
"confirmed_at": decision.confirmed_at,
|
|
156
|
+
"source": decision.source,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Optional[float] = None) -> DoneResult:
|
|
161
|
+
scope_result = evaluate_scope(root, task_dir)
|
|
162
|
+
if scope_result.decision == "pause":
|
|
163
|
+
return DoneResult("block", scope_result.files, (), scope_result.message)
|
|
164
|
+
context = load_context(_context_path(task_dir))
|
|
165
|
+
diff_hash = _diff_hash(root, scope_result.files)
|
|
166
|
+
all_evidence: list[Mapping[str, object]] = []
|
|
167
|
+
started = time.monotonic()
|
|
168
|
+
for binding in context.bindings:
|
|
169
|
+
metadata = load_spec_metadata(str(Path(root) / ".code-flow/specs" / binding.path))
|
|
170
|
+
confirmations: dict[str, Mapping[str, object]] = {}
|
|
171
|
+
for rule in binding.rules:
|
|
172
|
+
confirmation = _rule_manual_confirmation(rule)
|
|
173
|
+
if confirmation is not None:
|
|
174
|
+
confirmations[rule.ref] = confirmation
|
|
175
|
+
remaining = None if budget is None else budget - (time.monotonic() - started)
|
|
176
|
+
result = run_all_verifiers(
|
|
177
|
+
metadata, VerificationScope(root, scope_result.files, diff_hash), confirmations, cheap, remaining
|
|
178
|
+
)
|
|
179
|
+
all_evidence.extend(_evidence_data(item) for item in result.evidence)
|
|
180
|
+
updated = _apply_evidence(context, tuple(all_evidence))
|
|
181
|
+
if updated != context:
|
|
182
|
+
save_context(_context_path(task_dir), updated)
|
|
183
|
+
gate = validate_stage(updated, "code", diff_sha256=diff_hash)
|
|
184
|
+
return DoneResult(gate.decision, scope_result.files, tuple(all_evidence))
|