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.
Files changed (214) 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 +52 -536
  13. package/config/hooks/pre-tool-use.sh +46 -280
  14. package/config/hooks/session-start.sh +8 -10
  15. package/config/hooks/stop.sh +44 -424
  16. package/config/hooks/user-prompt-submit.sh +55 -457
  17. package/core/__pycache__/__init__.cpython-314.pyc +0 -0
  18. package/core/cognition/__pycache__/dreaming.cpython-313.pyc +0 -0
  19. package/core/cognition/dreaming.py +5 -0
  20. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  21. package/core/cognition/scheduler/daemon.py +10 -0
  22. package/core/forge/__init__.py +2 -0
  23. package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
  25. package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
  26. package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
  27. package/core/forge/__pycache__/runtime_dispatcher.cpython-313.pyc +0 -0
  28. package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
  29. package/core/forge/orchestrator.py +50 -27
  30. package/core/forge/persistence.py +16 -0
  31. package/core/forge/renderer.py +16 -0
  32. package/core/forge/runtime_dispatcher.py +34 -19
  33. package/core/forge/schema.py +12 -0
  34. package/core/governance/__pycache__/__init__.cpython-312.pyc +0 -0
  35. package/core/governance/__pycache__/__init__.cpython-314.pyc +0 -0
  36. package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
  37. package/core/governance/__pycache__/closing_marker_check.cpython-314.pyc +0 -0
  38. package/core/governance/__pycache__/compliance_telemetry.cpython-313.pyc +0 -0
  39. package/core/governance/__pycache__/constitution.cpython-312.pyc +0 -0
  40. package/core/governance/__pycache__/constitution.cpython-314.pyc +0 -0
  41. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  42. package/core/governance/__pycache__/kb_cite_check.cpython-314.pyc +0 -0
  43. package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
  44. package/core/governance/__pycache__/meta_tag_check.cpython-314.pyc +0 -0
  45. package/core/governance/__pycache__/qg_verdict.cpython-313.pyc +0 -0
  46. package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
  47. package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
  48. package/core/governance/__pycache__/skill_proposer.cpython-314.pyc +0 -0
  49. package/core/governance/__pycache__/sycophancy_detector.cpython-314.pyc +0 -0
  50. package/core/governance/closing_marker_check.py +9 -5
  51. package/core/governance/compliance_telemetry.py +1 -1
  52. package/core/governance/compliance_telemetry_cli.py +1 -1
  53. package/core/governance/evidence_checks.py +421 -0
  54. package/core/governance/meta_tag_check.py +36 -0
  55. package/core/governance/qg_verdict.py +78 -0
  56. package/core/governance/review_workflow.py +34 -1
  57. package/core/governance/skill_proposer.py +2 -1
  58. package/core/hooks/__init__.py +12 -0
  59. package/core/hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  60. package/core/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  61. package/core/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  62. package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
  63. package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
  64. package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
  65. package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
  66. package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
  67. package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
  68. package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
  69. package/core/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  70. package/core/hooks/__pycache__/stop.cpython-312.pyc +0 -0
  71. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  72. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  73. package/core/hooks/__pycache__/user_prompt_submit.cpython-312.pyc +0 -0
  74. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  75. package/core/hooks/_shared.py +110 -0
  76. package/core/hooks/post_tool_use.py +689 -0
  77. package/core/hooks/pre_tool_use.py +267 -0
  78. package/core/hooks/stop.py +395 -0
  79. package/core/hooks/user_prompt_submit.py +600 -0
  80. package/core/jobs/__pycache__/__init__.cpython-312.pyc +0 -0
  81. package/core/jobs/__pycache__/__init__.cpython-314.pyc +0 -0
  82. package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
  83. package/core/jobs/__pycache__/auto_doc_worker.cpython-314.pyc +0 -0
  84. package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
  85. package/core/jobs/__pycache__/manager.cpython-314.pyc +0 -0
  86. package/core/knowledge/__pycache__/embedder.cpython-312.pyc +0 -0
  87. package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
  88. package/core/knowledge/__pycache__/vector_store.cpython-312.pyc +0 -0
  89. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  90. package/core/knowledge/embedder.py +118 -11
  91. package/core/knowledge/vector_store.py +111 -14
  92. package/core/memory/__pycache__/session_store.cpython-314.pyc +0 -0
  93. package/core/obsidian/__pycache__/relator.cpython-313.pyc +0 -0
  94. package/core/obsidian/relator.py +18 -8
  95. package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  96. package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/core/runtime/__pycache__/__init__.cpython-314.pyc +0 -0
  98. package/core/runtime/__pycache__/base.cpython-312.pyc +0 -0
  99. package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
  100. package/core/runtime/__pycache__/base.cpython-314.pyc +0 -0
  101. package/core/runtime/__pycache__/capabilities_cli.cpython-312.pyc +0 -0
  102. package/core/runtime/__pycache__/capabilities_cli.cpython-313.pyc +0 -0
  103. package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
  104. package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
  105. package/core/runtime/__pycache__/claude_code.cpython-314.pyc +0 -0
  106. package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
  107. package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
  108. package/core/runtime/__pycache__/codex_cli.cpython-314.pyc +0 -0
  109. package/core/runtime/__pycache__/cost_governor.cpython-312.pyc +0 -0
  110. package/core/runtime/__pycache__/cost_governor.cpython-313.pyc +0 -0
  111. package/core/runtime/__pycache__/cursor.cpython-312.pyc +0 -0
  112. package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
  113. package/core/runtime/__pycache__/cursor.cpython-314.pyc +0 -0
  114. package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
  115. package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
  116. package/core/runtime/__pycache__/gemini_cli.cpython-314.pyc +0 -0
  117. package/core/runtime/__pycache__/llm_cost_telemetry.cpython-314.pyc +0 -0
  118. package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
  119. package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
  120. package/core/runtime/__pycache__/llm_provider.cpython-314.pyc +0 -0
  121. package/core/runtime/__pycache__/llm_retry.cpython-312.pyc +0 -0
  122. package/core/runtime/__pycache__/llm_retry.cpython-313.pyc +0 -0
  123. package/core/runtime/__pycache__/llm_retry.cpython-314.pyc +0 -0
  124. package/core/runtime/__pycache__/native_usage.cpython-312.pyc +0 -0
  125. package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
  126. package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
  127. package/core/runtime/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  128. package/core/runtime/__pycache__/path_resolver.cpython-312.pyc +0 -0
  129. package/core/runtime/__pycache__/pricing.cpython-312.pyc +0 -0
  130. package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
  131. package/core/runtime/__pycache__/pricing.cpython-314.pyc +0 -0
  132. package/core/runtime/__pycache__/registry.cpython-312.pyc +0 -0
  133. package/core/runtime/__pycache__/registry.cpython-313.pyc +0 -0
  134. package/core/runtime/__pycache__/registry.cpython-314.pyc +0 -0
  135. package/core/runtime/__pycache__/squad_orchestrator.cpython-312.pyc +0 -0
  136. package/core/runtime/base.py +29 -0
  137. package/core/runtime/capabilities_cli.py +74 -0
  138. package/core/runtime/claude_code.py +8 -0
  139. package/core/runtime/codex_cli.py +205 -37
  140. package/core/runtime/cost_governor.py +184 -0
  141. package/core/runtime/cursor.py +8 -0
  142. package/core/runtime/gemini_cli.py +8 -0
  143. package/core/runtime/llm_provider.py +32 -13
  144. package/core/runtime/llm_retry.py +233 -0
  145. package/core/runtime/native_usage.py +186 -0
  146. package/core/runtime/pricing.py +9 -0
  147. package/core/runtime/registry.py +3 -0
  148. package/core/shared/__pycache__/__init__.cpython-314.pyc +0 -0
  149. package/core/shared/__pycache__/safe_session_id.cpython-314.pyc +0 -0
  150. package/core/shared/__pycache__/test_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
  151. package/core/shared/__pycache__/test_evidence.cpython-313.pyc +0 -0
  152. package/core/shared/test_evidence.py +34 -0
  153. package/core/synapse/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/core/synapse/__pycache__/agent_experiences_layer.cpython-312.pyc +0 -0
  155. package/core/synapse/__pycache__/cache.cpython-314.pyc +0 -0
  156. package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
  157. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  158. package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
  159. package/core/synapse/__pycache__/graph_context_layer.cpython-313.pyc +0 -0
  160. package/core/synapse/__pycache__/kb_cache.cpython-312.pyc +0 -0
  161. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  162. package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
  163. package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
  164. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  165. package/core/synapse/__pycache__/layers.cpython-314.pyc +0 -0
  166. package/core/synapse/engine.py +7 -0
  167. package/core/synapse/graph_context_layer.py +212 -0
  168. package/core/synapse/kb_cache.py +3 -0
  169. package/core/synapse/layers.py +104 -20
  170. package/core/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  171. package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
  172. package/core/workflow/__pycache__/engine.cpython-314.pyc +0 -0
  173. package/core/workflow/__pycache__/flow_enforcer.cpython-312.pyc +0 -0
  174. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  175. package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
  176. package/core/workflow/__pycache__/gate_checkpoint.cpython-312.pyc +0 -0
  177. package/core/workflow/__pycache__/gate_checkpoint.cpython-313.pyc +0 -0
  178. package/core/workflow/__pycache__/gate_checkpoint.cpython-314.pyc +0 -0
  179. package/core/workflow/__pycache__/loader.cpython-314.pyc +0 -0
  180. package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
  181. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  182. package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
  183. package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
  184. package/core/workflow/__pycache__/schema.cpython-314.pyc +0 -0
  185. package/core/workflow/__pycache__/specialist_enforcer.cpython-312.pyc +0 -0
  186. package/core/workflow/__pycache__/specialist_enforcer.cpython-313.pyc +0 -0
  187. package/core/workflow/__pycache__/state.cpython-314.pyc +0 -0
  188. package/core/workflow/enforcer.py +5 -16
  189. package/core/workflow/flow_enforcer.py +36 -11
  190. package/core/workflow/gate_checkpoint.py +149 -0
  191. package/core/workflow/research_gate.py +15 -3
  192. package/core/workflow/rules_registry.py +137 -389
  193. package/core/workflow/specialist_enforcer.py +21 -4
  194. package/dashboard/app/pages/knowledge/index.vue +4 -1
  195. package/departments/ops/skills/n8n-flow/SKILL.md +13 -0
  196. package/departments/ops/skills/workflow-automate/SKILL.md +16 -0
  197. package/departments/quality/SKILL.md +45 -5
  198. package/departments/quality/agents/copy-director.yaml +3 -1
  199. package/departments/quality/agents/tech-director.yaml +3 -1
  200. package/installer/adapters/claude-code.js +46 -2
  201. package/installer/config-seed.js +39 -14
  202. package/installer/doctor.js +8 -0
  203. package/installer/graphify.js +153 -0
  204. package/installer/index.js +20 -0
  205. package/installer/init.js +11 -0
  206. package/installer/update.js +19 -0
  207. package/package.json +1 -1
  208. package/pyproject.toml +3 -1
  209. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  210. package/scripts/__pycache__/knowledge-index.cpython-313.pyc +0 -0
  211. package/scripts/__pycache__/synapse-bridge.cpython-312.pyc +0 -0
  212. package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
  213. package/scripts/knowledge-index.py +8 -2
  214. package/scripts/synapse-bridge.py +102 -26
