arkaos 4.0.1 → 4.1.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 +2 -2
- package/VERSION +1 -1
- package/arka/SKILL.md +16 -22
- package/arka/skills/flow/SKILL.md +108 -234
- package/config/claude-agents/eduardo-copy.md +53 -0
- package/config/claude-agents/francisca-tech.md +52 -0
- package/config/claude-agents/marta-cqo.md +52 -0
- package/config/claude-agents/paulo-tech-lead.md +50 -0
- package/config/cognition/schedules.yaml +15 -0
- package/config/constitution.yaml +17 -21
- package/config/hooks/_lib/workflow-classifier.sh +1 -1
- package/config/hooks/post-tool-use.sh +40 -539
- package/config/hooks/pre-tool-use.sh +32 -281
- package/config/hooks/session-start.sh +8 -10
- package/config/hooks/stop.sh +30 -425
- package/config/hooks/user-prompt-submit.sh +42 -459
- package/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/cognition/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/cognition/__pycache__/dreaming.cpython-313.pyc +0 -0
- package/core/cognition/__pycache__/retrieval.cpython-312.pyc +0 -0
- package/core/cognition/capture/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/cognition/capture/__pycache__/store.cpython-312.pyc +0 -0
- package/core/cognition/dreaming.py +5 -0
- package/core/cognition/insights/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/cognition/insights/__pycache__/store.cpython-312.pyc +0 -0
- package/core/cognition/memory/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/cognition/memory/__pycache__/obsidian.cpython-312.pyc +0 -0
- package/core/cognition/memory/__pycache__/schemas.cpython-312.pyc +0 -0
- package/core/cognition/memory/__pycache__/vector.cpython-312.pyc +0 -0
- package/core/cognition/memory/__pycache__/writer.cpython-312.pyc +0 -0
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +10 -0
- package/core/forge/__init__.py +2 -0
- package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/runtime_dispatcher.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/forge/orchestrator.py +50 -27
- package/core/forge/persistence.py +16 -0
- package/core/forge/renderer.py +16 -0
- package/core/forge/runtime_dispatcher.py +34 -19
- package/core/forge/schema.py +12 -0
- package/core/governance/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/compliance_telemetry.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/constitution.cpython-312.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/qg_verdict.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
- package/core/governance/closing_marker_check.py +9 -5
- package/core/governance/compliance_telemetry.py +1 -1
- package/core/governance/compliance_telemetry_cli.py +1 -1
- package/core/governance/evidence_checks.py +421 -0
- package/core/governance/meta_tag_check.py +36 -0
- package/core/governance/qg_verdict.py +78 -0
- package/core/governance/review_workflow.py +34 -1
- package/core/governance/skill_proposer.py +2 -1
- package/core/hooks/__init__.py +12 -0
- package/core/hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/_shared.py +110 -0
- package/core/hooks/post_tool_use.py +689 -0
- package/core/hooks/pre_tool_use.py +267 -0
- package/core/hooks/stop.py +395 -0
- package/core/hooks/user_prompt_submit.py +600 -0
- package/core/jobs/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/chunker.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/embedder.py +118 -11
- package/core/knowledge/vector_store.py +111 -14
- package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/ollama_provider.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/path_resolver.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/squad_orchestrator.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/user_paths.cpython-312.pyc +0 -0
- package/core/runtime/base.py +29 -0
- package/core/runtime/capabilities_cli.py +74 -0
- package/core/runtime/claude_code.py +8 -0
- package/core/runtime/codex_cli.py +205 -37
- package/core/runtime/cost_governor.py +184 -0
- package/core/runtime/cursor.py +8 -0
- package/core/runtime/gemini_cli.py +8 -0
- package/core/runtime/llm_provider.py +32 -13
- package/core/runtime/llm_retry.py +233 -0
- package/core/runtime/native_usage.py +186 -0
- package/core/runtime/pricing.py +9 -0
- package/core/runtime/registry.py +3 -0
- package/core/shared/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/test_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
- package/core/shared/__pycache__/test_evidence.cpython-313.pyc +0 -0
- package/core/shared/test_evidence.py +34 -0
- package/core/synapse/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/cache.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/graph_context_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +7 -0
- package/core/synapse/graph_context_layer.py +212 -0
- package/core/synapse/kb_cache.py +3 -0
- package/core/synapse/layers.py +104 -20
- package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/loader.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/schema.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/enforcer.py +5 -16
- package/core/workflow/flow_enforcer.py +36 -11
- package/core/workflow/gate_checkpoint.py +149 -0
- package/core/workflow/research_gate.py +15 -3
- package/core/workflow/rules_registry.py +137 -389
- package/core/workflow/specialist_enforcer.py +21 -4
- package/dashboard/app/pages/knowledge/index.vue +4 -1
- package/departments/brand/agents/ux-designer.yaml +1 -0
- package/departments/brand/agents/visual-designer.yaml +2 -0
- package/departments/dev/agents/frontend-dev.yaml +2 -0
- package/departments/ops/skills/n8n-flow/SKILL.md +13 -0
- package/departments/ops/skills/workflow-automate/SKILL.md +16 -0
- package/departments/quality/SKILL.md +45 -5
- package/departments/quality/agents/copy-director.yaml +3 -1
- package/departments/quality/agents/tech-director.yaml +3 -1
- package/installer/adapters/claude-code.js +46 -2
- package/installer/config-seed.js +39 -14
- package/installer/doctor.js +8 -0
- package/installer/graphify.js +153 -0
- package/installer/index.js +24 -0
- package/installer/init.js +11 -0
- package/installer/update.js +23 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/__pycache__/knowledge-index.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
- package/scripts/bench/__pycache__/__init__.cpython-313.pyc +0 -0
- package/scripts/bench/__pycache__/harness.cpython-313.pyc +0 -0
- package/scripts/bench/__pycache__/run.cpython-313.pyc +0 -0
- package/scripts/knowledge-index.py +8 -2
- package/scripts/synapse-bridge.py +102 -26
- package/scripts/tools/__pycache__/sync_wiki.cpython-313.pyc +0 -0
- package/scripts/tools/sync_wiki.py +163 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Evidence check engine — executable checks behind the Quality Gate (PR-4).
|
|
2
|
+
|
|
3
|
+
Gate 4 verdicts DERIVE from tool output, not persona narrative. This
|
|
4
|
+
module runs real, read-only checks over a project (lint, typecheck,
|
|
5
|
+
tests, coverage, security grep, spellcheck) and returns a structured
|
|
6
|
+
``EvidenceReport``. Reviewers INTERPRET the report; they cannot
|
|
7
|
+
override it — ``overall == "fail"`` forces REJECTED.
|
|
8
|
+
|
|
9
|
+
Safety contract:
|
|
10
|
+
- subprocesses run with ``cwd=project_dir``, ``capture_output=True``,
|
|
11
|
+
argument lists only (never ``shell=True`` with interpolated input)
|
|
12
|
+
- 300s cap per check; on expiry the process is killed and the check
|
|
13
|
+
reports ``ran=True, passed=None, summary="timeout"``
|
|
14
|
+
- nothing that mutates: no installs, no git, no writes to the project
|
|
15
|
+
|
|
16
|
+
CLI (for hooks/skills)::
|
|
17
|
+
|
|
18
|
+
python -m core.governance.evidence_checks <project_dir> \
|
|
19
|
+
[--checks lint,tests] [--test-command '...'] \
|
|
20
|
+
[--changed-files f1,f2] [--json]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import shlex
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
from dataclasses import asdict, dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from core.shared.test_evidence import coverage_percent_from_xml
|
|
35
|
+
|
|
36
|
+
TIMEOUT_SECONDS = 300
|
|
37
|
+
COVERAGE_THRESHOLD = 80.0
|
|
38
|
+
ALL_CHECKS: tuple[str, ...] = (
|
|
39
|
+
"lint", "typecheck", "tests", "coverage", "security-grep", "spellcheck",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_MAX_SUMMARY_CHARS = 800
|
|
43
|
+
_MAX_GREP_HITS = 20
|
|
44
|
+
|
|
45
|
+
# Obvious-classes security sweep. Line-level, over caller-supplied files.
|
|
46
|
+
_SECURITY_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
47
|
+
("aws-access-key", re.compile(r"AKIA[0-9A-Z]{16}")),
|
|
48
|
+
("api-secret-key", re.compile(r"\bsk-[A-Za-z0-9_-]{20,}")),
|
|
49
|
+
("github-token", re.compile(r"\bghp_[A-Za-z0-9]{20,}")),
|
|
50
|
+
(
|
|
51
|
+
"hardcoded-password",
|
|
52
|
+
re.compile(r"password\s*=\s*['\"][^'\"]{4,}['\"]", re.IGNORECASE),
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"eval-exec-on-input",
|
|
56
|
+
re.compile(
|
|
57
|
+
r"\b(?:eval|exec)\s*\(.*(?:input|request|argv|params|body)",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
("sql-percent-interpolation", re.compile(r"execute\s*\([^)]*['\"]\s*%")),
|
|
62
|
+
("sql-fstring-interpolation", re.compile(r"execute\s*\(\s*f['\"]")),
|
|
63
|
+
("curl-pipe-shell", re.compile(r"curl[^|\n]*\|\s*(?:ba|z)?sh\b")),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CheckResult:
|
|
69
|
+
"""Outcome of one executable check."""
|
|
70
|
+
|
|
71
|
+
check: str
|
|
72
|
+
ran: bool
|
|
73
|
+
passed: bool | None
|
|
74
|
+
command: str
|
|
75
|
+
exit_code: int | None
|
|
76
|
+
summary: str
|
|
77
|
+
details_path: str | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class EvidenceReport:
|
|
82
|
+
"""All check results plus the derived overall evidence status."""
|
|
83
|
+
|
|
84
|
+
project_dir: str
|
|
85
|
+
overall: str # "pass" | "fail" | "insufficient-evidence"
|
|
86
|
+
results: list[CheckResult] = field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict:
|
|
89
|
+
return {
|
|
90
|
+
"project_dir": self.project_dir,
|
|
91
|
+
"overall": self.overall,
|
|
92
|
+
"results": [asdict(r) for r in self.results],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ─── Subprocess plumbing ────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _tail(text: str, limit: int = _MAX_SUMMARY_CHARS) -> str:
|
|
100
|
+
text = (text or "").strip()
|
|
101
|
+
return text[-limit:] if len(text) > limit else text
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _skip(check: str, reason: str) -> CheckResult:
|
|
105
|
+
return CheckResult(
|
|
106
|
+
check=check, ran=False, passed=None, command="",
|
|
107
|
+
exit_code=None, summary=reason,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _run(
|
|
112
|
+
check: str, cmd: list[str], project_dir: Path, timeout: int,
|
|
113
|
+
) -> CheckResult:
|
|
114
|
+
"""Run one read-only project command; capture exit code + tail."""
|
|
115
|
+
command_str = " ".join(cmd)
|
|
116
|
+
try:
|
|
117
|
+
proc = subprocess.run(
|
|
118
|
+
cmd, cwd=project_dir, capture_output=True, text=True,
|
|
119
|
+
timeout=timeout,
|
|
120
|
+
)
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
return _skip(check, f"tool not found: {cmd[0]}")
|
|
123
|
+
except subprocess.TimeoutExpired:
|
|
124
|
+
# subprocess.run kills the child on expiry before raising.
|
|
125
|
+
return CheckResult(
|
|
126
|
+
check=check, ran=True, passed=None, command=command_str,
|
|
127
|
+
exit_code=None, summary="timeout",
|
|
128
|
+
)
|
|
129
|
+
output = _tail(proc.stdout.strip() or proc.stderr.strip())
|
|
130
|
+
return CheckResult(
|
|
131
|
+
check=check, ran=True, passed=proc.returncode == 0,
|
|
132
|
+
command=command_str, exit_code=proc.returncode, summary=output,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ─── Applicability detection ────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _suffixes(changed_files: list[str] | None) -> set[str]:
|
|
140
|
+
return {Path(f).suffix for f in changed_files or []}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _has_python(project_dir: Path, changed_files: list[str] | None) -> bool:
|
|
144
|
+
if ".py" in _suffixes(changed_files):
|
|
145
|
+
return True
|
|
146
|
+
if (project_dir / "pyproject.toml").is_file():
|
|
147
|
+
return True
|
|
148
|
+
return any(project_dir.glob("*.py")) or any(project_dir.glob("*/*.py"))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _package_json_script(project_dir: Path, script: str) -> bool:
|
|
152
|
+
pkg = project_dir / "package.json"
|
|
153
|
+
if not pkg.is_file():
|
|
154
|
+
return False
|
|
155
|
+
try:
|
|
156
|
+
scripts = json.loads(pkg.read_text(encoding="utf-8")).get("scripts", {})
|
|
157
|
+
except (json.JSONDecodeError, OSError):
|
|
158
|
+
return False
|
|
159
|
+
value = str(scripts.get(script, ""))
|
|
160
|
+
return bool(value) and "no test specified" not in value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _mypy_configured(project_dir: Path) -> bool:
|
|
164
|
+
if (project_dir / "mypy.ini").is_file():
|
|
165
|
+
return True
|
|
166
|
+
for name in ("pyproject.toml", "setup.cfg"):
|
|
167
|
+
path = project_dir / name
|
|
168
|
+
try:
|
|
169
|
+
text = path.read_text(encoding="utf-8") if path.is_file() else ""
|
|
170
|
+
except OSError:
|
|
171
|
+
text = ""
|
|
172
|
+
if "[tool.mypy]" in text or "[mypy]" in text:
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ─── Individual checks ──────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _check_lint(
|
|
181
|
+
project_dir: Path, changed: list[str] | None,
|
|
182
|
+
test_command: str | None, timeout: int,
|
|
183
|
+
) -> CheckResult:
|
|
184
|
+
if _has_python(project_dir, changed) and shutil.which("ruff"):
|
|
185
|
+
return _run("lint", ["ruff", "check", "."], project_dir, timeout)
|
|
186
|
+
if _package_json_script(project_dir, "lint"):
|
|
187
|
+
return _run(
|
|
188
|
+
"lint", ["npm", "run", "--silent", "lint"], project_dir, timeout,
|
|
189
|
+
)
|
|
190
|
+
pint = project_dir / "vendor" / "bin" / "pint"
|
|
191
|
+
if pint.is_file():
|
|
192
|
+
return _run("lint", [str(pint), "--test"], project_dir, timeout)
|
|
193
|
+
return _skip("lint", "no lint tooling detected (ruff/eslint/pint)")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _check_typecheck(
|
|
197
|
+
project_dir: Path, changed: list[str] | None,
|
|
198
|
+
test_command: str | None, timeout: int,
|
|
199
|
+
) -> CheckResult:
|
|
200
|
+
if _mypy_configured(project_dir) and shutil.which("mypy"):
|
|
201
|
+
return _run("typecheck", ["mypy", "."], project_dir, timeout)
|
|
202
|
+
if (project_dir / "tsconfig.json").is_file():
|
|
203
|
+
local_tsc = project_dir / "node_modules" / ".bin" / "tsc"
|
|
204
|
+
if local_tsc.is_file():
|
|
205
|
+
return _run(
|
|
206
|
+
"typecheck", [str(local_tsc), "--noEmit"], project_dir, timeout,
|
|
207
|
+
)
|
|
208
|
+
if shutil.which("tsc"):
|
|
209
|
+
return _run("typecheck", ["tsc", "--noEmit"], project_dir, timeout)
|
|
210
|
+
return _skip("typecheck", "tsconfig.json present but tsc not installed")
|
|
211
|
+
return _skip("typecheck", "no typecheck configuration detected")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _check_tests(
|
|
215
|
+
project_dir: Path, changed: list[str] | None,
|
|
216
|
+
test_command: str | None, timeout: int,
|
|
217
|
+
) -> CheckResult:
|
|
218
|
+
if test_command:
|
|
219
|
+
return _run("tests", shlex.split(test_command), project_dir, timeout)
|
|
220
|
+
if _has_python(project_dir, changed) and shutil.which("pytest"):
|
|
221
|
+
return _run("tests", ["pytest", "-q"], project_dir, timeout)
|
|
222
|
+
if _package_json_script(project_dir, "test"):
|
|
223
|
+
return _run(
|
|
224
|
+
"tests", ["npm", "test", "--silent"], project_dir, timeout,
|
|
225
|
+
)
|
|
226
|
+
return _skip("tests", "no test runner detected (pytest/npm test)")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _junit_result(junit: Path) -> CheckResult:
|
|
230
|
+
try:
|
|
231
|
+
text = junit.read_text(encoding="utf-8")
|
|
232
|
+
except OSError:
|
|
233
|
+
text = ""
|
|
234
|
+
failures = re.search(r'failures="(\d+)"', text)
|
|
235
|
+
errors = re.search(r'errors="(\d+)"', text)
|
|
236
|
+
if failures is None and errors is None:
|
|
237
|
+
return CheckResult(
|
|
238
|
+
check="coverage", ran=True, passed=None,
|
|
239
|
+
command=f"parse:{junit.name}", exit_code=None,
|
|
240
|
+
summary="junit.xml present but unparseable",
|
|
241
|
+
details_path=str(junit),
|
|
242
|
+
)
|
|
243
|
+
failed = sum(int(m.group(1)) for m in (failures, errors) if m)
|
|
244
|
+
return CheckResult(
|
|
245
|
+
check="coverage", ran=True, passed=failed == 0,
|
|
246
|
+
command=f"parse:{junit.name}", exit_code=None,
|
|
247
|
+
summary=f"junit: {failed} failures/errors", details_path=str(junit),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _check_coverage(
|
|
252
|
+
project_dir: Path, changed: list[str] | None,
|
|
253
|
+
test_command: str | None, timeout: int,
|
|
254
|
+
) -> CheckResult:
|
|
255
|
+
coverage_xml = project_dir / "coverage.xml"
|
|
256
|
+
if coverage_xml.is_file():
|
|
257
|
+
percent = coverage_percent_from_xml(coverage_xml)
|
|
258
|
+
if percent is None:
|
|
259
|
+
return CheckResult(
|
|
260
|
+
check="coverage", ran=True, passed=None,
|
|
261
|
+
command="parse:coverage.xml", exit_code=None,
|
|
262
|
+
summary="coverage.xml present but unparseable",
|
|
263
|
+
details_path=str(coverage_xml),
|
|
264
|
+
)
|
|
265
|
+
return CheckResult(
|
|
266
|
+
check="coverage", ran=True,
|
|
267
|
+
passed=percent >= COVERAGE_THRESHOLD,
|
|
268
|
+
command="parse:coverage.xml", exit_code=None,
|
|
269
|
+
summary=(
|
|
270
|
+
f"coverage {percent:.1f}% "
|
|
271
|
+
f"(threshold {COVERAGE_THRESHOLD:.0f}%)"
|
|
272
|
+
),
|
|
273
|
+
details_path=str(coverage_xml),
|
|
274
|
+
)
|
|
275
|
+
junit = project_dir / "junit.xml"
|
|
276
|
+
if junit.is_file():
|
|
277
|
+
return _junit_result(junit)
|
|
278
|
+
return _skip("coverage", "no coverage.xml or junit.xml on disk")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _grep_file(path: Path) -> list[str]:
|
|
282
|
+
try:
|
|
283
|
+
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
284
|
+
except OSError:
|
|
285
|
+
return []
|
|
286
|
+
hits = []
|
|
287
|
+
for lineno, line in enumerate(lines, start=1):
|
|
288
|
+
for name, pattern in _SECURITY_PATTERNS:
|
|
289
|
+
if pattern.search(line):
|
|
290
|
+
hits.append(f"{path}:{lineno} [{name}]")
|
|
291
|
+
return hits
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _check_security_grep(
|
|
295
|
+
project_dir: Path, changed: list[str] | None,
|
|
296
|
+
test_command: str | None, timeout: int,
|
|
297
|
+
) -> CheckResult:
|
|
298
|
+
if not changed:
|
|
299
|
+
return _skip("security-grep", "no changed files provided")
|
|
300
|
+
hits: list[str] = []
|
|
301
|
+
for name in changed:
|
|
302
|
+
path = Path(name)
|
|
303
|
+
if not path.is_absolute():
|
|
304
|
+
path = project_dir / name
|
|
305
|
+
if path.is_file():
|
|
306
|
+
hits.extend(_grep_file(path))
|
|
307
|
+
summary = (
|
|
308
|
+
"no security patterns matched"
|
|
309
|
+
if not hits
|
|
310
|
+
else "; ".join(hits[:_MAX_GREP_HITS])
|
|
311
|
+
)
|
|
312
|
+
return CheckResult(
|
|
313
|
+
check="security-grep", ran=True, passed=not hits,
|
|
314
|
+
command=f"security-grep over {len(changed)} changed file(s)",
|
|
315
|
+
exit_code=None, summary=_tail(summary),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _check_spellcheck(
|
|
320
|
+
project_dir: Path, changed: list[str] | None,
|
|
321
|
+
test_command: str | None, timeout: int,
|
|
322
|
+
) -> CheckResult:
|
|
323
|
+
if not shutil.which("codespell"):
|
|
324
|
+
return _skip("spellcheck", "codespell not installed")
|
|
325
|
+
md_files = [f for f in changed or [] if f.endswith(".md")]
|
|
326
|
+
if not md_files:
|
|
327
|
+
return _skip("spellcheck", "no changed .md files")
|
|
328
|
+
return _run("spellcheck", ["codespell", *md_files], project_dir, timeout)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
_CHECK_DISPATCH = {
|
|
332
|
+
"lint": _check_lint,
|
|
333
|
+
"typecheck": _check_typecheck,
|
|
334
|
+
"tests": _check_tests,
|
|
335
|
+
"coverage": _check_coverage,
|
|
336
|
+
"security-grep": _check_security_grep,
|
|
337
|
+
"spellcheck": _check_spellcheck,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ─── Public API ─────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _derive_overall(results: list[CheckResult]) -> str:
|
|
345
|
+
"""fail if any ran check failed; insufficient if nothing concluded."""
|
|
346
|
+
if any(r.ran and r.passed is False for r in results):
|
|
347
|
+
return "fail"
|
|
348
|
+
if any(r.ran and r.passed is True for r in results):
|
|
349
|
+
return "pass"
|
|
350
|
+
return "insufficient-evidence"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def run_evidence_checks(
|
|
354
|
+
project_dir: Path,
|
|
355
|
+
changed_files: list[str] | None = None,
|
|
356
|
+
checks: list[str] | None = None,
|
|
357
|
+
test_command: str | None = None,
|
|
358
|
+
timeout: int = TIMEOUT_SECONDS,
|
|
359
|
+
) -> EvidenceReport:
|
|
360
|
+
"""Run the selected checks and derive the overall evidence status."""
|
|
361
|
+
project_dir = Path(project_dir)
|
|
362
|
+
selected = list(checks) if checks else list(ALL_CHECKS)
|
|
363
|
+
results: list[CheckResult] = []
|
|
364
|
+
for name in selected:
|
|
365
|
+
check_fn = _CHECK_DISPATCH.get(name)
|
|
366
|
+
if check_fn is None:
|
|
367
|
+
results.append(_skip(name, f"unknown check: {name}"))
|
|
368
|
+
continue
|
|
369
|
+
results.append(check_fn(project_dir, changed_files, test_command, timeout))
|
|
370
|
+
return EvidenceReport(
|
|
371
|
+
project_dir=str(project_dir),
|
|
372
|
+
overall=_derive_overall(results),
|
|
373
|
+
results=results,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ─── CLI ────────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
381
|
+
parser = argparse.ArgumentParser(
|
|
382
|
+
prog="python -m core.governance.evidence_checks",
|
|
383
|
+
description="Run executable evidence checks for the Quality Gate.",
|
|
384
|
+
)
|
|
385
|
+
parser.add_argument("project_dir", type=Path)
|
|
386
|
+
parser.add_argument("--checks", help="comma-separated subset of checks")
|
|
387
|
+
parser.add_argument("--test-command", help="override for the tests check")
|
|
388
|
+
parser.add_argument("--changed-files", help="comma-separated changed files")
|
|
389
|
+
parser.add_argument("--json", action="store_true", help="emit JSON report")
|
|
390
|
+
return parser
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _csv(value: str | None) -> list[str] | None:
|
|
394
|
+
if not value:
|
|
395
|
+
return None
|
|
396
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def main(argv: list[str] | None = None) -> int:
|
|
400
|
+
args = _build_parser().parse_args(argv)
|
|
401
|
+
report = run_evidence_checks(
|
|
402
|
+
project_dir=args.project_dir,
|
|
403
|
+
changed_files=_csv(args.changed_files),
|
|
404
|
+
checks=_csv(args.checks),
|
|
405
|
+
test_command=args.test_command,
|
|
406
|
+
)
|
|
407
|
+
if args.json:
|
|
408
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
409
|
+
else:
|
|
410
|
+
for result in report.results:
|
|
411
|
+
state = (
|
|
412
|
+
"SKIP" if not result.ran
|
|
413
|
+
else {True: "PASS", False: "FAIL", None: "N/A"}[result.passed]
|
|
414
|
+
)
|
|
415
|
+
print(f"[{state}] {result.check}: {result.summary}")
|
|
416
|
+
print(f"overall: {report.overall}")
|
|
417
|
+
return {"pass": 0, "fail": 1}.get(report.overall, 2)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
if __name__ == "__main__":
|
|
421
|
+
raise SystemExit(main())
|
|
@@ -61,3 +61,39 @@ def _has_bypass_marker(text: str, markers: tuple[str, ...]) -> bool:
|
|
|
61
61
|
|
|
62
62
|
def _is_trivial_length(text: str) -> bool:
|
|
63
63
|
return len(text.split()) < _TRIVIAL_WORD_THRESHOLD
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ─── kb=N reconciliation (structural honesty PR-2) ──────────────────────
|
|
67
|
+
|
|
68
|
+
_KB_REPORTED_RE: re.Pattern[str] = re.compile(
|
|
69
|
+
r"\[arka:meta\][^\n]*?\bkb=(\d+)", re.IGNORECASE
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_reported_kb(response_text: str) -> int | None:
|
|
74
|
+
"""Extract the self-reported kb=N from a closing [arka:meta] line.
|
|
75
|
+
|
|
76
|
+
Returns None when the tag or the kb field is absent.
|
|
77
|
+
"""
|
|
78
|
+
match = _KB_REPORTED_RE.search(response_text or "")
|
|
79
|
+
return int(match.group(1)) if match else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def reconcile_kb_count(
|
|
83
|
+
reported: int | None, injected: int | None
|
|
84
|
+
) -> dict[str, int | bool | None]:
|
|
85
|
+
"""Reconcile the self-reported kb=N against the Synapse-injected count.
|
|
86
|
+
|
|
87
|
+
``injected`` is ground truth (written by synapse-bridge from the L2.5
|
|
88
|
+
layer result); ``reported`` is the model's claim. Warn-only contract:
|
|
89
|
+
``kb_inflated`` is True only when BOTH values are known and the claim
|
|
90
|
+
exceeds reality. Unknown on either side never flags inflation.
|
|
91
|
+
"""
|
|
92
|
+
inflated = (
|
|
93
|
+
reported is not None and injected is not None and reported > injected
|
|
94
|
+
)
|
|
95
|
+
return {
|
|
96
|
+
"kb_reported": reported,
|
|
97
|
+
"kb_injected": injected,
|
|
98
|
+
"kb_inflated": inflated,
|
|
99
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Structured Quality Gate verdict schema (PR-4 evidence Quality Gate).
|
|
2
|
+
|
|
3
|
+
``QGVerdict`` is the contract every QG reviewer subagent must return.
|
|
4
|
+
The verdict is INTERPRETATION of an ``EvidenceReport`` from
|
|
5
|
+
``core.governance.evidence_checks`` — never narrative alone:
|
|
6
|
+
|
|
7
|
+
- ``evidence_report.overall == "fail"`` forces ``verdict=REJECTED``;
|
|
8
|
+
a persona cannot override failing evidence with prose.
|
|
9
|
+
- ``APPROVED`` requires ``overall == "pass"``, or
|
|
10
|
+
``insufficient-evidence`` with an explicit justification in notes.
|
|
11
|
+
|
|
12
|
+
``QG_VERDICT_JSON_SCHEMA`` is the dict to pass as the structured-output
|
|
13
|
+
schema when dispatching reviewers via the Agent tool.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field, model_validator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QGBlocker(BaseModel):
|
|
24
|
+
"""One concrete issue that blocks approval."""
|
|
25
|
+
|
|
26
|
+
check: str = Field(description="Evidence check or rubric area that failed")
|
|
27
|
+
detail: str = Field(description="What exactly is wrong, with evidence")
|
|
28
|
+
file: str | None = Field(
|
|
29
|
+
default=None, description="File (and line if known) of the issue"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class QGEvidenceSummary(BaseModel):
|
|
34
|
+
"""Embedded summary of the EvidenceReport the reviewer interpreted."""
|
|
35
|
+
|
|
36
|
+
overall: Literal["pass", "fail", "insufficient-evidence"]
|
|
37
|
+
checks_ran: list[str] = Field(default_factory=list)
|
|
38
|
+
checks_failed: list[str] = Field(default_factory=list)
|
|
39
|
+
checks_skipped: list[str] = Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class QGVerdict(BaseModel):
|
|
43
|
+
"""Binary Quality Gate verdict derived from executable evidence."""
|
|
44
|
+
|
|
45
|
+
verdict: Literal["APPROVED", "REJECTED"]
|
|
46
|
+
evidence_report: QGEvidenceSummary
|
|
47
|
+
blockers: list[QGBlocker] = Field(default_factory=list)
|
|
48
|
+
reviewer: str = Field(description="Reviewer id, e.g. tech-director-francisca")
|
|
49
|
+
model_used: str = Field(description="Model tier the review ran on")
|
|
50
|
+
notes: str = Field(
|
|
51
|
+
default="",
|
|
52
|
+
description=(
|
|
53
|
+
"Interpretation notes; MUST justify approval explicitly when "
|
|
54
|
+
"evidence is insufficient"
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@model_validator(mode="after")
|
|
59
|
+
def enforce_evidence_floor(self) -> "QGVerdict":
|
|
60
|
+
"""APPROVED cannot coexist with failing evidence (evidence floor)."""
|
|
61
|
+
if self.verdict == "APPROVED" and self.evidence_report.overall == "fail":
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"APPROVED verdict with evidence overall='fail' — a persona "
|
|
64
|
+
"cannot override failing evidence with narrative"
|
|
65
|
+
)
|
|
66
|
+
if (
|
|
67
|
+
self.verdict == "APPROVED"
|
|
68
|
+
and self.evidence_report.overall == "insufficient-evidence"
|
|
69
|
+
and not self.notes.strip()
|
|
70
|
+
):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"APPROVED on insufficient-evidence requires explicit "
|
|
73
|
+
"justification in notes"
|
|
74
|
+
)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
QG_VERDICT_JSON_SCHEMA: dict = QGVerdict.model_json_schema()
|
|
@@ -54,6 +54,7 @@ class ReviewerVote:
|
|
|
54
54
|
comments: str = ""
|
|
55
55
|
flagged_rules: list[str] = field(default_factory=list)
|
|
56
56
|
voted_at: str = ""
|
|
57
|
+
evidence_overall: str | None = None # "pass" | "fail" | "insufficient-evidence"
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
@dataclass
|
|
@@ -209,6 +210,7 @@ class ReviewWorkflowEngine:
|
|
|
209
210
|
opinion: ReviewerOpinion,
|
|
210
211
|
comments: str = "",
|
|
211
212
|
flagged_rules: list[str] | None = None,
|
|
213
|
+
evidence_overall: str | None = None,
|
|
212
214
|
) -> bool:
|
|
213
215
|
"""Record a reviewer's vote.
|
|
214
216
|
|
|
@@ -218,9 +220,15 @@ class ReviewWorkflowEngine:
|
|
|
218
220
|
opinion: Their vote
|
|
219
221
|
comments: Optional comments
|
|
220
222
|
flagged_rules: Constitution rules that were violated
|
|
223
|
+
evidence_overall: EvidenceReport.overall the vote interprets
|
|
224
|
+
("pass" | "fail" | "insufficient-evidence")
|
|
221
225
|
|
|
222
226
|
Returns:
|
|
223
227
|
True if vote recorded successfully
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ValueError: APPROVE vote against evidence_overall == "fail" —
|
|
231
|
+
a reviewer cannot approve over failing executable evidence.
|
|
224
232
|
"""
|
|
225
233
|
workflow = self._workflows.get(workflow_id)
|
|
226
234
|
if not workflow:
|
|
@@ -229,12 +237,19 @@ class ReviewWorkflowEngine:
|
|
|
229
237
|
if workflow.phase not in (ReviewPhase.REVIEW, ReviewPhase.DELIBERATION):
|
|
230
238
|
return False
|
|
231
239
|
|
|
240
|
+
if opinion == ReviewerOpinion.APPROVE and evidence_overall == "fail":
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"reviewer {reviewer} voted APPROVE with evidence "
|
|
243
|
+
"overall='fail' — evidence floor violated"
|
|
244
|
+
)
|
|
245
|
+
|
|
232
246
|
vote = ReviewerVote(
|
|
233
247
|
reviewer=reviewer,
|
|
234
248
|
opinion=opinion,
|
|
235
249
|
comments=comments,
|
|
236
250
|
flagged_rules=flagged_rules or [],
|
|
237
251
|
voted_at=datetime.now(timezone.utc).isoformat(),
|
|
252
|
+
evidence_overall=evidence_overall,
|
|
238
253
|
)
|
|
239
254
|
workflow.votes.append(vote)
|
|
240
255
|
return True
|
|
@@ -276,17 +291,35 @@ class ReviewWorkflowEngine:
|
|
|
276
291
|
self._announce_phase_complete(ReviewPhase.DELIBERATION, result)
|
|
277
292
|
return result
|
|
278
293
|
|
|
279
|
-
def reach_verdict(
|
|
294
|
+
def reach_verdict(
|
|
295
|
+
self,
|
|
296
|
+
workflow_id: str,
|
|
297
|
+
verdict: Verdict,
|
|
298
|
+
feedback: str = "",
|
|
299
|
+
evidence_overall: str | None = None,
|
|
300
|
+
) -> PhaseResult:
|
|
280
301
|
"""Record the final verdict.
|
|
281
302
|
|
|
282
303
|
Args:
|
|
283
304
|
workflow_id: ID of the workflow
|
|
284
305
|
verdict: Final verdict
|
|
285
306
|
feedback: Feedback for the submitter
|
|
307
|
+
evidence_overall: EvidenceReport.overall the verdict derives
|
|
308
|
+
from ("pass" | "fail" | "insufficient-evidence")
|
|
286
309
|
|
|
287
310
|
Returns:
|
|
288
311
|
PhaseResult with verdict outcome
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
ValueError: APPROVED with evidence_overall == "fail" — the
|
|
315
|
+
bookkeeping enforces the evidence floor.
|
|
289
316
|
"""
|
|
317
|
+
if verdict == Verdict.APPROVED and evidence_overall == "fail":
|
|
318
|
+
raise ValueError(
|
|
319
|
+
"APPROVED verdict with evidence overall='fail' — evidence "
|
|
320
|
+
"floor violated; failing checks force REJECTED"
|
|
321
|
+
)
|
|
322
|
+
|
|
290
323
|
workflow = self._workflows.get(workflow_id)
|
|
291
324
|
if not workflow:
|
|
292
325
|
return PhaseResult(
|
|
@@ -15,7 +15,8 @@ from datetime import datetime, timezone
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
_COMPLETION_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
18
|
-
re.compile(r"\[arka:
|
|
18
|
+
re.compile(r"\[arka:gate:4\]", re.IGNORECASE),
|
|
19
|
+
re.compile(r"\[arka:phase:13\]", re.IGNORECASE), # legacy, v4.1 window
|
|
19
20
|
re.compile(r"\barc complete\b", re.IGNORECASE),
|
|
20
21
|
)
|
|
21
22
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Consolidated hook entrypoints (PR-6 v4.1.0 — hook hygiene).
|
|
2
|
+
|
|
3
|
+
One python process per hook event. Each module exposes
|
|
4
|
+
``main(stdin_json) -> int`` and is invoked by the thin bash wrappers in
|
|
5
|
+
``config/hooks/*.sh`` as ``python3 -m core.hooks.<event>``.
|
|
6
|
+
|
|
7
|
+
CRITICAL degradation contract: ambient python3 lacks PyYAML on some
|
|
8
|
+
machines. This package and every entrypoint import ONLY stdlib at module
|
|
9
|
+
level; anything that can pull yaml (core.workflow.*, core.governance.*,
|
|
10
|
+
core.synapse engine, ...) is imported lazily inside try/except so
|
|
11
|
+
stdlib-only paths (budget check, gotchas, nudges) still run.
|
|
12
|
+
"""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|