arkaos 4.0.2 → 4.1.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 +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 +52 -536
- package/config/hooks/pre-tool-use.sh +46 -280
- package/config/hooks/session-start.sh +8 -10
- package/config/hooks/stop.sh +44 -424
- package/config/hooks/user-prompt-submit.sh +55 -457
- package/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/cognition/__pycache__/dreaming.cpython-313.pyc +0 -0
- package/core/cognition/dreaming.py +5 -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__/__init__.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/closing_marker_check.cpython-314.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__/constitution.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/kb_cite_check.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-314.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/__pycache__/skill_proposer.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/sycophancy_detector.cpython-314.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__/__init__.cpython-314.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__/_shared.cpython-314.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__/pre_tool_use.cpython-314.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__/stop.cpython-314.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__/__init__.cpython-314.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-314.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-314.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/memory/__pycache__/session_store.cpython-314.pyc +0 -0
- package/core/obsidian/__pycache__/relator.cpython-313.pyc +0 -0
- package/core/obsidian/relator.py +18 -8
- package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-314.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__/base.cpython-314.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__/claude_code.cpython-314.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__/codex_cli.cpython-314.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__/cursor.cpython-314.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__/gemini_cli.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-314.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_provider.cpython-314.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__/llm_retry.cpython-314.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__/native_usage.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/ollama_provider.cpython-314.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__/pricing.cpython-314.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__/registry.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/squad_orchestrator.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-314.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-314.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-314.pyc +0 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/cache.cpython-314.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__/engine.cpython-314.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__/kb_cache.cpython-314.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/__pycache__/layers.cpython-314.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-314.pyc +0 -0
- package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/engine.cpython-314.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__/flow_enforcer.cpython-314.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__/gate_checkpoint.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/loader.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/schema.cpython-314.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/__pycache__/state.cpython-314.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/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 +20 -0
- package/installer/init.js +11 -0
- package/installer/update.js +19 -0
- package/package.json +1 -1
- package/pyproject.toml +3 -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/knowledge-index.py +8 -2
- package/scripts/synapse-bridge.py +102 -26
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
"""Workflow enforcement rules registry.
|
|
1
|
+
"""Workflow enforcement rules registry — disk-verifiable rules only.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Only rules whose state CAN be read from disk (or from the operation
|
|
4
|
+
being evaluated) live here; behavioral rules (human-writing,
|
|
5
|
+
squad-routing, full-visibility, context-verification, ...) are enforced
|
|
6
|
+
by hooks/telemetry, not by this in-process registry.
|
|
7
|
+
|
|
8
|
+
History (structural honesty PR-2): this registry once narrated 14
|
|
9
|
+
NON-NEGOTIABLE rules, but every check trusted caller-supplied booleans
|
|
10
|
+
(`tests_run`, `spec_status`, `claude_md_read`, ...) — self-reported
|
|
11
|
+
state that made enforcement theater, not enforcement. Rules that could
|
|
12
|
+
not be verified from disk were deleted, not stubbed. The three rules
|
|
13
|
+
that remain read real state:
|
|
14
|
+
|
|
15
|
+
- branch-isolation: reads the current branch from git (read-only)
|
|
16
|
+
- spec-driven: checks an approved spec file exists on disk
|
|
17
|
+
- mandatory-qa: checks test evidence (coverage/junit) on disk
|
|
5
18
|
|
|
6
19
|
Each rule has:
|
|
7
|
-
- id:
|
|
8
|
-
- name:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- severity: BLOCK | ESCALATE
|
|
13
|
-
- description: What the rule enforces
|
|
20
|
+
- id: unique identifier
|
|
21
|
+
- name: human-readable name
|
|
22
|
+
- trigger_patterns: what causes a violation
|
|
23
|
+
- check_fn: function evaluating the violation against real state
|
|
24
|
+
- severity: BLOCK | ESCALATE | WARN
|
|
14
25
|
"""
|
|
15
26
|
|
|
27
|
+
import re
|
|
28
|
+
import subprocess
|
|
16
29
|
from dataclasses import dataclass
|
|
17
|
-
from
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Callable, Optional
|
|
32
|
+
|
|
33
|
+
from core.shared.test_evidence import (
|
|
34
|
+
TEST_EVIDENCE_FILES as _TEST_EVIDENCE_FILES,
|
|
35
|
+
coverage_percent_from_xml as _coverage_percent_from_xml,
|
|
36
|
+
find_test_evidence as _find_test_evidence,
|
|
37
|
+
)
|
|
18
38
|
|
|
19
39
|
|
|
20
40
|
@dataclass
|
|
@@ -31,287 +51,135 @@ class RuleDefinition:
|
|
|
31
51
|
auto_recoverable: bool = True
|
|
32
52
|
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
command = context.get("command", "")
|
|
39
|
-
|
|
40
|
-
if tool == "Bash" and "git commit" in command:
|
|
41
|
-
branch = context.get("git_branch", "")
|
|
42
|
-
if branch in ("main", "master", "dev", ""):
|
|
43
|
-
return (
|
|
44
|
-
True,
|
|
45
|
-
f"VIOLATION [branch-isolation]: Commit on {branch} while workflow active. Use feature branch.",
|
|
46
|
-
)
|
|
47
|
-
return False, ""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _check_obsidian_output(context: dict) -> tuple[bool, str]:
|
|
51
|
-
"""Check if code was modified without vault save step."""
|
|
52
|
-
tool = context.get("tool_name", "")
|
|
53
|
-
if tool in ("Write", "Edit"):
|
|
54
|
-
file_path = context.get("file_path", "")
|
|
55
|
-
if any(
|
|
56
|
-
file_path.endswith(ext) for ext in [".py", ".js", ".ts", ".vue", ".php", ".jsx", ".tsx"]
|
|
57
|
-
):
|
|
58
|
-
if not context.get("vault_saved", False):
|
|
59
|
-
return (
|
|
60
|
-
True,
|
|
61
|
-
f"VIOLATION [obsidian-output]: {file_path} modified without vault save.",
|
|
62
|
-
)
|
|
63
|
-
return False, ""
|
|
64
|
-
|
|
54
|
+
_PROTECTED_BRANCHES: tuple[str, ...] = ("main", "master", "dev")
|
|
55
|
+
_CODE_EXTENSIONS: tuple[str, ...] = (".py", ".js", ".ts", ".vue", ".php", ".jsx", ".tsx")
|
|
56
|
+
_ACTIVE_SPEC_STATUSES: tuple[str, ...] = ("approved", "in_progress", "completed")
|
|
57
|
+
_SPEC_STATUS_RE = re.compile(r"^status:\s*['\"]?(\w+)", re.MULTILINE)
|
|
65
58
|
|
|
66
|
-
def _check_authority_boundaries(context: dict) -> tuple[bool, str]:
|
|
67
|
-
"""Check if Tier 2 agent attempted privileged operation."""
|
|
68
|
-
agent_tier = context.get("agent_tier", 2)
|
|
69
|
-
tool = context.get("tool_name", "")
|
|
70
|
-
command = context.get("command", "")
|
|
71
59
|
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
def _context_cwd(context: dict) -> Path:
|
|
61
|
+
"""Working directory the operation runs in (defaults to process cwd)."""
|
|
62
|
+
return Path(context.get("cwd") or ".")
|
|
74
63
|
|
|
75
|
-
if agent_tier > 0:
|
|
76
|
-
if tool in privileged_tools:
|
|
77
|
-
return (
|
|
78
|
-
True,
|
|
79
|
-
f"VIOLATION [authority-boundaries]: Tier {agent_tier} agent attempted privileged operation: {tool}",
|
|
80
|
-
)
|
|
81
|
-
if any(cmd in command.upper() for cmd in privileged_commands):
|
|
82
|
-
return (
|
|
83
|
-
True,
|
|
84
|
-
f"VIOLATION [authority-boundaries]: Tier {agent_tier} agent attempted privileged command",
|
|
85
|
-
)
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# branch-isolation — real branch read from git, never a supplied string
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
89
68
|
|
|
90
|
-
|
|
69
|
+
def _read_current_branch(cwd: Path) -> Optional[str]:
|
|
70
|
+
"""Read the current branch from git (read-only). None when unavailable."""
|
|
71
|
+
try:
|
|
72
|
+
proc = subprocess.run(
|
|
73
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=2,
|
|
77
|
+
cwd=str(cwd),
|
|
78
|
+
)
|
|
79
|
+
except (OSError, subprocess.SubprocessError):
|
|
80
|
+
return None
|
|
81
|
+
if proc.returncode != 0:
|
|
82
|
+
return None
|
|
83
|
+
return proc.stdout.strip() or None
|
|
91
84
|
|
|
92
85
|
|
|
93
|
-
def
|
|
94
|
-
"""
|
|
86
|
+
def _check_branch_isolation(context: dict) -> tuple[bool, str]:
|
|
87
|
+
"""Block `git commit` on protected branches — branch read from disk."""
|
|
95
88
|
tool = context.get("tool_name", "")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
command = context.get("command", "")
|
|
90
|
+
if tool != "Bash" or "git commit" not in command:
|
|
91
|
+
return False, ""
|
|
92
|
+
branch = _read_current_branch(_context_cwd(context))
|
|
93
|
+
if branch in _PROTECTED_BRANCHES:
|
|
94
|
+
return (
|
|
95
|
+
True,
|
|
96
|
+
f"VIOLATION [branch-isolation]: Commit on {branch}. Use a feature branch.",
|
|
97
|
+
)
|
|
102
98
|
return False, ""
|
|
103
99
|
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if tool in ("Write", "Edit"):
|
|
109
|
-
file_path = context.get("file_path", "")
|
|
110
|
-
if any(
|
|
111
|
-
file_path.endswith(ext) for ext in [".py", ".js", ".ts", ".vue", ".php", ".jsx", ".tsx"]
|
|
112
|
-
):
|
|
113
|
-
if not context.get("claude_md_read", False):
|
|
114
|
-
return (
|
|
115
|
-
True,
|
|
116
|
-
f"VIOLATION [context-first]: {file_path} modified without reading CLAUDE.md first",
|
|
117
|
-
)
|
|
118
|
-
return False, ""
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# spec-driven — an approved spec file must exist on disk
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
119
104
|
|
|
105
|
+
def _spec_dir(context: dict) -> Path:
|
|
106
|
+
"""Resolve the spec directory for the current operation.
|
|
120
107
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
108
|
+
Path contract: approved Living Specs are YAML files saved via
|
|
109
|
+
``core.specs.manager.SpecManager.save_to_yaml`` under
|
|
110
|
+
``<project>/.arkaos/specs/``. ``context['specs_dir']`` overrides the
|
|
111
|
+
default for callers that persist specs elsewhere.
|
|
112
|
+
"""
|
|
113
|
+
override = context.get("specs_dir", "")
|
|
114
|
+
if override:
|
|
115
|
+
return Path(override)
|
|
116
|
+
return _context_cwd(context) / ".arkaos" / "specs"
|
|
125
117
|
|
|
126
|
-
code_smells = [
|
|
127
|
-
("God Class", r"class \w{50,}"),
|
|
128
|
-
("Long Method", r"def \w+\([^)]{100,}\)"),
|
|
129
|
-
("Magic Number", r"[0-9]{5,}"),
|
|
130
|
-
("Nested Ifs", r"if .+:\s+if .+:\s+if .+:"),
|
|
131
|
-
]
|
|
132
118
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
119
|
+
def _spec_file_is_active(path: Path) -> bool:
|
|
120
|
+
"""True when the spec YAML carries status approved/in_progress/completed."""
|
|
121
|
+
try:
|
|
122
|
+
text = path.read_text(encoding="utf-8")
|
|
123
|
+
except OSError:
|
|
124
|
+
return False
|
|
125
|
+
match = _SPEC_STATUS_RE.search(text)
|
|
126
|
+
return bool(match and match.group(1).lower() in _ACTIVE_SPEC_STATUSES)
|
|
136
127
|
|
|
137
|
-
if re.search(pattern, output):
|
|
138
|
-
return True, f"VIOLATION [solid-clean-code]: Code smell detected: {smell_name}"
|
|
139
128
|
|
|
140
|
-
|
|
129
|
+
def _has_approved_spec_on_disk(context: dict) -> bool:
|
|
130
|
+
spec_dir = _spec_dir(context)
|
|
131
|
+
if not spec_dir.is_dir():
|
|
132
|
+
return False
|
|
133
|
+
return any(
|
|
134
|
+
_spec_file_is_active(path)
|
|
135
|
+
for pattern in ("*.yaml", "*.yml")
|
|
136
|
+
for path in spec_dir.glob(pattern)
|
|
137
|
+
)
|
|
141
138
|
|
|
142
139
|
|
|
143
140
|
def _check_spec_driven(context: dict) -> tuple[bool, str]:
|
|
144
|
-
"""
|
|
145
|
-
tool = context.get("tool_name", "")
|
|
146
|
-
if tool in ("Write", "Edit"):
|
|
147
|
-
file_path = context.get("file_path", "")
|
|
148
|
-
if any(
|
|
149
|
-
file_path.endswith(ext) for ext in [".py", ".js", ".ts", ".vue", ".php", ".jsx", ".tsx"]
|
|
150
|
-
):
|
|
151
|
-
spec_status = context.get("spec_status", "missing")
|
|
152
|
-
if spec_status != "completed":
|
|
153
|
-
return (
|
|
154
|
-
True,
|
|
155
|
-
f"VIOLATION [spec-driven]: {file_path} modified without completed spec (status: {spec_status})",
|
|
156
|
-
)
|
|
157
|
-
return False, ""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _check_human_writing(context: dict) -> tuple[bool, str]:
|
|
161
|
-
"""Check for AI patterns in text output."""
|
|
141
|
+
"""Block code modification when no approved spec exists on disk."""
|
|
162
142
|
tool = context.get("tool_name", "")
|
|
143
|
+
file_path = context.get("file_path", "")
|
|
144
|
+
if tool not in ("Write", "Edit") or not file_path.endswith(_CODE_EXTENSIONS):
|
|
145
|
+
return False, ""
|
|
146
|
+
if _has_approved_spec_on_disk(context):
|
|
147
|
+
return False, ""
|
|
148
|
+
return (
|
|
149
|
+
True,
|
|
150
|
+
f"VIOLATION [spec-driven]: {file_path} modified without an approved "
|
|
151
|
+
f"spec on disk (looked in {_spec_dir(context)}).",
|
|
152
|
+
)
|
|
163
153
|
|
|
164
|
-
if tool in ("Write", "Edit"):
|
|
165
|
-
file_path = context.get("file_path", "")
|
|
166
|
-
if file_path.endswith((".md", ".txt", ".json")):
|
|
167
|
-
content = context.get("tool_output", "")
|
|
168
|
-
ai_patterns = [
|
|
169
|
-
"As an AI language model",
|
|
170
|
-
"I'm sorry, but I cannot",
|
|
171
|
-
"Please note that",
|
|
172
|
-
"It is important to note",
|
|
173
|
-
"Additionally,",
|
|
174
|
-
"In conclusion,",
|
|
175
|
-
"Furthermore,",
|
|
176
|
-
"Moreover,",
|
|
177
|
-
]
|
|
178
|
-
for pattern in ai_patterns:
|
|
179
|
-
if pattern.lower() in content.lower():
|
|
180
|
-
return True, f"VIOLATION [human-writing]: AI pattern detected: '{pattern}'"
|
|
181
|
-
|
|
182
|
-
return False, ""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _check_squad_routing(context: dict) -> tuple[bool, str]:
|
|
186
|
-
"""Check if non-department command was executed without routing."""
|
|
187
|
-
command = context.get("command", "")
|
|
188
|
-
user_input = context.get("user_input", "")
|
|
189
|
-
|
|
190
|
-
explicit_prefixes = {
|
|
191
|
-
"/dev",
|
|
192
|
-
"/mkt",
|
|
193
|
-
"/fin",
|
|
194
|
-
"/strat",
|
|
195
|
-
"/ops",
|
|
196
|
-
"/ecom",
|
|
197
|
-
"/kb",
|
|
198
|
-
"/brand",
|
|
199
|
-
"/saas",
|
|
200
|
-
"/landing",
|
|
201
|
-
"/content",
|
|
202
|
-
"/community",
|
|
203
|
-
"/sales",
|
|
204
|
-
"/lead",
|
|
205
|
-
"/org",
|
|
206
|
-
"/do",
|
|
207
|
-
"/arka",
|
|
208
|
-
"/qa",
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if user_input.startswith("/"):
|
|
212
|
-
prefix = user_input.split()[0] if " " in user_input else user_input
|
|
213
|
-
if prefix not in explicit_prefixes and not any(
|
|
214
|
-
user_input.startswith(p) for p in explicit_prefixes
|
|
215
|
-
):
|
|
216
|
-
return (
|
|
217
|
-
True,
|
|
218
|
-
f"VIOLATION [squad-routing]: Command '{prefix}' executed without squad routing",
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
return False, ""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _check_full_visibility(context: dict) -> tuple[bool, str]:
|
|
225
|
-
"""Check if phase announcements were made."""
|
|
226
|
-
phase = context.get("workflow_phase", "")
|
|
227
|
-
announcements = context.get("announcements_made", [])
|
|
228
|
-
|
|
229
|
-
required_announcements = ["starting", "completing"]
|
|
230
|
-
if phase and not all(ann in announcements for ann in required_announcements):
|
|
231
|
-
return True, f"VIOLATION [full-visibility]: Phase '{phase}' missing required announcements"
|
|
232
|
-
|
|
233
|
-
return False, ""
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def _check_sequential_validation(context: dict) -> tuple[bool, str]:
|
|
237
|
-
"""Check if tasks executed in correct order."""
|
|
238
|
-
current_phase = context.get("workflow_phase", "")
|
|
239
|
-
completed_phases = context.get("completed_phases", [])
|
|
240
|
-
|
|
241
|
-
phase_order = ["spec", "planning", "implementation", "review", "qa", "delivery"]
|
|
242
154
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if completed in phase_order:
|
|
247
|
-
completed_idx = phase_order.index(completed)
|
|
248
|
-
if completed_idx > current_idx:
|
|
249
|
-
return (
|
|
250
|
-
True,
|
|
251
|
-
f"VIOLATION [sequential-validation]: Phase '{current_phase}' started before '{completed}' completed",
|
|
252
|
-
)
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# mandatory-qa — test evidence must exist on disk, booleans are not trusted
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
253
158
|
|
|
254
|
-
|
|
159
|
+
# _find_test_evidence / _coverage_percent_from_xml live in
|
|
160
|
+
# core.shared.test_evidence (shared with core.governance.evidence_checks).
|
|
255
161
|
|
|
256
162
|
|
|
257
163
|
def _check_mandatory_qa(context: dict) -> tuple[bool, str]:
|
|
258
|
-
"""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if not context_injected:
|
|
278
|
-
return True, "VIOLATION [arka-supremacy]: Operation without ArkaOS context override"
|
|
279
|
-
return False, ""
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def _check_context_verification(context: dict) -> tuple[bool, str]:
|
|
283
|
-
"""Check if clarifying questions were asked before execution."""
|
|
284
|
-
user_input = context.get("user_input", "")
|
|
285
|
-
clarifying_asked = context.get("clarifying_questions_asked", False)
|
|
286
|
-
|
|
287
|
-
complex_commands = {"implement", "create", "build", "design", "architect", "develop"}
|
|
288
|
-
if any(cmd in user_input.lower() for cmd in complex_commands):
|
|
289
|
-
if not clarifying_asked:
|
|
290
|
-
return (
|
|
291
|
-
True,
|
|
292
|
-
"VIOLATION [context-verification]: Complex task executed without clarifying questions",
|
|
293
|
-
)
|
|
294
|
-
return False, ""
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def _check_forge_governance(context: dict) -> tuple[bool, str]:
|
|
298
|
-
"""Check if Forge plan violations occurred."""
|
|
299
|
-
forge_active = context.get("forge_active", False)
|
|
300
|
-
tool = context.get("tool_name", "")
|
|
301
|
-
file_path = context.get("file_path", "")
|
|
302
|
-
|
|
303
|
-
if forge_active and tool in ("Write", "Edit"):
|
|
304
|
-
forge_deliverables = context.get("forge_deliverables", [])
|
|
305
|
-
if forge_deliverables:
|
|
306
|
-
is_deliverable = any(
|
|
307
|
-
file_path.endswith(d) or d in file_path for d in forge_deliverables
|
|
308
|
-
)
|
|
309
|
-
if not is_deliverable:
|
|
310
|
-
return (
|
|
311
|
-
True,
|
|
312
|
-
f"VIOLATION [forge-governance]: {file_path} outside Forge plan deliverables",
|
|
313
|
-
)
|
|
314
|
-
|
|
164
|
+
"""Block delivery without test evidence on disk (or coverage < 80%)."""
|
|
165
|
+
if context.get("workflow_phase", "") != "delivery":
|
|
166
|
+
return False, ""
|
|
167
|
+
cwd = _context_cwd(context)
|
|
168
|
+
evidence = _find_test_evidence(cwd)
|
|
169
|
+
if evidence is None:
|
|
170
|
+
return (
|
|
171
|
+
True,
|
|
172
|
+
"VIOLATION [mandatory-qa]: no test evidence on disk "
|
|
173
|
+
"(expected coverage.xml, .coverage, junit.xml or .pytest_cache/).",
|
|
174
|
+
)
|
|
175
|
+
coverage_xml = cwd / "coverage.xml"
|
|
176
|
+
coverage = _coverage_percent_from_xml(coverage_xml) if coverage_xml.is_file() else None
|
|
177
|
+
if coverage is not None and coverage < 80:
|
|
178
|
+
return (
|
|
179
|
+
True,
|
|
180
|
+
f"VIOLATION [mandatory-qa]: coverage {coverage:.1f}% below 80% "
|
|
181
|
+
"threshold (coverage.xml).",
|
|
182
|
+
)
|
|
315
183
|
return False, ""
|
|
316
184
|
|
|
317
185
|
|
|
@@ -326,146 +194,26 @@ RULES_REGISTRY: dict[str, RuleDefinition] = {
|
|
|
326
194
|
severity="BLOCK",
|
|
327
195
|
auto_recoverable=False,
|
|
328
196
|
),
|
|
329
|
-
"obsidian-output": RuleDefinition(
|
|
330
|
-
id="obsidian-output",
|
|
331
|
-
name="Obsidian Output",
|
|
332
|
-
description="All department output is saved to the Obsidian vault",
|
|
333
|
-
trigger_patterns=["code modification without vault save"],
|
|
334
|
-
check_fn=_check_obsidian_output,
|
|
335
|
-
recovery_fn=None,
|
|
336
|
-
severity="BLOCK",
|
|
337
|
-
auto_recoverable=True,
|
|
338
|
-
),
|
|
339
|
-
"authority-boundaries": RuleDefinition(
|
|
340
|
-
id="authority-boundaries",
|
|
341
|
-
name="Authority Boundaries",
|
|
342
|
-
description="Agents operate within their tier authority",
|
|
343
|
-
trigger_patterns=["privileged operations by Tier 2+"],
|
|
344
|
-
check_fn=_check_authority_boundaries,
|
|
345
|
-
recovery_fn=None,
|
|
346
|
-
severity="ESCALATE",
|
|
347
|
-
auto_recoverable=False,
|
|
348
|
-
),
|
|
349
|
-
"security-gate": RuleDefinition(
|
|
350
|
-
id="security-gate",
|
|
351
|
-
name="Security Gate",
|
|
352
|
-
description="No code ships without OWASP security audit",
|
|
353
|
-
trigger_patterns=["delivery without security review"],
|
|
354
|
-
check_fn=_check_security_gate,
|
|
355
|
-
recovery_fn=None,
|
|
356
|
-
severity="BLOCK",
|
|
357
|
-
auto_recoverable=False,
|
|
358
|
-
),
|
|
359
|
-
"context-first": RuleDefinition(
|
|
360
|
-
id="context-first",
|
|
361
|
-
name="Context First",
|
|
362
|
-
description="Always read project context before modifying code",
|
|
363
|
-
trigger_patterns=["code modification without reading CLAUDE.md"],
|
|
364
|
-
check_fn=_check_context_first,
|
|
365
|
-
recovery_fn=None,
|
|
366
|
-
severity="BLOCK",
|
|
367
|
-
auto_recoverable=True,
|
|
368
|
-
),
|
|
369
|
-
"solid-clean-code": RuleDefinition(
|
|
370
|
-
id="solid-clean-code",
|
|
371
|
-
name="SOLID/Clean Code",
|
|
372
|
-
description="SOLID principles and Clean Code enforced on all code",
|
|
373
|
-
trigger_patterns=["code smells detected"],
|
|
374
|
-
check_fn=_check_solid_clean_code,
|
|
375
|
-
recovery_fn=None,
|
|
376
|
-
severity="WARN",
|
|
377
|
-
auto_recoverable=False,
|
|
378
|
-
),
|
|
379
197
|
"spec-driven": RuleDefinition(
|
|
380
198
|
id="spec-driven",
|
|
381
199
|
name="Spec Driven",
|
|
382
|
-
description="No code is written until
|
|
383
|
-
trigger_patterns=["code modification without
|
|
200
|
+
description="No code is written until an approved spec exists on disk",
|
|
201
|
+
trigger_patterns=["code modification without approved spec on disk"],
|
|
384
202
|
check_fn=_check_spec_driven,
|
|
385
203
|
recovery_fn=None,
|
|
386
204
|
severity="BLOCK",
|
|
387
205
|
auto_recoverable=False,
|
|
388
206
|
),
|
|
389
|
-
"human-writing": RuleDefinition(
|
|
390
|
-
id="human-writing",
|
|
391
|
-
name="Human Writing",
|
|
392
|
-
description="All text output reads as naturally human-written",
|
|
393
|
-
trigger_patterns=["AI patterns detected in text"],
|
|
394
|
-
check_fn=_check_human_writing,
|
|
395
|
-
recovery_fn=None,
|
|
396
|
-
severity="WARN",
|
|
397
|
-
auto_recoverable=True,
|
|
398
|
-
),
|
|
399
|
-
"squad-routing": RuleDefinition(
|
|
400
|
-
id="squad-routing",
|
|
401
|
-
name="Squad Routing",
|
|
402
|
-
description="Every request routes through the appropriate department squad",
|
|
403
|
-
trigger_patterns=["non-department command without routing"],
|
|
404
|
-
check_fn=_check_squad_routing,
|
|
405
|
-
recovery_fn=None,
|
|
406
|
-
severity="BLOCK",
|
|
407
|
-
auto_recoverable=False,
|
|
408
|
-
),
|
|
409
|
-
"full-visibility": RuleDefinition(
|
|
410
|
-
id="full-visibility",
|
|
411
|
-
name="Full Visibility",
|
|
412
|
-
description="Every phase announces what is starting and what resulted",
|
|
413
|
-
trigger_patterns=["phase without announcements"],
|
|
414
|
-
check_fn=_check_full_visibility,
|
|
415
|
-
recovery_fn=None,
|
|
416
|
-
severity="WARN",
|
|
417
|
-
auto_recoverable=True,
|
|
418
|
-
),
|
|
419
|
-
"sequential-validation": RuleDefinition(
|
|
420
|
-
id="sequential-validation",
|
|
421
|
-
name="Sequential Validation",
|
|
422
|
-
description="Task N+1 only starts after Task N is complete",
|
|
423
|
-
trigger_patterns=["task executed out of order"],
|
|
424
|
-
check_fn=_check_sequential_validation,
|
|
425
|
-
recovery_fn=None,
|
|
426
|
-
severity="BLOCK",
|
|
427
|
-
auto_recoverable=False,
|
|
428
|
-
),
|
|
429
207
|
"mandatory-qa": RuleDefinition(
|
|
430
208
|
id="mandatory-qa",
|
|
431
209
|
name="Mandatory QA",
|
|
432
|
-
description="
|
|
433
|
-
trigger_patterns=["delivery without
|
|
210
|
+
description="Delivery requires test evidence on disk with coverage >= 80%",
|
|
211
|
+
trigger_patterns=["delivery without test evidence or coverage < 80%"],
|
|
434
212
|
check_fn=_check_mandatory_qa,
|
|
435
213
|
recovery_fn=None,
|
|
436
214
|
severity="BLOCK",
|
|
437
215
|
auto_recoverable=False,
|
|
438
216
|
),
|
|
439
|
-
"arka-supremacy": RuleDefinition(
|
|
440
|
-
id="arka-supremacy",
|
|
441
|
-
name="ArkaOS Supremacy",
|
|
442
|
-
description="ArkaOS instructions override runtime defaults",
|
|
443
|
-
trigger_patterns=["operation without ArkaOS context"],
|
|
444
|
-
check_fn=_check_arka_supremacy,
|
|
445
|
-
recovery_fn=None,
|
|
446
|
-
severity="BLOCK",
|
|
447
|
-
auto_recoverable=True,
|
|
448
|
-
),
|
|
449
|
-
"context-verification": RuleDefinition(
|
|
450
|
-
id="context-verification",
|
|
451
|
-
name="Context Verification",
|
|
452
|
-
description="Orchestrators must confirm understanding before executing",
|
|
453
|
-
trigger_patterns=["complex task without clarifying questions"],
|
|
454
|
-
check_fn=_check_context_verification,
|
|
455
|
-
recovery_fn=None,
|
|
456
|
-
severity="BLOCK",
|
|
457
|
-
auto_recoverable=True,
|
|
458
|
-
),
|
|
459
|
-
"forge-governance": RuleDefinition(
|
|
460
|
-
id="forge-governance",
|
|
461
|
-
name="Forge Governance",
|
|
462
|
-
description="Forge plans must pass critic validation before approval",
|
|
463
|
-
trigger_patterns=["file edited outside Forge deliverables"],
|
|
464
|
-
check_fn=_check_forge_governance,
|
|
465
|
-
recovery_fn=None,
|
|
466
|
-
severity="BLOCK",
|
|
467
|
-
auto_recoverable=False,
|
|
468
|
-
),
|
|
469
217
|
}
|
|
470
218
|
|
|
471
219
|
|
|
@@ -140,6 +140,7 @@ class _Ctx:
|
|
|
140
140
|
persona: str | None = None
|
|
141
141
|
marker: str | None = None
|
|
142
142
|
config: dict = field(default_factory=dict)
|
|
143
|
+
preloaded_messages: list[str] | None = None
|
|
143
144
|
|
|
144
145
|
|
|
145
146
|
# ─── Config + Ownership loaders ────────────────────────────────────────
|
|
@@ -305,9 +306,12 @@ def _populate_context(ctx: _Ctx) -> None:
|
|
|
305
306
|
or ""
|
|
306
307
|
)
|
|
307
308
|
ctx.config = _load_ownership()
|
|
308
|
-
ctx.
|
|
309
|
-
ctx.
|
|
310
|
-
|
|
309
|
+
if ctx.preloaded_messages is not None:
|
|
310
|
+
ctx.messages = ctx.preloaded_messages[-ASSISTANT_WINDOW:]
|
|
311
|
+
else:
|
|
312
|
+
ctx.messages = _load_last_assistant_messages(
|
|
313
|
+
ctx.transcript_path, ASSISTANT_WINDOW
|
|
314
|
+
)
|
|
311
315
|
ctx.persona, ctx.marker = _resolve_persona(ctx.messages)
|
|
312
316
|
|
|
313
317
|
|
|
@@ -414,11 +418,17 @@ def evaluate(
|
|
|
414
418
|
session_id: str = "",
|
|
415
419
|
cwd: str = "",
|
|
416
420
|
tool_input: dict | None = None,
|
|
421
|
+
messages: list[str] | None = None,
|
|
417
422
|
) -> Decision:
|
|
418
|
-
"""Decide whether a Write/Edit/MultiEdit/NotebookEdit may proceed.
|
|
423
|
+
"""Decide whether a Write/Edit/MultiEdit/NotebookEdit may proceed.
|
|
424
|
+
|
|
425
|
+
``messages`` (PR-6 hook consolidation): pre-parsed assistant messages;
|
|
426
|
+
when None the transcript is read from ``transcript_path``.
|
|
427
|
+
"""
|
|
419
428
|
ctx = _Ctx(
|
|
420
429
|
tool_name=tool_name, transcript_path=transcript_path,
|
|
421
430
|
session_id=session_id, cwd=cwd, tool_input=tool_input or {},
|
|
431
|
+
preloaded_messages=messages,
|
|
422
432
|
)
|
|
423
433
|
for early_check in (_check_tool_gated, _check_feature_flag):
|
|
424
434
|
decision = early_check(ctx)
|
|
@@ -438,11 +448,17 @@ def record_telemetry(
|
|
|
438
448
|
decision: Decision,
|
|
439
449
|
cwd: str = "",
|
|
440
450
|
target_file: str = "",
|
|
451
|
+
model_requested: str = "",
|
|
441
452
|
) -> None:
|
|
442
453
|
"""Append a structured record to the specialist-dispatch telemetry log.
|
|
443
454
|
|
|
444
455
|
Drops the record silently when session_id fails the safe-id check
|
|
445
456
|
(path-traversal mitigation, CWE-22).
|
|
457
|
+
|
|
458
|
+
``model_requested`` carries the ``model`` param of the dispatched tool
|
|
459
|
+
call when the caller has one (Agent dispatches), so ``/arka costs`` can
|
|
460
|
+
later cross-check requested vs billed model tiers. Warn-only metadata —
|
|
461
|
+
empty string when the tool has no model concept (Write/Edit).
|
|
446
462
|
"""
|
|
447
463
|
safe = _safe_session_id_module.safe_session_id(session_id)
|
|
448
464
|
if safe is None:
|
|
@@ -453,6 +469,7 @@ def record_telemetry(
|
|
|
453
469
|
"tool": tool,
|
|
454
470
|
"cwd": cwd,
|
|
455
471
|
"target_file": target_file or decision.target_file or "",
|
|
472
|
+
"model_requested": model_requested,
|
|
456
473
|
**asdict(decision),
|
|
457
474
|
}
|
|
458
475
|
try:
|