@@ -0,0 +1,600 @@
1
+ """UserPromptSubmit — consolidated entrypoint (PR-6 v4.1.0 hook hygiene).
2
+
3
+ Replaces the ~31 python3/jq spawn sites of the old ``user-prompt-submit.sh``
4
+ with ONE python process. The Synapse bridge (scripts/synapse-bridge.py) is
5
+ imported and called in-process via its ``run_bridge()`` function instead of
6
+ being spawned as a second interpreter.
7
+
8
+ Section order preserved exactly:
9
+ 1. V1 migration detection (short-circuit)
10
+ 2. Sync version detection ([arka:update-available])
11
+ 3. Flow marker + obsidian-query cache invalidation (invalidate_marker)
12
+ 4. Synapse bridge → 12-layer context string
13
+ 5. Workflow-state + Forge tags appended to the bridge context
14
+ 6. KB auto-inject (session cache overlap)
15
+ 7. Bash-parity fallback context (L0 constitution, branch, workflow,
16
+ forge) when the bridge degrades
17
+ 8. Token hygiene suggestions (4 checks, ported from token-hygiene.sh)
18
+ 9. Persistent routing reminder + workflow classifier directive
19
+ 10. Cognitive context injection + one-shot nudges (kb-cite, meta-tag,
20
+ closing-marker) gated by effort level
21
+ 11. additionalContext JSON output + hook metrics
22
+
23
+ Keep the classifier verb pattern in sync with
24
+ ``config/hooks/_lib/workflow-classifier.sh`` (still the CLI entry).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import contextlib
30
+ import importlib.util
31
+ import io
32
+ import json
33
+ import os
34
+ import re
35
+ import subprocess
36
+ import time
37
+ from pathlib import Path
38
+
39
+ from core.hooks._shared import (
40
+ ensure_root_on_path,
41
+ get_str,
42
+ read_stdin_json,
43
+ repo_path,
44
+ resolve_arkaos_root,
45
+ safe_session_id,
46
+ )
47
+
48
+ _CACHE_DIR = Path("/tmp/arkaos-context-cache")
49
+ _CACHE_TTL = 300 # Constitution cache: 5 minutes
50
+
51
+ _L0_FALLBACK = (
52
+ "[Constitution] NON-NEGOTIABLE: branch-isolation, obsidian-output, "
53
+ "authority-boundaries, security-gate, context-first, solid-clean-code, "
54
+ "spec-driven, human-writing, squad-routing, full-visibility, "
55
+ "sequential-validation, mandatory-qa, arka-supremacy | QUALITY-GATE: "
56
+ "marta-cqo, eduardo-copy, francisca-tech-ux | MUST: "
57
+ "conventional-commits, test-coverage, pattern-matching, "
58
+ "actionable-output, memory-persistence"
59
+ )
60
+
61
+ _ROUTE_REMINDER = """
62
+ [ARKA:ROUTE]
63
+ EVERY response MUST route through a department squad.
64
+ NO generic assistant replies. Announce the squad before responding.
65
+ When [knowledge:N chunks] is present, cite at least one source.
66
+ If [knowledge:N chunks] is absent on a non-trivial ArkaOS topic, query Obsidian first."""
67
+
68
+ _WORKFLOW_DIRECTIVE = """
69
+ [ARKA:WORKFLOW-REQUIRED] CREATION/IMPLEMENTATION detected — the 4-gate evidence
70
+ flow applies (constitution rule evidence-flow; source arka/skills/flow/SKILL.md).
71
+ G1 CONTEXT ([arka:routing] + grounding) -> G2 PLAN (explicit approval) ->
72
+ G3 EXECUTE (real test run + exit 0 on record) -> G4 REVIEW (executable checks).
73
+ Emit [arka:gate:N] at each gate start. No writes before G2 approval.
74
+ Trivial bypass: [arka:trivial] <reason>, single file, under 10 lines."""
75
+
76
+ # Keep in sync with ARKA_WF_VERB_PATTERN in _lib/workflow-classifier.sh.
77
+ _WF_VERB_PATTERN = (
78
+ r"(criar?|crie[ms]?|cria[mr]?|adicionar?|adiciona[mr]?|implementar?"
79
+ r"|implementa[mr]?|desenvolver?|desenvolve[mr]?|construir?"
80
+ r"|constru[ií]a?[mr]?|fazer?|faz[ae]?[mr]?|refactor(izar?)?|corrigir?"
81
+ r"|corrige[mr]?|consertar?|conserta[mr]?|continuar?|continua[mr]?"
82
+ r"|forçar?|força[mr]?|colocar?|coloca[mr]?|p[oô]r|melhorar?"
83
+ r"|melhora[mr]?|terminar?|termina[mr]?|acabar?|acaba[mr]?|publicar?"
84
+ r"|publica[mr]?|lançar?|lança[mr]?|create[sd]?|creating|build(s|ing)?"
85
+ r"|add(s|ed|ing)?|implement(s|ed|ing)?|develop(s|ed|ing)?"
86
+ r"|fix(es|ed|ing)?|refactor(s|ed|ing)?|make[sd]?|making|continue[sd]?"
87
+ r"|continuing|ship(s|ped|ping)?|merge[sd]?|merging|publish(es|ed|ing)?"
88
+ r"|release[sd]?|releasing|deploy(s|ed|ing)?|finish(es|ed|ing)?"
89
+ r"|improve[sd]?|improving)"
90
+ )
91
+ _WF_VERB_RE = re.compile(rf"\b{_WF_VERB_PATTERN}\b", re.IGNORECASE)
92
+
93
+ _STOPWORDS = frozenset(
94
+ "the a an and or but if then of for to in on at by with from is are was "
95
+ "were be been being do does did have has had this that these those it "
96
+ "its as i you we they he she them my your our their so not no yes can "
97
+ "will would could should may might must need want fix make use get set "
98
+ "add remove".split()
99
+ )
100
+
101
+ _VAGUE_PHRASES = (
102
+ "fix the bug", "that file", "the error",
103
+ "esse ficheiro", "esse erro", "aquele bug",
104
+ )
105
+
106
+
107
+ # ─── Sections 1-2: migration + sync detection ────────────────────────────
108
+
109
+
110
+ def _v1_migration_notice() -> str | None:
111
+ v1_paths = (
112
+ Path.home() / ".claude" / "skills" / "arka-os",
113
+ Path.home() / ".claude" / "skills" / "arkaos",
114
+ )
115
+ marker = Path.home() / ".arkaos" / "migrated-from-v1"
116
+ for v1_path in v1_paths:
117
+ if v1_path.is_dir() and not marker.is_file():
118
+ return (
119
+ f"[MIGRATION] ArkaOS v1 detected at {v1_path}. Run: npx "
120
+ f"arkaos migrate — This will backup v1, preserve your data, "
121
+ f"and install v2. See: "
122
+ f"https://github.com/andreagroferreira/arka-os#install"
123
+ )
124
+ return None
125
+
126
+
127
+ def _sync_notice() -> str:
128
+ repo = repo_path()
129
+ if not repo:
130
+ return ""
131
+ current = ""
132
+ version_file = Path(repo) / "VERSION"
133
+ package_json = Path(repo) / "package.json"
134
+ try:
135
+ if version_file.is_file():
136
+ current = version_file.read_text(encoding="utf-8").strip()
137
+ elif package_json.is_file():
138
+ current = str(
139
+ json.loads(package_json.read_text(encoding="utf-8"))["version"]
140
+ )
141
+ except Exception:
142
+ current = ""
143
+ if not current:
144
+ return ""
145
+ synced = "none"
146
+ sync_state = Path.home() / ".arkaos" / "sync-state.json"
147
+ if sync_state.is_file():
148
+ try:
149
+ synced = str(
150
+ json.loads(sync_state.read_text(encoding="utf-8"))["version"]
151
+ )
152
+ except Exception:
153
+ synced = "none"
154
+ if current != synced:
155
+ return (
156
+ f"[arka:update-available] ArkaOS v{current} installed "
157
+ f"(synced: {synced}). Run /arka update to sync all projects. "
158
+ )
159
+ return ""
160
+
161
+
162
+ # ─── Section 3: per-turn cache invalidation ──────────────────────────────
163
+
164
+
165
+ def _invalidate_turn_caches(session_id: str) -> None:
166
+ """invalidate_marker + invalidate_obsidian_query (new turn reset)."""
167
+ if not session_id:
168
+ return
169
+ try:
170
+ from core.workflow.marker_cache import invalidate_marker
171
+ invalidate_marker(session_id)
172
+ except Exception:
173
+ pass
174
+ try:
175
+ from core.synapse.kb_cache import invalidate_obsidian_query
176
+ invalidate_obsidian_query(session_id)
177
+ except Exception:
178
+ pass
179
+
180
+
181
+ # ─── Section 4: Synapse bridge (in-process) ──────────────────────────────
182
+
183
+
184
+ def _run_bridge(root: str, user_input: str, session_id: str) -> str:
185
+ bridge_path = Path(root) / "scripts" / "synapse-bridge.py"
186
+ if not bridge_path.is_file() or not Path(root).is_dir():
187
+ return ""
188
+ try:
189
+ spec = importlib.util.spec_from_file_location(
190
+ "arka_synapse_bridge", bridge_path
191
+ )
192
+ if spec is None or spec.loader is None:
193
+ return ""
194
+ module = importlib.util.module_from_spec(spec)
195
+ # The old hook piped bridge stderr to /dev/null — keep it quiet.
196
+ with contextlib.redirect_stderr(io.StringIO()):
197
+ spec.loader.exec_module(module)
198
+ output, code = module.run_bridge(
199
+ {"user_input": user_input, "session_id": session_id},
200
+ Path(root),
201
+ )
202
+ if code == 0:
203
+ return str(output.get("context_string", ""))
204
+ except Exception:
205
+ pass
206
+ return ""
207
+
208
+
209
+ # ─── Section 5: workflow-state + forge tags ──────────────────────────────
210
+
211
+
212
+ def _workflow_tag() -> str:
213
+ state_file = Path.home() / ".arkaos" / "workflow-state.json"
214
+ if not state_file.is_file():
215
+ return ""
216
+ try:
217
+ state = json.loads(state_file.read_text(encoding="utf-8"))
218
+ except (OSError, json.JSONDecodeError):
219
+ return ""
220
+ if not isinstance(state, dict):
221
+ return ""
222
+ phases = state.get("phases", {}) or {}
223
+ workflow = str(state.get("workflow", "") or "")
224
+ branch = str(state.get("branch", "") or "")
225
+ violations = len(state.get("violations", []) or [])
226
+ total = len(phases)
227
+ completed = sum(
228
+ 1 for p in phases.values() if p.get("status") == "completed"
229
+ )
230
+ current = next(
231
+ (k for k, p in phases.items() if p.get("status") == "in_progress"),
232
+ "none",
233
+ )
234
+ tag = (
235
+ f"[workflow:{workflow}] [phase:{current}] "
236
+ f"[branch:{branch}] [violations:{violations}]"
237
+ )
238
+ if violations != 0:
239
+ tag = f"WARNING: {violations} workflow violation(s). {tag}"
240
+ return tag
241
+
242
+
243
+ def _forge_tag() -> str:
244
+ active = Path.home() / ".arkaos" / "plans" / "active.yaml"
245
+ if not active.is_file():
246
+ return ""
247
+ try:
248
+ forge_id = active.read_text(encoding="utf-8").strip()
249
+ except OSError:
250
+ return ""
251
+ forge_file = Path.home() / ".arkaos" / "plans" / f"{forge_id}.yaml"
252
+ if not forge_file.is_file():
253
+ return ""
254
+ status = ""
255
+ try:
256
+ import yaml
257
+ data = yaml.safe_load(forge_file.read_text(encoding="utf-8")) or {}
258
+ status = str(data.get("status", ""))
259
+ except Exception:
260
+ status = "" # yaml-less python3 → tag still appended, empty status
261
+ return f"[forge:{forge_id}] [forge-status:{status}]"
262
+
263
+
264
+ # ─── Section 6: KB auto-inject (session cache) ───────────────────────────
265
+
266
+
267
+ def _kb_auto_inject(root: str, user_input: str) -> str:
268
+ import hashlib
269
+
270
+ kb_session_id = (
271
+ os.environ.get("ARKAOS_SESSION_ID")
272
+ or os.environ.get("CLAUDE_SESSION_ID")
273
+ or f"bridge-{os.getpid()}"
274
+ )
275
+ project_hash = hashlib.md5(
276
+ root.encode(), usedforsecurity=False
277
+ ).hexdigest()[:12]
278
+ cache_dir = Path("/tmp") / f"arkaos-kb-{project_hash}"
279
+ if not cache_dir.is_dir():
280
+ return ""
281
+ try:
282
+ from core.synapse.kb_cache import KBSessionCache
283
+ cache = KBSessionCache(session_id=kb_session_id, project_path=root)
284
+ results = cache.get_overlap(user_input, threshold=0.3)
285
+ except Exception:
286
+ return ""
287
+ if not results:
288
+ return ""
289
+ snippets = []
290
+ for r in results[:3]:
291
+ src = r.get("source", "").split("/")[-1] if r.get("source") else ""
292
+ txt = str(r.get("text", ""))[:200].replace("\n", " ")
293
+ snippets.append(f"{src}: {txt}" if src else txt)
294
+ return " | ".join(snippets)
295
+
296
+
297
+ # ─── Section 7: bash-parity fallback context ─────────────────────────────
298
+
299
+
300
+ def _l0_constitution() -> str:
301
+ cache_file = _CACHE_DIR / "l0-constitution"
302
+ try:
303
+ if cache_file.is_file():
304
+ age = time.time() - cache_file.stat().st_mtime
305
+ if age < _CACHE_TTL:
306
+ return cache_file.read_text(encoding="utf-8")
307
+ except OSError:
308
+ pass
309
+ try:
310
+ cache_file.write_text(_L0_FALLBACK, encoding="utf-8")
311
+ except OSError:
312
+ pass
313
+ return _L0_FALLBACK
314
+
315
+
316
+ def _git_branch_tag() -> str:
317
+ try:
318
+ branch = subprocess.run(
319
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
320
+ capture_output=True, text=True, timeout=2,
321
+ ).stdout.strip()
322
+ except Exception:
323
+ branch = ""
324
+ if branch and branch not in ("main", "master", "dev"):
325
+ return f"[branch:{branch}]"
326
+ return ""
327
+
328
+
329
+ def _fallback_context() -> str:
330
+ l0 = _l0_constitution()
331
+ l4 = _git_branch_tag()
332
+ l7 = "" # Time layer intentionally skipped in fallback (cache churn).
333
+ l8 = _workflow_tag()
334
+ l9 = _forge_tag()
335
+ return f"{l0} {l4} {l7} {l8} {l9}"
336
+
337
+
338
+ # ─── Section 8: token hygiene (ported from token-hygiene.sh) ─────────────
339
+
340
+
341
+ def _keywords(text: str) -> list[str]:
342
+ tokens = re.split(r"[^a-z0-9]+", text.lower())
343
+ return sorted({
344
+ t for t in tokens if len(t) > 3 and t not in _STOPWORDS
345
+ })
346
+
347
+
348
+ def _last_user_messages(transcript_path: str, n: int = 3) -> str:
349
+ if not transcript_path or not Path(transcript_path).is_file():
350
+ return ""
351
+ try:
352
+ lines = Path(transcript_path).read_text(
353
+ encoding="utf-8", errors="replace"
354
+ ).splitlines()[-200:]
355
+ except OSError:
356
+ return ""
357
+ msgs: list[str] = []
358
+ for line in lines:
359
+ try:
360
+ record = json.loads(line)
361
+ except (json.JSONDecodeError, ValueError):
362
+ continue
363
+ if not isinstance(record, dict):
364
+ continue
365
+ if record.get("type") == "user" or record.get("role") == "user":
366
+ content = record.get("content") or record.get(
367
+ "message", {}
368
+ ).get("content", "")
369
+ if isinstance(content, list):
370
+ content = " ".join(
371
+ p.get("text", "") for p in content if isinstance(p, dict)
372
+ )
373
+ msgs.append(str(content))
374
+ return "\n".join(msgs[-n:])
375
+
376
+
377
+ def _token_hygiene(prompt: str, transcript_path: str) -> str:
378
+ suggestions: list[str] = []
379
+
380
+ ctx_raw = os.environ.get("CLAUDE_CONTEXT_USED", "").rstrip("%")
381
+ if ctx_raw:
382
+ try:
383
+ ctx = int(ctx_raw)
384
+ if ctx > 80:
385
+ suggestions.append(
386
+ f"[arka:warn] Context at {ctx}% — /compact recommended NOW."
387
+ )
388
+ elif ctx > 60:
389
+ suggestions.append(
390
+ f"[arka:suggest] Context at {ctx}% — consider /compact."
391
+ )
392
+ except ValueError:
393
+ pass
394
+
395
+ if prompt:
396
+ prior = _last_user_messages(transcript_path)
397
+ cur_kw = _keywords(prompt)[:20]
398
+ if prior and cur_kw and len(cur_kw) > 2:
399
+ overlap = len(set(cur_kw) & set(_keywords(prior)))
400
+ if overlap * 100 // len(cur_kw) < 30:
401
+ suggestions.append(
402
+ "[arka:suggest] Topic shift detected — consider /clear "
403
+ "for a fresh session."
404
+ )
405
+
406
+ if len(prompt) > 2000 and "```" in prompt:
407
+ suggestions.append(
408
+ f"[arka:suggest] Large paste detected ({len(prompt)} chars) "
409
+ f"— consider @filepath reference for better token economy."
410
+ )
411
+
412
+ lower = prompt.lower()
413
+ if any(p in lower for p in _VAGUE_PHRASES) and "@" not in prompt:
414
+ suggestions.append(
415
+ "[arka:suggest] Vague reference — use @path/to/file.ext "
416
+ "for precision."
417
+ )
418
+
419
+ return " ".join(suggestions)
420
+
421
+
422
+ # ─── Section 9: workflow classifier ──────────────────────────────────────
423
+
424
+
425
+ def _wf_classify(text: str) -> bool:
426
+ if not text or text[0] in ("/", "!"):
427
+ return False
428
+ return bool(_WF_VERB_RE.search(text))
429
+
430
+
431
+ def _wf_mark_required(session_id: str) -> None:
432
+ if safe_session_id(session_id) is None:
433
+ return
434
+ marker_dir = Path(
435
+ os.environ.get("ARKA_WF_REQUIRED_DIR", "/tmp/arkaos-wf-required")
436
+ )
437
+ try:
438
+ marker_dir.mkdir(parents=True, exist_ok=True)
439
+ (marker_dir / session_id).write_text(
440
+ time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
441
+ encoding="utf-8",
442
+ )
443
+ except OSError:
444
+ pass
445
+
446
+
447
+ # ─── Section 10: cognitive inject + one-shot nudges ──────────────────────
448
+
449
+
450
+ def _cognitive_hits(session_id: str) -> str:
451
+ if not session_id:
452
+ return ""
453
+ repo = repo_path()
454
+ if not repo or not Path(repo).is_dir():
455
+ return ""
456
+ try:
457
+ from core.cognition.retrieval import format_advisory, read_context
458
+ return format_advisory(read_context(session_id)) or ""
459
+ except Exception:
460
+ return ""
461
+
462
+
463
+ def _one_shot_nudge(subdir: str, session_id: str) -> str:
464
+ """Read + delete a /tmp/<subdir>/<session>.json nudge state file."""
465
+ nudge_file = Path("/tmp") / subdir / f"{session_id}.json"
466
+ if not nudge_file.is_file():
467
+ return ""
468
+ nudge = ""
469
+ try:
470
+ data = json.loads(nudge_file.read_text(encoding="utf-8"))
471
+ suggestion = data.get("suggestion")
472
+ if data.get("passed") is False and suggestion:
473
+ nudge = f"[arka:suggest] {suggestion}"
474
+ except (OSError, json.JSONDecodeError, ValueError):
475
+ pass
476
+ try:
477
+ nudge_file.unlink(missing_ok=True)
478
+ except OSError:
479
+ pass
480
+ return nudge
481
+
482
+
483
+ # ─── Section 11: metrics ─────────────────────────────────────────────────
484
+
485
+
486
+ def _log_metrics(elapsed_ms: int, user_input: str) -> None:
487
+ if elapsed_ms <= 0:
488
+ return
489
+ at_mentions = len(
490
+ re.findall(r"(?:^|\s)@[A-Za-z0-9_./-]+", user_input)
491
+ )
492
+ try:
493
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
494
+ with (_CACHE_DIR / "hook-metrics.jsonl").open(
495
+ "a", encoding="utf-8"
496
+ ) as fh:
497
+ fh.write(json.dumps({
498
+ "hook": "user-prompt-submit-v2",
499
+ "ms": elapsed_ms,
500
+ "at_mentions": at_mentions,
501
+ }) + "\n")
502
+ except OSError:
503
+ pass
504
+
505
+
506
+ # ─── Entry point ─────────────────────────────────────────────────────────
507
+
508
+
509
+ def main(stdin_json: dict | None = None, raw: str = "") -> int:
510
+ start = time.monotonic()
511
+ if stdin_json is None:
512
+ stdin_json, raw = read_stdin_json()
513
+
514
+ migration = _v1_migration_notice()
515
+ if migration is not None:
516
+ print(json.dumps({"additionalContext": migration}))
517
+ return 0
518
+
519
+ sync_notice = _sync_notice()
520
+
521
+ root = resolve_arkaos_root()
522
+ ensure_root_on_path(root)
523
+ try:
524
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
525
+ except OSError:
526
+ pass
527
+
528
+ user_input = get_str(stdin_json, "userInput") or get_str(
529
+ stdin_json, "message"
530
+ )
531
+ session_id = get_str(stdin_json, "session_id")
532
+ effort_level = get_str(stdin_json, "effort", "level") or os.environ.get(
533
+ "CLAUDE_EFFORT", ""
534
+ )
535
+ surface_nudges = (effort_level or "high") not in ("low", "medium")
536
+
537
+ _invalidate_turn_caches(session_id)
538
+
539
+ if not user_input:
540
+ user_input = raw[:2000]
541
+
542
+ # ─── Synapse bridge + tags ───────────────────────────────────────
543
+ python_result = _run_bridge(root, user_input, session_id)
544
+ if python_result:
545
+ wf_tag = _workflow_tag()
546
+ if wf_tag:
547
+ python_result = f"{python_result} {wf_tag}"
548
+ forge_tag = _forge_tag()
549
+ if forge_tag:
550
+ python_result = f"{python_result} {forge_tag}"
551
+ if "[knowledge:" in python_result:
552
+ kb_content = _kb_auto_inject(root, user_input)
553
+ if kb_content:
554
+ python_result = f"{kb_content} {python_result}"
555
+ else:
556
+ python_result = _fallback_context()
557
+
558
+ hygiene = _token_hygiene(
559
+ user_input, get_str(stdin_json, "transcript_path")
560
+ )
561
+
562
+ workflow_directive = ""
563
+ if user_input and _wf_classify(user_input):
564
+ _wf_mark_required(session_id)
565
+ workflow_directive = _WORKFLOW_DIRECTIVE
566
+
567
+ context_hits = _cognitive_hits(session_id)
568
+
569
+ kb_cite_nudge = meta_tag_nudge = closing_marker_nudge = ""
570
+ if session_id and surface_nudges:
571
+ kb_cite_nudge = _one_shot_nudge("arkaos-cite", session_id)
572
+ meta_tag_nudge = _one_shot_nudge("arkaos-meta", session_id)
573
+ closing_marker_nudge = _one_shot_nudge("arkaos-closing", session_id)
574
+
575
+ # ─── Output (assembly order identical to the bash version) ──────
576
+ out = (
577
+ f"{sync_notice}{_ROUTE_REMINDER}{workflow_directive} {python_result}"
578
+ )
579
+ if hygiene:
580
+ out = f"{out} {hygiene}"
581
+ for nudge in (kb_cite_nudge, meta_tag_nudge, closing_marker_nudge):
582
+ if nudge:
583
+ out = f"{out}\n{nudge}"
584
+ if context_hits:
585
+ out = f"{out}\n{context_hits}"
586
+ print(json.dumps({"additionalContext": out}))
587
+
588
+ _log_metrics(int((time.monotonic() - start) * 1000), user_input)
589
+ return 0
590
+
591
+
592
+ if __name__ == "__main__":
593
+ try:
594
+ raise SystemExit(main())
595
+ except SystemExit:
596
+ raise
597
+ except Exception:
598
+ # Fail open with the minimal constitution context.
599
+ print(json.dumps({"additionalContext": _L0_FALLBACK}))
600
+ raise SystemExit(0)