arkaos 4.0.2 → 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.
Files changed (167) hide show
  1. package/README.md +2 -2
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +16 -22
  4. package/arka/skills/flow/SKILL.md +108 -234
  5. package/config/claude-agents/eduardo-copy.md +53 -0
  6. package/config/claude-agents/francisca-tech.md +52 -0
  7. package/config/claude-agents/marta-cqo.md +52 -0
  8. package/config/claude-agents/paulo-tech-lead.md +50 -0
  9. package/config/cognition/schedules.yaml +15 -0
  10. package/config/constitution.yaml +17 -21
  11. package/config/hooks/_lib/workflow-classifier.sh +1 -1
  12. package/config/hooks/post-tool-use.sh +40 -539
  13. package/config/hooks/pre-tool-use.sh +32 -281
  14. package/config/hooks/session-start.sh +8 -10
  15. package/config/hooks/stop.sh +30 -425
  16. package/config/hooks/user-prompt-submit.sh +42 -459
  17. package/core/cognition/__pycache__/dreaming.cpython-313.pyc +0 -0
  18. package/core/cognition/dreaming.py +5 -0
  19. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  20. package/core/cognition/scheduler/daemon.py +10 -0
  21. package/core/forge/__init__.py +2 -0
  22. package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
  23. package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
  24. package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
  25. package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
  26. package/core/forge/__pycache__/runtime_dispatcher.cpython-313.pyc +0 -0
  27. package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
  28. package/core/forge/orchestrator.py +50 -27
  29. package/core/forge/persistence.py +16 -0
  30. package/core/forge/renderer.py +16 -0
  31. package/core/forge/runtime_dispatcher.py +34 -19
  32. package/core/forge/schema.py +12 -0
  33. package/core/governance/__pycache__/__init__.cpython-312.pyc +0 -0
  34. package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
  35. package/core/governance/__pycache__/compliance_telemetry.cpython-313.pyc +0 -0
  36. package/core/governance/__pycache__/constitution.cpython-312.pyc +0 -0
  37. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  38. package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
  39. package/core/governance/__pycache__/qg_verdict.cpython-313.pyc +0 -0
  40. package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
  41. package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
  42. package/core/governance/closing_marker_check.py +9 -5
  43. package/core/governance/compliance_telemetry.py +1 -1
  44. package/core/governance/compliance_telemetry_cli.py +1 -1
  45. package/core/governance/evidence_checks.py +421 -0
  46. package/core/governance/meta_tag_check.py +36 -0
  47. package/core/governance/qg_verdict.py +78 -0
  48. package/core/governance/review_workflow.py +34 -1
  49. package/core/governance/skill_proposer.py +2 -1
  50. package/core/hooks/__init__.py +12 -0
  51. package/core/hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  52. package/core/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
  54. package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
  55. package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
  56. package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
  57. package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
  58. package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
  59. package/core/hooks/__pycache__/stop.cpython-312.pyc +0 -0
  60. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  61. package/core/hooks/__pycache__/user_prompt_submit.cpython-312.pyc +0 -0
  62. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  63. package/core/hooks/_shared.py +110 -0
  64. package/core/hooks/post_tool_use.py +689 -0
  65. package/core/hooks/pre_tool_use.py +267 -0
  66. package/core/hooks/stop.py +395 -0
  67. package/core/hooks/user_prompt_submit.py +600 -0
  68. package/core/jobs/__pycache__/__init__.cpython-312.pyc +0 -0
  69. package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
  70. package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
  71. package/core/knowledge/__pycache__/embedder.cpython-312.pyc +0 -0
  72. package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
  73. package/core/knowledge/__pycache__/vector_store.cpython-312.pyc +0 -0
  74. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  75. package/core/knowledge/embedder.py +118 -11
  76. package/core/knowledge/vector_store.py +111 -14
  77. package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  78. package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  79. package/core/runtime/__pycache__/base.cpython-312.pyc +0 -0
  80. package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
  81. package/core/runtime/__pycache__/capabilities_cli.cpython-312.pyc +0 -0
  82. package/core/runtime/__pycache__/capabilities_cli.cpython-313.pyc +0 -0
  83. package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
  84. package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
  85. package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
  86. package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
  87. package/core/runtime/__pycache__/cost_governor.cpython-312.pyc +0 -0
  88. package/core/runtime/__pycache__/cost_governor.cpython-313.pyc +0 -0
  89. package/core/runtime/__pycache__/cursor.cpython-312.pyc +0 -0
  90. package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
  91. package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
  92. package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
  93. package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
  94. package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
  95. package/core/runtime/__pycache__/llm_retry.cpython-312.pyc +0 -0
  96. package/core/runtime/__pycache__/llm_retry.cpython-313.pyc +0 -0
  97. package/core/runtime/__pycache__/native_usage.cpython-312.pyc +0 -0
  98. package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
  99. package/core/runtime/__pycache__/path_resolver.cpython-312.pyc +0 -0
  100. package/core/runtime/__pycache__/pricing.cpython-312.pyc +0 -0
  101. package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
  102. package/core/runtime/__pycache__/registry.cpython-312.pyc +0 -0
  103. package/core/runtime/__pycache__/registry.cpython-313.pyc +0 -0
  104. package/core/runtime/__pycache__/squad_orchestrator.cpython-312.pyc +0 -0
  105. package/core/runtime/base.py +29 -0
  106. package/core/runtime/capabilities_cli.py +74 -0
  107. package/core/runtime/claude_code.py +8 -0
  108. package/core/runtime/codex_cli.py +205 -37
  109. package/core/runtime/cost_governor.py +184 -0
  110. package/core/runtime/cursor.py +8 -0
  111. package/core/runtime/gemini_cli.py +8 -0
  112. package/core/runtime/llm_provider.py +32 -13
  113. package/core/runtime/llm_retry.py +233 -0
  114. package/core/runtime/native_usage.py +186 -0
  115. package/core/runtime/pricing.py +9 -0
  116. package/core/runtime/registry.py +3 -0
  117. package/core/shared/__pycache__/test_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
  118. package/core/shared/__pycache__/test_evidence.cpython-313.pyc +0 -0
  119. package/core/shared/test_evidence.py +34 -0
  120. package/core/synapse/__pycache__/agent_experiences_layer.cpython-312.pyc +0 -0
  121. package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
  122. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  123. package/core/synapse/__pycache__/graph_context_layer.cpython-313.pyc +0 -0
  124. package/core/synapse/__pycache__/kb_cache.cpython-312.pyc +0 -0
  125. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  126. package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
  127. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  128. package/core/synapse/engine.py +7 -0
  129. package/core/synapse/graph_context_layer.py +212 -0
  130. package/core/synapse/kb_cache.py +3 -0
  131. package/core/synapse/layers.py +104 -20
  132. package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
  133. package/core/workflow/__pycache__/flow_enforcer.cpython-312.pyc +0 -0
  134. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  135. package/core/workflow/__pycache__/gate_checkpoint.cpython-312.pyc +0 -0
  136. package/core/workflow/__pycache__/gate_checkpoint.cpython-313.pyc +0 -0
  137. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  138. package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
  139. package/core/workflow/__pycache__/specialist_enforcer.cpython-312.pyc +0 -0
  140. package/core/workflow/__pycache__/specialist_enforcer.cpython-313.pyc +0 -0
  141. package/core/workflow/enforcer.py +5 -16
  142. package/core/workflow/flow_enforcer.py +36 -11
  143. package/core/workflow/gate_checkpoint.py +149 -0
  144. package/core/workflow/research_gate.py +15 -3
  145. package/core/workflow/rules_registry.py +137 -389
  146. package/core/workflow/specialist_enforcer.py +21 -4
  147. package/dashboard/app/pages/knowledge/index.vue +4 -1
  148. package/departments/ops/skills/n8n-flow/SKILL.md +13 -0
  149. package/departments/ops/skills/workflow-automate/SKILL.md +16 -0
  150. package/departments/quality/SKILL.md +45 -5
  151. package/departments/quality/agents/copy-director.yaml +3 -1
  152. package/departments/quality/agents/tech-director.yaml +3 -1
  153. package/installer/adapters/claude-code.js +46 -2
  154. package/installer/config-seed.js +39 -14
  155. package/installer/doctor.js +8 -0
  156. package/installer/graphify.js +153 -0
  157. package/installer/index.js +20 -0
  158. package/installer/init.js +11 -0
  159. package/installer/update.js +19 -0
  160. package/package.json +1 -1
  161. package/pyproject.toml +1 -1
  162. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  163. package/scripts/__pycache__/knowledge-index.cpython-313.pyc +0 -0
  164. package/scripts/__pycache__/synapse-bridge.cpython-312.pyc +0 -0
  165. package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
  166. package/scripts/knowledge-index.py +8 -2
  167. 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
