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.
- package/README.md +2 -2
- package/VERSION +1 -1
- package/arka/SKILL.md +16 -22
- package/arka/skills/flow/SKILL.md +108 -234
- package/config/claude-agents/eduardo-copy.md +53 -0
- package/config/claude-agents/francisca-tech.md +52 -0
- package/config/claude-agents/marta-cqo.md +52 -0
- package/config/claude-agents/paulo-tech-lead.md +50 -0
- package/config/cognition/schedules.yaml +15 -0
- package/config/constitution.yaml +17 -21
- package/config/hooks/_lib/workflow-classifier.sh +1 -1
- package/config/hooks/post-tool-use.sh +40 -539
- package/config/hooks/pre-tool-use.sh +32 -281
- package/config/hooks/session-start.sh +8 -10
- package/config/hooks/stop.sh +30 -425
- package/config/hooks/user-prompt-submit.sh +42 -459
- package/core/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__/closing_marker_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/compliance_telemetry.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/constitution.cpython-312.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/qg_verdict.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
- package/core/governance/closing_marker_check.py +9 -5
- package/core/governance/compliance_telemetry.py +1 -1
- package/core/governance/compliance_telemetry_cli.py +1 -1
- package/core/governance/evidence_checks.py +421 -0
- package/core/governance/meta_tag_check.py +36 -0
- package/core/governance/qg_verdict.py +78 -0
- package/core/governance/review_workflow.py +34 -1
- package/core/governance/skill_proposer.py +2 -1
- package/core/hooks/__init__.py +12 -0
- package/core/hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/_shared.py +110 -0
- package/core/hooks/post_tool_use.py +689 -0
- package/core/hooks/pre_tool_use.py +267 -0
- package/core/hooks/stop.py +395 -0
- package/core/hooks/user_prompt_submit.py +600 -0
- package/core/jobs/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/embedder.py +118 -11
- package/core/knowledge/vector_store.py +111 -14
- package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/path_resolver.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/squad_orchestrator.cpython-312.pyc +0 -0
- package/core/runtime/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__/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__/agent_experiences_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/graph_context_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +7 -0
- package/core/synapse/graph_context_layer.py +212 -0
- package/core/synapse/kb_cache.py +3 -0
- package/core/synapse/layers.py +104 -20
- package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/enforcer.py +5 -16
- package/core/workflow/flow_enforcer.py +36 -11
- package/core/workflow/gate_checkpoint.py +149 -0
- package/core/workflow/research_gate.py +15 -3
- package/core/workflow/rules_registry.py +137 -389
- package/core/workflow/specialist_enforcer.py +21 -4
- package/dashboard/app/pages/knowledge/index.vue +4 -1
- package/departments/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 +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/__pycache__/knowledge-index.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
- package/scripts/knowledge-index.py +8 -2
- 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)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|