@michaelxwb/code-flow 0.6.5 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -42
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +52 -39
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +52 -39
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +53 -45
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lock and validate plan Acceptance Coverage entries."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from typing import IO, Mapping, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from codeflow.acceptance.cf_acceptance_manifest_core import (
|
|
16
|
+
IMMUTABLE_SCENARIO_FIELDS,
|
|
17
|
+
contract_commands as _contract_commands,
|
|
18
|
+
coverage_execution,
|
|
19
|
+
extract_manifest,
|
|
20
|
+
kind as _kind,
|
|
21
|
+
legacy_kind as _legacy_kind,
|
|
22
|
+
validate_manifest,
|
|
23
|
+
)
|
|
24
|
+
from codeflow.acceptance.cf_acceptance_schema import load_manifest, scenario_check_ids, validate_check_levels
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_RUNTIME_SCENARIO_FIELDS = ("status", "revision", "evidence", "runs", "stale_checks", "stale_reason")
|
|
28
|
+
|
|
29
|
+
_CHECK_CONFIG_FIELDS = ("env", "env_scope", "parallel_safe", "profile", "inputs", "preflight", "accepted_risk",
|
|
30
|
+
"suite", "test_ids", "markers", "collect", "level", "environment_revision", "evidence_ttl_seconds")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _manifest_source(task_file: str, output: str, scenarios_file: str) -> str:
|
|
34
|
+
if scenarios_file:
|
|
35
|
+
return scenarios_file
|
|
36
|
+
if Path(output).is_file():
|
|
37
|
+
try:
|
|
38
|
+
stored = json.loads(Path(output).read_text(encoding="utf-8"))
|
|
39
|
+
recorded = stored.get("scenarios_file") if isinstance(stored, Mapping) else None
|
|
40
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
41
|
+
sys.stderr.write(f"acceptance relock source unavailable: {exc}\n")
|
|
42
|
+
recorded = None
|
|
43
|
+
if isinstance(recorded, str) and recorded.strip():
|
|
44
|
+
return str((Path(output).parent / recorded).resolve())
|
|
45
|
+
candidate = Path(task_file).with_suffix(".scenarios.md")
|
|
46
|
+
return str(candidate) if candidate.is_file() else ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _project_source_checks(manifest: dict[str, object], scenarios_file: str) -> None:
|
|
50
|
+
if scenarios_file:
|
|
51
|
+
from codeflow.cli.cf_scenarios import parse_scenarios
|
|
52
|
+
source = parse_scenarios(scenarios_file)
|
|
53
|
+
rows = {str(item["id"]): item for item in manifest["scenarios"]}
|
|
54
|
+
check_ids = {str(item["id"]) for item in manifest.get("checks", [])}
|
|
55
|
+
for scenario in source["scenarios"]:
|
|
56
|
+
scenario_id = str(scenario["id"])
|
|
57
|
+
row = rows.get(scenario_id)
|
|
58
|
+
if row is None:
|
|
59
|
+
continue
|
|
60
|
+
checks = [str(value) for value in scenario.get("checks", [])]
|
|
61
|
+
unknown = [value for value in checks if value not in check_ids]
|
|
62
|
+
if unknown:
|
|
63
|
+
raise ValueError(f"unknown_check:{scenario_id}:{','.join(unknown)}")
|
|
64
|
+
if checks:
|
|
65
|
+
row["checks"] = checks
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def write_manifest(task_file: str, output: str, scenarios_file: str = "", *,
|
|
69
|
+
register_checks: bool = False, registration_file: str = "") -> dict[str, object]:
|
|
70
|
+
scenarios_file = _manifest_source(task_file, output, scenarios_file)
|
|
71
|
+
manifest = extract_manifest(task_file, scenarios_file=scenarios_file)
|
|
72
|
+
_project_source_checks(manifest, scenarios_file)
|
|
73
|
+
target = Path(output)
|
|
74
|
+
if scenarios_file:
|
|
75
|
+
source_path = Path(scenarios_file).resolve()
|
|
76
|
+
try:
|
|
77
|
+
manifest["scenarios_file"] = str(source_path.relative_to(target.parent.resolve()))
|
|
78
|
+
except ValueError:
|
|
79
|
+
manifest["scenarios_file"] = str(source_path)
|
|
80
|
+
# Re-lock explicitly opts into current quality; reading old plans does not.
|
|
81
|
+
manifest["e2e_gate"] = 1
|
|
82
|
+
manifest["quality_schema"] = 2
|
|
83
|
+
_preserve_check_config(target, manifest)
|
|
84
|
+
if register_checks or registration_file:
|
|
85
|
+
from codeflow.acceptance.cf_acceptance_registration import register_checks as configure
|
|
86
|
+
from codeflow.evidence.cf_evidence_store import discover_project_root
|
|
87
|
+
root = discover_project_root(task_file)
|
|
88
|
+
manifest["registration"] = configure(manifest, str(root), registration_file)
|
|
89
|
+
validate_check_levels(manifest["scenarios"], manifest["checks"])
|
|
90
|
+
_preserve_runtime_state(task_file, target, manifest)
|
|
91
|
+
for scenario in manifest.get("scenarios", []):
|
|
92
|
+
if isinstance(scenario, dict) and scenario.get("status") in ("verified", "failed") and not scenario.get("evidence"):
|
|
93
|
+
scenario["status"] = "planned"
|
|
94
|
+
try:
|
|
95
|
+
manifest["task_file"] = str(Path(task_file).resolve().relative_to(target.parent.resolve()))
|
|
96
|
+
except ValueError:
|
|
97
|
+
manifest["task_file"] = str(Path(task_file).resolve())
|
|
98
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
target.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
100
|
+
return manifest
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def record_accepted_risk(
|
|
104
|
+
manifest_file: str, check_id: str, owner: str, reason: str, expires_at: str,
|
|
105
|
+
confirmed_by: str, source: str,
|
|
106
|
+
) -> dict[str, object]:
|
|
107
|
+
"""Attach a user-confirmed, time-boxed risk acceptance to one check."""
|
|
108
|
+
if not all(str(value).strip() for value in (check_id, owner, reason, expires_at, confirmed_by, source)):
|
|
109
|
+
raise ValueError("accepted_risk requires check, owner, reason, expires_at, confirmed_by and source")
|
|
110
|
+
agent_names = {
|
|
111
|
+
"agent", "assistant", "claude", "claude code", "codex", "opencode",
|
|
112
|
+
"costrict", "ai", "bot",
|
|
113
|
+
}
|
|
114
|
+
identity = confirmed_by.strip().lower().split(":", 1)[0]
|
|
115
|
+
if identity in agent_names:
|
|
116
|
+
raise ValueError("accepted_risk must be confirmed by the user, not the agent")
|
|
117
|
+
try:
|
|
118
|
+
deadline = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
|
|
119
|
+
except ValueError as exc:
|
|
120
|
+
raise ValueError(f"invalid expires_at: {expires_at}") from exc
|
|
121
|
+
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", expires_at.strip()):
|
|
122
|
+
deadline = deadline.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
123
|
+
if deadline.tzinfo is None:
|
|
124
|
+
deadline = deadline.replace(tzinfo=timezone.utc)
|
|
125
|
+
now = datetime.now(timezone.utc)
|
|
126
|
+
if deadline <= now:
|
|
127
|
+
raise ValueError("expires_at must be in the future")
|
|
128
|
+
path = Path(manifest_file)
|
|
129
|
+
data = load_manifest(path)
|
|
130
|
+
checks = data.get("checks")
|
|
131
|
+
if not isinstance(checks, list):
|
|
132
|
+
raise ValueError("accepted_risk requires a schema3 manifest with checks")
|
|
133
|
+
for check in checks:
|
|
134
|
+
if isinstance(check, dict) and check.get("id") == check_id:
|
|
135
|
+
from codeflow.acceptance.cf_acceptance_risk import risk_scenario_ids
|
|
136
|
+
scenario_ids = risk_scenario_ids(data, check_id, owner.strip())
|
|
137
|
+
if not scenario_ids:
|
|
138
|
+
raise ValueError(f"accepted_risk owner {owner!r} owns no scenario for {check_id}")
|
|
139
|
+
check["accepted_risk"] = {
|
|
140
|
+
"owner": owner.strip(),
|
|
141
|
+
"scenario_ids": scenario_ids,
|
|
142
|
+
"reason": reason.strip(),
|
|
143
|
+
"expires_at": deadline.isoformat(),
|
|
144
|
+
"confirmed_by": confirmed_by.strip(),
|
|
145
|
+
"confirmed_at": now.isoformat(),
|
|
146
|
+
"source": source.strip(),
|
|
147
|
+
}
|
|
148
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
149
|
+
return {"ok": True, "check_id": check_id, "accepted_risk": check["accepted_risk"]}
|
|
150
|
+
raise ValueError(f"unknown check: {check_id}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _preserve_check_config(target: Path, manifest: dict[str, object]) -> None:
|
|
154
|
+
"""Re-locking the plan must not erase user-authored check environment.
|
|
155
|
+
|
|
156
|
+
Check IDs are command identities, so a stored check with the same ID still
|
|
157
|
+
describes the same execution; its env/preflight configuration remains valid.
|
|
158
|
+
"""
|
|
159
|
+
if not target.is_file():
|
|
160
|
+
return
|
|
161
|
+
try:
|
|
162
|
+
stored = {
|
|
163
|
+
item.get("id"): item
|
|
164
|
+
for item in load_manifest(target).get("checks", [])
|
|
165
|
+
if isinstance(item, dict) and isinstance(item.get("id"), str)
|
|
166
|
+
}
|
|
167
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
168
|
+
return
|
|
169
|
+
checks = manifest.get("checks")
|
|
170
|
+
if not isinstance(checks, list):
|
|
171
|
+
return
|
|
172
|
+
scenarios = manifest.get("scenarios")
|
|
173
|
+
for item in checks:
|
|
174
|
+
if not isinstance(item, dict):
|
|
175
|
+
continue
|
|
176
|
+
old = stored.get(item.get("id"))
|
|
177
|
+
if not isinstance(old, dict):
|
|
178
|
+
continue
|
|
179
|
+
derived_profile = any(
|
|
180
|
+
isinstance(row, dict)
|
|
181
|
+
and "smoke" in row
|
|
182
|
+
and item.get("id") in scenario_check_ids(row)
|
|
183
|
+
for row in (scenarios if isinstance(scenarios, list) else [])
|
|
184
|
+
)
|
|
185
|
+
for key in _CHECK_CONFIG_FIELDS:
|
|
186
|
+
if key == "profile" and derived_profile:
|
|
187
|
+
continue
|
|
188
|
+
if key in old:
|
|
189
|
+
item[key] = old[key]
|
|
190
|
+
if item.get("env_scope", "shared") != "isolated":
|
|
191
|
+
item["parallel_safe"] = False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _preserve_runtime_state(task_file: str, target: Path, manifest: dict[str, object]) -> None:
|
|
195
|
+
"""Keep history per scenario; changed contracts become visibly stale."""
|
|
196
|
+
if not target.is_file():
|
|
197
|
+
return
|
|
198
|
+
try:
|
|
199
|
+
previous = load_manifest(target)
|
|
200
|
+
except (OSError, ValueError) as exc:
|
|
201
|
+
manifest["relock"] = {"warnings": [f"previous_manifest_invalid: {exc}"]}
|
|
202
|
+
return
|
|
203
|
+
previous_task = previous.get("task_file")
|
|
204
|
+
if previous_task and (target.parent / str(previous_task)).resolve() != Path(task_file).resolve():
|
|
205
|
+
manifest["relock"] = {"warnings": ["previous_manifest_task_mismatch"]}
|
|
206
|
+
return
|
|
207
|
+
stored = {item["id"]: item for item in previous["scenarios"]}
|
|
208
|
+
current_ids = {item["id"] for item in manifest["scenarios"]}
|
|
209
|
+
report: dict[str, object] = {"preserved": [], "stale": [], "removed": sorted(set(stored) - current_ids)}
|
|
210
|
+
for item in manifest.get("scenarios", []):
|
|
211
|
+
old = stored.get(item.get("id"))
|
|
212
|
+
if not isinstance(old, dict):
|
|
213
|
+
continue
|
|
214
|
+
for key in _RUNTIME_SCENARIO_FIELDS:
|
|
215
|
+
if key in old:
|
|
216
|
+
item[key] = old[key]
|
|
217
|
+
changed = _scenario_contract(old, previous) != _scenario_contract(item, manifest)
|
|
218
|
+
if changed and any(key in old for key in ("evidence", "runs")):
|
|
219
|
+
item["status"] = "stale"
|
|
220
|
+
item["stale_checks"] = scenario_check_ids(item)
|
|
221
|
+
item["stale_reason"] = "scenario_or_check_contract_changed"
|
|
222
|
+
report["stale"].append(item["id"])
|
|
223
|
+
else:
|
|
224
|
+
report["preserved"].append(item["id"])
|
|
225
|
+
manifest["relock"] = report
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _scenario_contract(row: Mapping[str, object], manifest: Mapping[str, object]) -> dict[str, object]:
|
|
229
|
+
ids = scenario_check_ids(row)
|
|
230
|
+
checks = {item["id"]: item for item in manifest.get("checks", [])}
|
|
231
|
+
return {"scenario": {key: row.get(key) for key in IMMUTABLE_SCENARIO_FIELDS},
|
|
232
|
+
"checks": {key: checks.get(key) for key in ids}}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def record_manual_evidence(manifest_file: str, scenario_id: str, confirmed_by: str, evidence: str) -> None:
|
|
236
|
+
if not confirmed_by or confirmed_by.lower().split(":", 1)[0] in {"agent", "assistant", "codex", "claude", "opencode", "costrict"}:
|
|
237
|
+
raise ValueError("manual evidence must be confirmed by a user")
|
|
238
|
+
path = Path(manifest_file)
|
|
239
|
+
data = load_manifest(path)
|
|
240
|
+
scenarios = data.get("scenarios")
|
|
241
|
+
if not isinstance(scenarios, list):
|
|
242
|
+
raise ValueError("invalid manifest scenarios")
|
|
243
|
+
found = False
|
|
244
|
+
owner = ""
|
|
245
|
+
task_file = data.get("task_file")
|
|
246
|
+
for item in scenarios:
|
|
247
|
+
if isinstance(item, dict) and item.get("id") == scenario_id:
|
|
248
|
+
if item.get("kind") != "manual" and item.get("exec") != "manual":
|
|
249
|
+
raise ValueError("scenario is not manual")
|
|
250
|
+
item["status"] = "verified"
|
|
251
|
+
item["evidence"] = {"confirmed_by": confirmed_by, "evidence": evidence}
|
|
252
|
+
found = True
|
|
253
|
+
owner = item.get("owner") if isinstance(item.get("owner"), str) else ""
|
|
254
|
+
if not found:
|
|
255
|
+
raise ValueError("scenario not found")
|
|
256
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
257
|
+
if isinstance(task_file, str) and task_file:
|
|
258
|
+
task_path = Path(task_file)
|
|
259
|
+
if not task_path.is_absolute():
|
|
260
|
+
task_path = path.parent / task_path
|
|
261
|
+
_sync_task_evidence(task_path, scenario_id, confirmed_by, evidence, owner)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _sync_task_evidence(task_file: Path, scenario_id: str, confirmed_by: str, evidence: str, owner: str = "") -> None:
|
|
265
|
+
from codeflow.acceptance.cf_acceptance_evidence import sync_task_evidence
|
|
266
|
+
sync_task_evidence(task_file, scenario_id, confirmed_by, evidence, owner)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _parser() -> argparse.ArgumentParser:
|
|
270
|
+
parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND") or None)
|
|
271
|
+
parser.add_argument("--task-file", default="")
|
|
272
|
+
parser.add_argument("--output", default="")
|
|
273
|
+
parser.add_argument("--scenarios-file", default="")
|
|
274
|
+
parser.add_argument("--register-checks", action="store_true",
|
|
275
|
+
help="collect pytest/Playwright tests and register checks before writing")
|
|
276
|
+
parser.add_argument("--registration-file", default="",
|
|
277
|
+
help="JSON check registration overrides for custom collectors")
|
|
278
|
+
parser.add_argument("--verify-plan", action="store_true")
|
|
279
|
+
parser.add_argument("--require-scenarios", action="store_true")
|
|
280
|
+
parser.add_argument("--task-dir", default="")
|
|
281
|
+
parser.add_argument("--record-manual", action="store_true")
|
|
282
|
+
parser.add_argument("--accept-risk", action="store_true")
|
|
283
|
+
parser.add_argument("--project", action="store_true",
|
|
284
|
+
help="write manifest and project sibling scenarios checks in one step")
|
|
285
|
+
parser.add_argument("--check-id", default="")
|
|
286
|
+
parser.add_argument("--risk-owner", default="")
|
|
287
|
+
parser.add_argument("--risk-reason", default="")
|
|
288
|
+
parser.add_argument("--expires-at", default="")
|
|
289
|
+
parser.add_argument("--scenario-id", default="")
|
|
290
|
+
parser.add_argument("--confirmed-by", default="")
|
|
291
|
+
parser.add_argument("--source", default="")
|
|
292
|
+
parser.add_argument("--evidence", default="")
|
|
293
|
+
return parser
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
297
|
+
args = _parser().parse_args(argv)
|
|
298
|
+
try:
|
|
299
|
+
if (args.project or args.verify_plan) and not args.task_file:
|
|
300
|
+
raise ValueError(f"{'--project' if args.project else '--verify-plan'} requires --task-file")
|
|
301
|
+
if args.verify_plan and (args.register_checks or args.registration_file):
|
|
302
|
+
raise ValueError("--verify-plan is read-only; register checks while writing the manifest")
|
|
303
|
+
if (args.accept_risk or args.record_manual) and (args.register_checks or args.registration_file):
|
|
304
|
+
raise ValueError("check registration requires writing the manifest")
|
|
305
|
+
if args.project:
|
|
306
|
+
stdout.write(json.dumps(_write_manifest_action(args), ensure_ascii=False))
|
|
307
|
+
return 0
|
|
308
|
+
if args.accept_risk:
|
|
309
|
+
result = record_accepted_risk(
|
|
310
|
+
args.output, args.check_id, args.risk_owner, args.risk_reason,
|
|
311
|
+
args.expires_at, args.confirmed_by, args.source,
|
|
312
|
+
)
|
|
313
|
+
stdout.write(json.dumps(result, ensure_ascii=False))
|
|
314
|
+
return 0
|
|
315
|
+
if args.record_manual:
|
|
316
|
+
if not args.output or not args.scenario_id or not args.evidence:
|
|
317
|
+
raise ValueError("--record-manual requires --output, --scenario-id and --evidence")
|
|
318
|
+
record_manual_evidence(args.output, args.scenario_id, args.confirmed_by, args.evidence)
|
|
319
|
+
stdout.write(json.dumps({"ok": True, "scenario": args.scenario_id}, ensure_ascii=False))
|
|
320
|
+
return 0
|
|
321
|
+
if args.verify_plan:
|
|
322
|
+
if not args.task_dir:
|
|
323
|
+
raise ValueError("--verify-plan requires --task-dir")
|
|
324
|
+
from codeflow.acceptance.cf_plan_quality import verify_plan
|
|
325
|
+
result = verify_plan(args.task_dir, args.task_file, args.require_scenarios)
|
|
326
|
+
stdout.write(json.dumps(result, ensure_ascii=False))
|
|
327
|
+
return 0 if result["decision"] == "pass" else 3
|
|
328
|
+
stdout.write(json.dumps(_write_manifest_action(args), ensure_ascii=False))
|
|
329
|
+
return 0
|
|
330
|
+
except (OSError, ValueError) as exc:
|
|
331
|
+
stdout.write(json.dumps({"ok": False, "error": str(exc),
|
|
332
|
+
**({"registration": exc.report} if hasattr(exc, "report") else {})}, ensure_ascii=False))
|
|
333
|
+
return 2
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _write_manifest_action(args: argparse.Namespace) -> dict[str, object]:
|
|
337
|
+
output = args.output or (str(Path(args.task_file).with_name(".acceptance-manifest.json")) if args.project else "")
|
|
338
|
+
if not output:
|
|
339
|
+
raise ValueError("--output is required unless --verify-plan is used")
|
|
340
|
+
if not args.task_file:
|
|
341
|
+
raise ValueError("--task-file is required")
|
|
342
|
+
manifest = write_manifest(args.task_file, output, args.scenarios_file,
|
|
343
|
+
register_checks=args.register_checks, registration_file=args.registration_file)
|
|
344
|
+
return {"ok": True, "manifest": output, "scenarios": len(manifest["scenarios"]),
|
|
345
|
+
"checks": len(manifest["checks"]),
|
|
346
|
+
"projected_scenarios": sum(1 for row in manifest["scenarios"] if row.get("checks")),
|
|
347
|
+
"relock": manifest.get("relock", {}), "registration": manifest.get("registration", {})}
|