arkaos 4.13.2 → 4.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/arka/SKILL.md +16 -0
- package/bin/arka-doctor +20 -1
- package/config/cognition/schedules.yaml +16 -0
- package/config/hooks/_lib/fastpath/engine.cjs +337 -0
- package/config/hooks/gate-manifest.json +741 -0
- package/config/hooks/post-tool-use.cjs +113 -0
- package/config/hooks/pre-tool-use.cjs +109 -0
- package/config/hooks/session-end.ps1 +40 -0
- package/config/hooks/session-end.sh +55 -0
- package/config/hooks/session-start.sh +25 -189
- package/config/hooks/subagent-stop.ps1 +40 -0
- package/config/hooks/subagent-stop.sh +56 -0
- package/config/mcp-policy.yaml +8 -0
- package/config/skills-curated.yaml +39 -0
- package/config/statusline.sh +57 -1
- package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
- package/core/cognition/capture/store.py +7 -2
- package/core/evals/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/sanitizer.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/schema.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/verdict_labels.cpython-314.pyc +0 -0
- package/core/forge/__pycache__/complexity.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/judge.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/leak_scanner.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/qg_verdict.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback_cli.cpython-313.pyc +0 -0
- package/core/governance/evidence_checks.py +42 -7
- package/core/governance/routing_feedback.py +229 -0
- package/core/governance/routing_feedback_cli.py +49 -0
- package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/gate_manifest.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__/session_end.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/subagent_stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/subagent_stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-314.pyc +0 -0
- package/core/hooks/gate_manifest.py +366 -0
- package/core/hooks/post_tool_use.py +5 -2
- package/core/hooks/session_end.py +127 -0
- package/core/hooks/session_start.py +322 -0
- package/core/hooks/stop.py +69 -0
- package/core/hooks/subagent_stop.py +186 -0
- package/core/hooks/user_prompt_submit.py +10 -6
- package/core/knowledge/__pycache__/embedding_backends.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/embedding_backends.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/pattern_cards.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/pattern_cards.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/recipes.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-314.pyc +0 -0
- package/core/knowledge/embedding_backends.py +278 -0
- package/core/knowledge/pattern_cards.py +49 -1
- package/core/knowledge/vector_store.py +14 -2
- package/core/memory/__pycache__/semantic_store.cpython-312.pyc +0 -0
- package/core/memory/__pycache__/semantic_store.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/semantic_store.cpython-314.pyc +0 -0
- package/core/memory/__pycache__/turn_capture.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/turn_capture.cpython-314.pyc +0 -0
- package/core/memory/semantic_store.py +301 -0
- package/core/memory/turn_capture.py +277 -0
- package/core/registry/__pycache__/generator.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/mcp_telemetry.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/decay.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/decay.cpython-314.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-314.pyc +0 -0
- package/core/shared/decay.py +94 -0
- package/core/shared/sqlite_recovery.py +266 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/pattern_library_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/recipe_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/routing_feedback_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/routing_feedback_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-314.pyc +0 -0
- package/core/synapse/agent_experiences_layer.py +42 -3
- package/core/synapse/engine.py +13 -0
- package/core/synapse/recipe_layer.py +27 -4
- package/core/synapse/routing_feedback_layer.py +132 -0
- package/core/synapse/session_memory_layer.py +152 -0
- package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/design_authorization.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/frontend_gate.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-314.pyc +0 -0
- package/installer/adapters/claude-code.js +37 -0
- package/installer/claude-plugins.js +6 -0
- package/installer/cli.js +23 -2
- package/installer/config-seed.js +25 -16
- package/installer/doctor.js +116 -1
- package/installer/hook-lib.js +28 -0
- package/installer/index.js +48 -153
- package/installer/mcp-runner.js +108 -0
- package/installer/skill-deploy.js +189 -0
- package/installer/skills-mode.js +64 -0
- package/installer/update.js +55 -79
- package/knowledge/commands-registry.json.bak +171 -4
- package/knowledge/skills-manifest.json +2337 -0
- package/mcps/arka-prompts/commands.py +451 -0
- package/mcps/arka-prompts/pyproject.toml +15 -0
- package/mcps/arka-prompts/server.py +156 -0
- package/mcps/arka-tools/__pycache__/server.cpython-313.pyc +0 -0
- package/mcps/arka-tools/pyproject.toml +10 -0
- package/mcps/arka-tools/server.py +319 -0
- package/mcps/profiles/base.json +19 -0
- package/mcps/profiles/brand.json +19 -0
- package/mcps/profiles/comms.json +13 -0
- package/mcps/profiles/content.json +20 -0
- package/mcps/profiles/ecommerce.json +13 -0
- package/mcps/profiles/full-stack.json +15 -0
- package/mcps/profiles/laravel.json +12 -0
- package/mcps/profiles/nextjs.json +13 -0
- package/mcps/profiles/nuxt.json +13 -0
- package/mcps/profiles/react.json +12 -0
- package/mcps/profiles/vue.json +12 -0
- package/mcps/registry.json +423 -0
- package/mcps/scripts/apply-mcps.sh +282 -0
- package/mcps/stacks/laravel-packages.json +49 -0
- package/mcps/stacks/react-packages.json +58 -0
- package/package.json +2 -1
- package/pyproject.toml +9 -1
- package/scripts/__pycache__/marketplace_gen.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-314.pyc +0 -0
- package/scripts/marketplace_gen.py +310 -0
- package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
- package/scripts/tools/__pycache__/skill_budget.cpython-313.pyc +0 -0
- package/scripts/tools/docs_stats.py +6 -4
- package/scripts/tools/skill_budget.py +209 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""SessionStart — consolidated entrypoint (F2-2, hook-hygiene completion).
|
|
2
|
+
|
|
3
|
+
Replaces the 13 python spawn sites of the old ``session-start.sh``
|
|
4
|
+
(profile x2, sync drift, forge YAML x4, Model Fabric, reorganizer
|
|
5
|
+
trigger, dashboard config read, rehydrator, memory recap, final JSON
|
|
6
|
+
print) with ONE python process that emits the full ``systemMessage``.
|
|
7
|
+
Measured baseline before this consolidation: 251ms p50 (isolated floor,
|
|
8
|
+
benchmarks/results.md); each spawn costs ~20-80ms.
|
|
9
|
+
|
|
10
|
+
Section order preserved exactly from the shell version:
|
|
11
|
+
banner+greeting → workflow line → version+forge+drift →
|
|
12
|
+
evidence-flow contract → meta-tag contract → Model Fabric directive
|
|
13
|
+
→ [SESSION] rehydrator resume → [SESSION-MEMORY] recap → JSON out.
|
|
14
|
+
Background side effects (reorganizer trigger, dashboard ensure) stay
|
|
15
|
+
detached and are config-gated; the NEW ``cognition.reorganize_on_session``
|
|
16
|
+
gate lands here (QG F2-1 follow-up, endorsed for this PR).
|
|
17
|
+
|
|
18
|
+
The shell wrapper only resolves the interpreter and ``exec``s this
|
|
19
|
+
module; with no usable venv it emits a static banner (fail-open).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from datetime import UTC, datetime
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from core.hooks._shared import get_str, read_stdin_json, repo_path
|
|
33
|
+
|
|
34
|
+
_BUDGET_MS = 300
|
|
35
|
+
_RECAP_ITEMS = 3
|
|
36
|
+
_SUMMARY_CHARS = 130
|
|
37
|
+
|
|
38
|
+
_BANNER = (
|
|
39
|
+
"\n╔══════════════════════════════════════════════╗\n"
|
|
40
|
+
"║ ║\n"
|
|
41
|
+
"║ A R K A O S ║\n"
|
|
42
|
+
"║ ║\n"
|
|
43
|
+
"║ The Operating System for AI Teams ║\n"
|
|
44
|
+
"║ by WizardingCode ║\n"
|
|
45
|
+
"║ ║\n"
|
|
46
|
+
"╚══════════════════════════════════════════════╝\n"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_EVIDENCE_CONTRACT = (
|
|
50
|
+
"\n\n[ARKA:EVIDENCE-FLOW] NON-NEGOTIABLE. Every non-trivial request runs"
|
|
51
|
+
" the 4-gate evidence flow (constitution rule evidence-flow; source"
|
|
52
|
+
" arka/skills/flow/SKILL.md):"
|
|
53
|
+
"\n G1 CONTEXT ([arka:routing] <dept> -> <lead> + KB/graph grounding,"
|
|
54
|
+
" cite or declare gap)"
|
|
55
|
+
"\n G2 PLAN (short plan -> EXPLICIT user approval; silence != approval)"
|
|
56
|
+
"\n G3 EXECUTE (closes only with real test run on record: command + exit 0)"
|
|
57
|
+
"\n G4 REVIEW (executable checks: lint/type/coverage/security/spell ->"
|
|
58
|
+
" honest summary)"
|
|
59
|
+
"\nEmit [arka:gate:N] at each gate start. Gates pass on evidence, never"
|
|
60
|
+
" on narration."
|
|
61
|
+
"\nBypass ONLY via [arka:trivial] <reason> for single-file edits under"
|
|
62
|
+
" 10 lines."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_META_TAG_CONTRACT = (
|
|
66
|
+
"\n\n[ARKA:META-TAG] Every substantive response ends with a single line:"
|
|
67
|
+
"\n [arka:meta] kb=N research=X persona=Y gap=Z critic=W"
|
|
68
|
+
"\nFields: kb=N (Obsidian/KB notes consulted), research=X (MCPs invoked:"
|
|
69
|
+
" perplexity,exa,context7,firecrawl,xmcp or 'none'), persona=Y (advisor"
|
|
70
|
+
" name or 'orchestrator'), gap=Z (KB gap topic or 'none'), critic=W"
|
|
71
|
+
" (passed|failed|skipped)."
|
|
72
|
+
"\nMandatory after: EFFECT tool calls, plan/recommendation outputs, QG"
|
|
73
|
+
" verdicts. Optional for pure read-only status replies."
|
|
74
|
+
"\nAbsence is measured by the Stop hook (warn-only in v2.34.0) before"
|
|
75
|
+
" promotion to hard enforcement."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _config() -> dict:
|
|
80
|
+
try:
|
|
81
|
+
data = json.loads(
|
|
82
|
+
(Path.home() / ".arkaos" / "config.json").read_text(encoding="utf-8")
|
|
83
|
+
)
|
|
84
|
+
return data if isinstance(data, dict) else {}
|
|
85
|
+
except (OSError, json.JSONDecodeError):
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _profile() -> tuple[str, str]:
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(
|
|
92
|
+
(Path.home() / ".arkaos" / "profile.json").read_text(encoding="utf-8")
|
|
93
|
+
)
|
|
94
|
+
name = data.get("name") or data.get("role") or "founder"
|
|
95
|
+
return str(name), str(data.get("company") or "WizardingCode")
|
|
96
|
+
except (OSError, json.JSONDecodeError, AttributeError):
|
|
97
|
+
return "founder", "WizardingCode"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _version(repo: str) -> str:
|
|
101
|
+
candidates = [Path.home() / ".arkaos" / "lib" / "VERSION"]
|
|
102
|
+
if repo:
|
|
103
|
+
candidates.insert(0, Path(repo) / "VERSION")
|
|
104
|
+
for candidate in candidates:
|
|
105
|
+
try:
|
|
106
|
+
if candidate.is_file():
|
|
107
|
+
return candidate.read_text(encoding="utf-8").strip()
|
|
108
|
+
except OSError:
|
|
109
|
+
continue
|
|
110
|
+
return "2.x"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _drift(version: str) -> str:
|
|
114
|
+
sync_state = Path.home() / ".arkaos" / "sync-state.json"
|
|
115
|
+
if not sync_state.is_file():
|
|
116
|
+
return "\n[arka:update-available] Never synced. Run /arka update."
|
|
117
|
+
try:
|
|
118
|
+
synced = str(json.loads(sync_state.read_text(encoding="utf-8"))["version"])
|
|
119
|
+
except (OSError, json.JSONDecodeError, KeyError):
|
|
120
|
+
synced = "none"
|
|
121
|
+
if synced != version:
|
|
122
|
+
return (
|
|
123
|
+
f"\n[arka:update-available] Core v{version} != synced"
|
|
124
|
+
f" v{synced}. Run /arka update."
|
|
125
|
+
)
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _workflow_line() -> str:
|
|
130
|
+
try:
|
|
131
|
+
from core.workflow.state import get_state
|
|
132
|
+
|
|
133
|
+
state = get_state()
|
|
134
|
+
except Exception: # degraded env — banner never breaks
|
|
135
|
+
return ""
|
|
136
|
+
if not state:
|
|
137
|
+
return ""
|
|
138
|
+
phases = state.get("phases") or {}
|
|
139
|
+
completed = sum(
|
|
140
|
+
1 for p in phases.values()
|
|
141
|
+
if isinstance(p, dict) and p.get("status") == "completed"
|
|
142
|
+
)
|
|
143
|
+
line = f"\nWorkflow: {state.get('workflow', '')} ({completed}/{len(phases)})"
|
|
144
|
+
if state.get("branch"):
|
|
145
|
+
line += f" branch:{state['branch']}"
|
|
146
|
+
violations = len(state.get("violations") or [])
|
|
147
|
+
if violations:
|
|
148
|
+
line += f" VIOLATIONS:{violations}"
|
|
149
|
+
return line + "\n"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _forge_line() -> str:
|
|
153
|
+
try:
|
|
154
|
+
from core.forge.persistence import get_active_plan
|
|
155
|
+
|
|
156
|
+
plan = get_active_plan()
|
|
157
|
+
except Exception:
|
|
158
|
+
return ""
|
|
159
|
+
if plan is None:
|
|
160
|
+
return ""
|
|
161
|
+
name = getattr(plan, "name", "")
|
|
162
|
+
status = getattr(plan, "status", "")
|
|
163
|
+
phases = len(getattr(plan, "plan_phases", []) or [])
|
|
164
|
+
if status == "approved":
|
|
165
|
+
return f"\n ⚒ Forge plan pending: {name} | Phases: {phases} | /forge resume"
|
|
166
|
+
if status == "executing":
|
|
167
|
+
governance = getattr(plan, "governance", None)
|
|
168
|
+
branch = getattr(governance, "branch_strategy", "") or "" if governance else ""
|
|
169
|
+
return f"\n ⚒ Forge executing: {name} | Phases: {phases} | Branch: {branch}"
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _model_fabric() -> str:
|
|
174
|
+
try:
|
|
175
|
+
from core.runtime.model_routing_context import routing_directive
|
|
176
|
+
|
|
177
|
+
directive = routing_directive()
|
|
178
|
+
return f"\n\n{directive}" if directive else ""
|
|
179
|
+
except Exception:
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _session_resume() -> str:
|
|
184
|
+
try:
|
|
185
|
+
from core.memory.rehydrator import build_resume_context
|
|
186
|
+
|
|
187
|
+
ctx = build_resume_context()
|
|
188
|
+
except Exception:
|
|
189
|
+
return ""
|
|
190
|
+
if not ctx:
|
|
191
|
+
return ""
|
|
192
|
+
return "\n\n[SESSION] " + ctx.replace("\n", "\n[SESSION] ")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _spawn_detached(cmd: list[str], repo: str, log_path: Path | None = None) -> None:
|
|
196
|
+
stdout = subprocess.DEVNULL
|
|
197
|
+
handle = None
|
|
198
|
+
try:
|
|
199
|
+
if log_path is not None:
|
|
200
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
handle = log_path.open("a")
|
|
202
|
+
stdout = handle
|
|
203
|
+
subprocess.Popen(
|
|
204
|
+
cmd,
|
|
205
|
+
stdout=stdout,
|
|
206
|
+
stderr=subprocess.STDOUT if handle else subprocess.DEVNULL,
|
|
207
|
+
env={**os.environ, "PYTHONPATH": repo, "ARKAOS_NO_BROWSER": "1"},
|
|
208
|
+
cwd=repo or None,
|
|
209
|
+
start_new_session=True,
|
|
210
|
+
)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
finally:
|
|
214
|
+
if handle is not None:
|
|
215
|
+
handle.close() # the child keeps its own inherited fd
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _trigger_reorganizer(repo: str, config: dict) -> None:
|
|
219
|
+
"""Stale-aware AND config-gated: ``cognition.reorganize_on_session``
|
|
220
|
+
(default True) — the F2-1 QG follow-up gate lands here. The staleness
|
|
221
|
+
guard uses UTC, same basis as before."""
|
|
222
|
+
cognition = config.get("cognition") or {}
|
|
223
|
+
if not cognition.get("reorganize_on_session", True):
|
|
224
|
+
return
|
|
225
|
+
today = datetime.now(UTC).strftime("%Y-%m-%d")
|
|
226
|
+
proposal = Path.home() / ".arkaos" / "reorganize-proposals" / f"{today}.md"
|
|
227
|
+
if proposal.is_file() or not repo:
|
|
228
|
+
return
|
|
229
|
+
_spawn_detached([sys.executable, "-m", "core.cognition.reorganizer_cli"], repo)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _ensure_dashboard(repo: str, config: dict) -> None:
|
|
233
|
+
dashboard = config.get("dashboard") or {}
|
|
234
|
+
if not dashboard.get("ensure_on_session", True):
|
|
235
|
+
return
|
|
236
|
+
script = Path(repo) / "scripts" / "start-dashboard.sh" if repo else None
|
|
237
|
+
if script is None or not script.is_file():
|
|
238
|
+
return
|
|
239
|
+
_spawn_detached(
|
|
240
|
+
["bash", str(script), "ensure"], repo,
|
|
241
|
+
log_path=Path.home() / ".arkaos" / "logs" / "dashboard-ensure.log",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def build_message(cwd: str) -> str:
|
|
246
|
+
repo = repo_path()
|
|
247
|
+
config = _config()
|
|
248
|
+
name, company = _profile()
|
|
249
|
+
version = _version(repo)
|
|
250
|
+
msg = _BANNER + f"\nOlá, {name} ({company})\n"
|
|
251
|
+
msg += _workflow_line()
|
|
252
|
+
msg += f"ArkaOS v{version}"
|
|
253
|
+
msg += _forge_line()
|
|
254
|
+
msg += _drift(version)
|
|
255
|
+
msg += _EVIDENCE_CONTRACT
|
|
256
|
+
msg += _META_TAG_CONTRACT
|
|
257
|
+
msg += _model_fabric()
|
|
258
|
+
_trigger_reorganizer(repo, config)
|
|
259
|
+
_ensure_dashboard(repo, config)
|
|
260
|
+
msg += _session_resume()
|
|
261
|
+
recap = build_recap(cwd)
|
|
262
|
+
if recap:
|
|
263
|
+
msg += f"\n\n{recap}"
|
|
264
|
+
return msg
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def build_recap(cwd: str, budget_ms: int = _BUDGET_MS) -> str:
|
|
268
|
+
"""[SESSION-MEMORY] importance+recency recap (F1-A3, semantics kept)."""
|
|
269
|
+
start = time.monotonic()
|
|
270
|
+
try:
|
|
271
|
+
from core.memory.semantic_store import (
|
|
272
|
+
SessionMemoryStore,
|
|
273
|
+
default_db_path,
|
|
274
|
+
neutralize_summary,
|
|
275
|
+
)
|
|
276
|
+
if not default_db_path().is_file():
|
|
277
|
+
return ""
|
|
278
|
+
# Scope-or-skip (defense-in-depth, mirrors L9.5): no resolvable
|
|
279
|
+
# project ⇒ no recap — never a silently-global read.
|
|
280
|
+
project = Path(cwd).name if cwd else ""
|
|
281
|
+
if not project:
|
|
282
|
+
return ""
|
|
283
|
+
store = SessionMemoryStore()
|
|
284
|
+
records = store.recent(project_name=project, limit=_RECAP_ITEMS)
|
|
285
|
+
if not records or (time.monotonic() - start) * 1000 > budget_ms:
|
|
286
|
+
return ""
|
|
287
|
+
lines = ["[SESSION-MEMORY] Prior turns (importance+recency — not semantic):"]
|
|
288
|
+
for record in records:
|
|
289
|
+
summary = neutralize_summary(record.summary)[:_SUMMARY_CHARS]
|
|
290
|
+
if not summary:
|
|
291
|
+
continue
|
|
292
|
+
lines.append(f"[SESSION-MEMORY] - {record.ts[:10]}: {summary}")
|
|
293
|
+
if len(lines) == 1:
|
|
294
|
+
return ""
|
|
295
|
+
backends = ",".join(sorted({r.embedding_backend for r in records})) or "none"
|
|
296
|
+
lines.append(
|
|
297
|
+
f"[SESSION-MEMORY] shown: {len(lines) - 1} turns ({project}),"
|
|
298
|
+
f" backends={backends}"
|
|
299
|
+
)
|
|
300
|
+
return "\n".join(lines)
|
|
301
|
+
except Exception: # recap is best-effort, banner never breaks
|
|
302
|
+
return ""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def main(stdin_json: dict | None = None) -> int:
|
|
306
|
+
if stdin_json is None:
|
|
307
|
+
stdin_json, _ = read_stdin_json()
|
|
308
|
+
cwd = (
|
|
309
|
+
get_str(stdin_json, "cwd")
|
|
310
|
+
or os.environ.get("ARKA_HOOK_CWD", "")
|
|
311
|
+
or os.getcwd()
|
|
312
|
+
)
|
|
313
|
+
try:
|
|
314
|
+
message = build_message(cwd)
|
|
315
|
+
except Exception: # absolute fail-open: static banner, exit 0
|
|
316
|
+
message = _BANNER + "\nOlá, founder (WizardingCode)\nArkaOS"
|
|
317
|
+
print(json.dumps({"systemMessage": message}))
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__": # pragma: no cover
|
|
322
|
+
raise SystemExit(main())
|
package/core/hooks/stop.py
CHANGED
|
@@ -35,6 +35,7 @@ from core.hooks._shared import (
|
|
|
35
35
|
ensure_root_on_path,
|
|
36
36
|
get_str,
|
|
37
37
|
read_stdin_json,
|
|
38
|
+
repo_path,
|
|
38
39
|
resolve_arkaos_root,
|
|
39
40
|
safe_session_id,
|
|
40
41
|
)
|
|
@@ -392,6 +393,67 @@ def _auto_doc_enqueue(
|
|
|
392
393
|
pass
|
|
393
394
|
|
|
394
395
|
|
|
396
|
+
def _session_memory_enabled() -> bool:
|
|
397
|
+
"""``memory.sessionMemory`` (default True) + env kill-switch."""
|
|
398
|
+
if os.environ.get("ARKA_SESSION_MEMORY", "").strip() == "0":
|
|
399
|
+
return False
|
|
400
|
+
config_path = Path.home() / ".arkaos" / "config.json"
|
|
401
|
+
try:
|
|
402
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
403
|
+
except (OSError, json.JSONDecodeError):
|
|
404
|
+
return True
|
|
405
|
+
memory_cfg = data.get("memory") or {}
|
|
406
|
+
return bool(memory_cfg.get("sessionMemory", True))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _enqueue_turn_capture(session_id: str, transcript_path: str, cwd: str) -> None:
|
|
410
|
+
"""F1-A2: fire-and-forget semantic turn capture — runs for EVERY
|
|
411
|
+
turn (before the WF_MARKER gate), all cost paid in the detached
|
|
412
|
+
worker, never on this hook."""
|
|
413
|
+
if not session_id or not transcript_path or not _session_memory_enabled():
|
|
414
|
+
return
|
|
415
|
+
repo = repo_path()
|
|
416
|
+
if not repo or not Path(repo).is_dir():
|
|
417
|
+
return
|
|
418
|
+
try:
|
|
419
|
+
import subprocess
|
|
420
|
+
import sys as _sys
|
|
421
|
+
subprocess.Popen(
|
|
422
|
+
[_sys.executable, "-m", "core.memory.turn_capture",
|
|
423
|
+
session_id, transcript_path, cwd or ""],
|
|
424
|
+
stdout=subprocess.DEVNULL,
|
|
425
|
+
stderr=subprocess.DEVNULL,
|
|
426
|
+
env={**os.environ, "PYTHONPATH": repo},
|
|
427
|
+
start_new_session=True, # same pattern as _enqueue_cognition_capture
|
|
428
|
+
)
|
|
429
|
+
except Exception: # noqa: BLE001 — capture is best-effort, hook never breaks
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _enqueue_routing_rebuild() -> None:
|
|
434
|
+
"""F1-B1: throttled detached rebuild of routing-scores.json (mtime
|
|
435
|
+
> 1h). One stat on the hot path; the aggregation runs detached."""
|
|
436
|
+
try:
|
|
437
|
+
from core.governance.routing_feedback import stale
|
|
438
|
+
if not stale(max_age_seconds=3600):
|
|
439
|
+
return
|
|
440
|
+
repo = repo_path()
|
|
441
|
+
if not repo or not Path(repo).is_dir():
|
|
442
|
+
return
|
|
443
|
+
import subprocess
|
|
444
|
+
import sys as _sys
|
|
445
|
+
subprocess.Popen(
|
|
446
|
+
[_sys.executable, "-m", "core.governance.routing_feedback_cli",
|
|
447
|
+
"rebuild"],
|
|
448
|
+
stdout=subprocess.DEVNULL,
|
|
449
|
+
stderr=subprocess.DEVNULL,
|
|
450
|
+
env={**os.environ, "PYTHONPATH": repo},
|
|
451
|
+
start_new_session=True,
|
|
452
|
+
)
|
|
453
|
+
except Exception: # noqa: BLE001 — rebuild is best-effort, hook never breaks
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
|
|
395
457
|
def main(stdin_json: dict | None = None) -> int:
|
|
396
458
|
if stdin_json is None:
|
|
397
459
|
stdin_json, _ = read_stdin_json()
|
|
@@ -417,6 +479,13 @@ def main(stdin_json: dict | None = None) -> int:
|
|
|
417
479
|
|
|
418
480
|
_native_usage(transcript_path, session_id, raw)
|
|
419
481
|
|
|
482
|
+
try:
|
|
483
|
+
_enqueue_turn_capture(session_id, transcript_path, cwd)
|
|
484
|
+
except Exception: # noqa: BLE001 — never let capture break the hook
|
|
485
|
+
pass
|
|
486
|
+
|
|
487
|
+
_enqueue_routing_rebuild()
|
|
488
|
+
|
|
420
489
|
# Only evaluate sessions where the classifier flagged creation intent.
|
|
421
490
|
wf_marker = arkaos_temp_dir("arkaos-wf-required") / session_id if session_id else None
|
|
422
491
|
if wf_marker is None or not safe_session_id(session_id) or not wf_marker.is_file():
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""SubagentStop — consolidated entrypoint (F2-4, Claude Code reform).
|
|
2
|
+
|
|
3
|
+
Fires when a dispatched subagent (Task tool) finishes. Two jobs, both
|
|
4
|
+
WARN-only (this hook never blocks — the subagent already ran):
|
|
5
|
+
|
|
6
|
+
1. Persist the subagent's final output to the session store so the
|
|
7
|
+
orchestrator's audit trail includes what each specialist produced
|
|
8
|
+
(the ArkaOS QG story: nothing a specialist ships is invisible).
|
|
9
|
+
2. Run the same honesty checks the Stop hook runs on the main turn —
|
|
10
|
+
phantom-action (does the output narrate effects with no tool calls?)
|
|
11
|
+
and meta-tag presence — and, when the output looks deliverable-shaped,
|
|
12
|
+
emit a stderr nudge to route it through the Quality Gate.
|
|
13
|
+
|
|
14
|
+
Telemetry (warn mode, same discipline as the Stop hook): one line per
|
|
15
|
+
subagent to ``~/.arkaos/telemetry/subagent-stop.jsonl``. Gate flag
|
|
16
|
+
``ARKA_SUBAGENT_QA`` = ``warn`` (default) | ``off``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
from datetime import UTC, datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from core.hooks._shared import (
|
|
27
|
+
ensure_root_on_path,
|
|
28
|
+
get_str,
|
|
29
|
+
read_stdin_json,
|
|
30
|
+
resolve_arkaos_root,
|
|
31
|
+
safe_session_id,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_TELEMETRY = Path.home() / ".arkaos" / "telemetry" / "subagent-stop.jsonl"
|
|
35
|
+
# A subagent output "looks deliverable-shaped" when it claims a build/fix
|
|
36
|
+
# a human would want gated — a cheap heuristic, warn-only, never blocks.
|
|
37
|
+
_DELIVERABLE_RE = None # compiled lazily
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _qa_mode() -> str:
|
|
41
|
+
mode = os.environ.get("ARKA_SUBAGENT_QA", "").strip().lower()
|
|
42
|
+
return mode if mode in ("warn", "off") else "warn"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _read_transcript(path: str) -> str | None:
|
|
46
|
+
if not path:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
# errors="replace": an invalid-UTF-8 transcript must not raise
|
|
50
|
+
# UnicodeDecodeError (this hook promises never to block). ValueError
|
|
51
|
+
# guards a null-byte path arriving from stdin JSON.
|
|
52
|
+
return Path(path).read_text(encoding="utf-8", errors="replace")
|
|
53
|
+
except (OSError, ValueError):
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _final_assistant_text(transcript_path: str, raw: str | None) -> str:
|
|
58
|
+
try:
|
|
59
|
+
from core.workflow.flow_enforcer import _load_last_assistant_messages
|
|
60
|
+
|
|
61
|
+
msgs = _load_last_assistant_messages(transcript_path, 1, raw_text=raw)
|
|
62
|
+
return msgs[-1] if msgs else ""
|
|
63
|
+
except Exception: # best-effort — hook never breaks
|
|
64
|
+
return ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _persist_output(session_id: str, agent_id: str, text: str) -> None:
|
|
68
|
+
if not session_id or not text:
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
from core.evals.sanitizer import SanitizerConfigMissing, sanitize_text
|
|
72
|
+
from core.memory.session_store import AgentOutput, SessionStore
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
clean, _counts = sanitize_text(text[:4000])
|
|
76
|
+
except SanitizerConfigMissing:
|
|
77
|
+
clean = "" # no sanitizer config => metadata only (recipes precedent)
|
|
78
|
+
store = SessionStore(session_id)
|
|
79
|
+
store.save_agent_output(AgentOutput(
|
|
80
|
+
agent_id=agent_id or "subagent",
|
|
81
|
+
phase_id="subagent-stop",
|
|
82
|
+
output=clean,
|
|
83
|
+
at=datetime.now(UTC).isoformat(),
|
|
84
|
+
))
|
|
85
|
+
except Exception: # persistence is best-effort
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _looks_deliverable(text: str) -> bool:
|
|
90
|
+
global _DELIVERABLE_RE
|
|
91
|
+
if _DELIVERABLE_RE is None:
|
|
92
|
+
import re
|
|
93
|
+
_DELIVERABLE_RE = re.compile(
|
|
94
|
+
r"\b(implemented|created|added|fixed|refactored|built|wrote|"
|
|
95
|
+
r"shipped|migrated|deployed)\b",
|
|
96
|
+
re.IGNORECASE,
|
|
97
|
+
)
|
|
98
|
+
return bool(_DELIVERABLE_RE.search(text))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_qa(text: str, raw: str | None) -> dict:
|
|
102
|
+
result = {"phantom": "skipped", "meta_tag": "skipped", "deliverable": False}
|
|
103
|
+
try:
|
|
104
|
+
from core.governance.meta_tag_check import check_meta_tag
|
|
105
|
+
from core.governance.phantom_action_check import check_phantom_actions
|
|
106
|
+
|
|
107
|
+
phantom = check_phantom_actions(text, raw)
|
|
108
|
+
result["phantom"] = "pass" if phantom.passed else "phantom-action"
|
|
109
|
+
meta = check_meta_tag(text)
|
|
110
|
+
result["meta_tag"] = "present" if meta.passed else "missing"
|
|
111
|
+
result["deliverable"] = _looks_deliverable(text)
|
|
112
|
+
except Exception: # QA is best-effort — never break the hook
|
|
113
|
+
pass
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _record(session_id: str, agent_id: str, qa: dict) -> None:
|
|
118
|
+
entry = {
|
|
119
|
+
"ts": datetime.now(UTC).isoformat(),
|
|
120
|
+
"mode": "warn",
|
|
121
|
+
"session_id": session_id,
|
|
122
|
+
"agent_id": agent_id,
|
|
123
|
+
**qa,
|
|
124
|
+
}
|
|
125
|
+
try:
|
|
126
|
+
_TELEMETRY.parent.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
with _TELEMETRY.open("a", encoding="utf-8") as fh:
|
|
128
|
+
fh.write(json.dumps(entry) + "\n")
|
|
129
|
+
except OSError:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _nudge(agent_id: str, qa: dict) -> str:
|
|
134
|
+
parts = []
|
|
135
|
+
if qa.get("phantom") == "phantom-action":
|
|
136
|
+
parts.append("narrates effects with no tool calls in the subagent turn")
|
|
137
|
+
if qa.get("meta_tag") == "missing":
|
|
138
|
+
parts.append("no [arka:meta] line")
|
|
139
|
+
if qa.get("deliverable") and parts:
|
|
140
|
+
return (
|
|
141
|
+
f"[arka:subagent-qa] {agent_id or 'subagent'} output looks"
|
|
142
|
+
f" deliverable-shaped but {'; '.join(parts)} — route it through"
|
|
143
|
+
f" the Quality Gate before accepting."
|
|
144
|
+
)
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(stdin_json: dict | None = None) -> int:
|
|
149
|
+
if stdin_json is None:
|
|
150
|
+
stdin_json, _ = read_stdin_json()
|
|
151
|
+
root = resolve_arkaos_root()
|
|
152
|
+
ensure_root_on_path(root)
|
|
153
|
+
|
|
154
|
+
session_id = get_str(stdin_json, "session_id")
|
|
155
|
+
if session_id and not safe_session_id(session_id):
|
|
156
|
+
return 0
|
|
157
|
+
agent_id = get_str(stdin_json, "subagent_type") or get_str(stdin_json, "agent_type")
|
|
158
|
+
transcript_path = get_str(stdin_json, "transcript_path")
|
|
159
|
+
|
|
160
|
+
if _qa_mode() == "off":
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
raw = _read_transcript(transcript_path)
|
|
164
|
+
text = _final_assistant_text(transcript_path, raw)
|
|
165
|
+
if not text:
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
_persist_output(session_id, agent_id, text)
|
|
169
|
+
qa = _run_qa(text, raw)
|
|
170
|
+
_record(session_id, agent_id, qa)
|
|
171
|
+
|
|
172
|
+
# The nudge must reach the ORCHESTRATOR. Claude Code discards hook
|
|
173
|
+
# stderr at exit 0 (it only surfaces stderr on a deny/exit 2), so a
|
|
174
|
+
# stderr nudge here would be inert. additionalContext on stdout is the
|
|
175
|
+
# channel the model actually receives.
|
|
176
|
+
nudge = _nudge(agent_id, qa)
|
|
177
|
+
if nudge:
|
|
178
|
+
print(json.dumps({"hookSpecificOutput": {
|
|
179
|
+
"hookEventName": "SubagentStop",
|
|
180
|
+
"additionalContext": nudge,
|
|
181
|
+
}}))
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__": # pragma: no cover
|
|
186
|
+
raise SystemExit(main())
|
|
@@ -207,7 +207,7 @@ def _invalidate_turn_caches(session_id: str) -> None:
|
|
|
207
207
|
# ─── Section 4: Synapse bridge (in-process) ──────────────────────────────
|
|
208
208
|
|
|
209
209
|
|
|
210
|
-
def _run_bridge(root: str, user_input: str, session_id: str) -> str:
|
|
210
|
+
def _run_bridge(root: str, user_input: str, session_id: str, cwd: str = "") -> str:
|
|
211
211
|
bridge_path = Path(root) / "scripts" / "synapse-bridge.py"
|
|
212
212
|
if not bridge_path.is_file() or not Path(root).is_dir():
|
|
213
213
|
return ""
|
|
@@ -221,10 +221,12 @@ def _run_bridge(root: str, user_input: str, session_id: str) -> str:
|
|
|
221
221
|
# The old hook piped bridge stderr to /dev/null — keep it quiet.
|
|
222
222
|
with contextlib.redirect_stderr(io.StringIO()):
|
|
223
223
|
spec.loader.exec_module(module)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
payload = {"user_input": user_input, "session_id": session_id}
|
|
225
|
+
if cwd:
|
|
226
|
+
# Explicit hook cwd — L9.5 scopes cross-session memory by
|
|
227
|
+
# project; an unscoped search would leak across clients.
|
|
228
|
+
payload["cwd"] = cwd
|
|
229
|
+
output, code = module.run_bridge(payload, Path(root))
|
|
228
230
|
if code == 0:
|
|
229
231
|
return str(output.get("context_string", ""))
|
|
230
232
|
except Exception:
|
|
@@ -573,7 +575,9 @@ def main(stdin_json: dict | None = None, raw: str = "") -> int:
|
|
|
573
575
|
user_input = raw[:2000]
|
|
574
576
|
|
|
575
577
|
# ─── Synapse bridge + tags ───────────────────────────────────────
|
|
576
|
-
python_result = _run_bridge(
|
|
578
|
+
python_result = _run_bridge(
|
|
579
|
+
root, user_input, session_id, get_str(stdin_json, "cwd")
|
|
580
|
+
)
|
|
577
581
|
if python_result:
|
|
578
582
|
wf_tag = _workflow_tag()
|
|
579
583
|
if wf_tag:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|