- Defines all 14 NON-NEGOTIABLE Constitution rules with their
4
- violation triggers, auto-recovery suggestions, and severity classification.
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: Unique identifier
8
- - name: Human-readable name
9
- - trigger: What causes a violation
10
- - check: Function to evaluate violation
11
- - recovery: Function to attempt auto-fix
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 typing import Callable, Optional, Any
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
- def _check_branch_isolation(context: dict) -> tuple[bool, str]:
35
- """Check if commit was made on protected branch while workflow active."""
36
- tool = context.get("tool_name", "")
37
- output = context.get("tool_output", "")
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
- privileged_tools = {"sudo", "chmod", "chown", "delete_branch", "force_push"}
73
- privileged_commands = {"DROP TABLE", "TRUNCATE", "rm -rf /", "git push --force"}
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
- if agent_tier > 0 and "Bash" in tool and ("sudo" in command or "chmod 777" in command):
88
- return True, f"VIOLATION [authority-boundaries]: Privileged shell command attempted"
65
+ # ---------------------------------------------------------------------------
66
+ # branch-isolation real branch read from git, never a supplied string
67
+ # ---------------------------------------------------------------------------
89
68
 
90
- return False, ""
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 _check_security_gate(context: dict) -> tuple[bool, str]:
94
- """Check if code shipped without security audit."""
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
- output = context.get("tool_output", "")
97
- phase = context.get("workflow_phase", "")
98
-
99
- if phase == "delivery" and tool in ("Write", "Edit"):
100
- if not context.get("security_reviewed", False):
101
- return True, "VIOLATION [security-gate]: Code shipping without OWASP security audit"
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
- def _check_context_first(context: dict) -> tuple[bool, str]:
106
- """Check if project context was read before code modification."""
107
- tool = context.get("tool_name", "")
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
- def _check_solid_clean_code(context: dict) -> tuple[bool, str]:
122
- """Check for SOLID/Clean Code violations."""
123
- tool = context.get("tool_name", "")
124
- output = context.get("tool_output", "")
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
- if tool in ("Write", "Edit"):
134
- for smell_name, pattern in code_smells:
135
- import re
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
- return False, ""
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
- """Check if code modified without completed spec."""
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
- if current_phase:
244
- current_idx = phase_order.index(current_phase) if current_phase in phase_order else -1
245
- for completed in completed_phases:
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
- return False, ""
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
- """Check if full test suite was run before delivery."""
259
- phase = context.get("workflow_phase", "")
260
- if phase == "delivery":
261
- if not context.get("tests_run", False):
262
- return (
263
- True,
264
- "VIOLATION [mandatory-qa]: Delivery attempted without running full test suite",
265
- )
266
- if context.get("test_coverage", 0) < 80:
267
- return (
268
- True,
269
- f"VIOLATION [mandatory-qa]: Test coverage {context.get('test_coverage')}% below 80% threshold",
270
- )
271
- return False, ""
272
-
273
-
274
- def _check_arka_supremacy(context: dict) -> tuple[bool, str]:
275
- """Check if ArkaOS context was used over runtime defaults."""
276
- context_injected = context.get("arkos_context_injected", True)
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 a detailed spec exists",
383
- trigger_patterns=["code modification without completed spec"],
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="QA runs ALL tests every time",
433
- trigger_patterns=["delivery without tests or coverage < 80%"],
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.messages = _load_last_assistant_messages(
309
- ctx.transcript_path, ASSISTANT_WINDOW
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: