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,366 @@
|
|
|
1
|
+
"""Gate manifest generator — the single source the Node fast-path trusts.
|
|
2
|
+
|
|
3
|
+
F2-6 (hook fast-path): ``config/hooks/pre-tool-use.cjs`` and
|
|
4
|
+
``post-tool-use.cjs`` short-circuit the trivial hook decisions in ~18ms
|
|
5
|
+
p50 (measured — the Node startup floor is ~10ms on the reference
|
|
6
|
+
machine) instead of spawning the 82-96ms bash->Python chain on every
|
|
7
|
+
tool call.
|
|
8
|
+
The shims decide from ``config/gate-manifest.json`` ONLY — never from
|
|
9
|
+
constants of their own — and this module is the only writer of that
|
|
10
|
+
file: every value is imported from the real gate modules at generation
|
|
11
|
+
time (precedent: ``scripts/tools/docs_stats.py`` + its consistency
|
|
12
|
+
test; hand-typed copies are how the v4.3.2 drift happened).
|
|
13
|
+
|
|
14
|
+
Regenerate after touching any source constant:
|
|
15
|
+
|
|
16
|
+
python -m core.hooks.gate_manifest
|
|
17
|
+
|
|
18
|
+
``tests/python/test_gate_manifest_parity.py`` fails when the committed
|
|
19
|
+
manifest drifts from these imports, and executes the embedded corpora
|
|
20
|
+
against the REAL Python functions; ``tests/installer/gate-manifest.test.js``
|
|
21
|
+
executes the same corpora against the Node engine. Drift on either side
|
|
22
|
+
breaks the build.
|
|
23
|
+
|
|
24
|
+
Scope contract: the manifest carries ONLY what the shims consume. The
|
|
25
|
+
shims have no deny path — they fast-allow a closed, corpus-proven set
|
|
26
|
+
or delegate to the Python chain — so gate internals that always
|
|
27
|
+
delegate (specialist globs, frontend markers, plan approval) stay out.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import re
|
|
34
|
+
from dataclasses import asdict
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
_SCHEMA_VERSION = 1
|
|
38
|
+
|
|
39
|
+
# Python-only regex constructs that would compile differently (or not at
|
|
40
|
+
# all) under JavaScript's RegExp. The generator refuses to export any
|
|
41
|
+
# pattern containing them — porting is a conscious decision, not a
|
|
42
|
+
# silent one.
|
|
43
|
+
_JS_INCOMPATIBLE = ("(?P", "(?#", "(?(", "\\A", "\\Z")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _js_pattern(compiled: re.Pattern) -> dict:
|
|
47
|
+
"""Export a compiled Python regex as a {py, js, flags} entry."""
|
|
48
|
+
pattern = compiled.pattern
|
|
49
|
+
for construct in _JS_INCOMPATIBLE:
|
|
50
|
+
if construct in pattern:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"regex {pattern!r} uses {construct!r} — not portable to "
|
|
53
|
+
f"JS; port it consciously before exporting"
|
|
54
|
+
)
|
|
55
|
+
flags = "i" if compiled.flags & re.IGNORECASE else ""
|
|
56
|
+
return {"py": pattern, "js": pattern, "flags": flags}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _relative_to_home(path: Path) -> str:
|
|
60
|
+
return str(path.relative_to(Path.home()))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _bash_corpus() -> list[dict]:
|
|
64
|
+
"""Golden Bash classifications. ``expect`` is asserted against the
|
|
65
|
+
REAL ``bash_is_effect`` by the pytest side; the node:test side
|
|
66
|
+
asserts the engine returns ``engine_expect`` (defaults to
|
|
67
|
+
``expect``). ``engine_expect`` may only ever be MORE conservative
|
|
68
|
+
("effect" where Python says "discovery" — the engine delegates and
|
|
69
|
+
Python re-classifies); the node suite enforces that one-sidedness
|
|
70
|
+
structurally. The corpus cannot drift alone."""
|
|
71
|
+
cases: list[tuple] = [
|
|
72
|
+
("git status", "discovery"),
|
|
73
|
+
("git log --oneline -5", "discovery"),
|
|
74
|
+
("git commit -m 'x'", "effect"),
|
|
75
|
+
("git push origin master", "effect"),
|
|
76
|
+
("ls -la /tmp", "discovery"),
|
|
77
|
+
("echo hello", "discovery"),
|
|
78
|
+
("echo hello > out.txt", "effect"),
|
|
79
|
+
("cat f.txt | grep needle", "discovery"),
|
|
80
|
+
("FOO=1 BAR=2 grep -r needle .", "discovery"),
|
|
81
|
+
("FOO=1 rm -rf /tmp/x", "effect"),
|
|
82
|
+
("rsync -av --dry-run a/ b/", "effect"),
|
|
83
|
+
("unknowncmd --version", "effect"),
|
|
84
|
+
("python3 -m pytest tests/", "discovery"),
|
|
85
|
+
("npm run build", "discovery"),
|
|
86
|
+
("npm install lodash", "effect"),
|
|
87
|
+
("docker ps", "discovery"),
|
|
88
|
+
("docker build -t x .", "effect"),
|
|
89
|
+
("kill -9 12345", "effect"),
|
|
90
|
+
("sed -i '' -e s/a/b/ f", "effect"),
|
|
91
|
+
("curl -s https://example.com", "discovery"),
|
|
92
|
+
("grep 'a > b' file.txt", "effect"), # redirect regex sees quotes
|
|
93
|
+
("", "discovery"),
|
|
94
|
+
(" ", "discovery"),
|
|
95
|
+
("touch marker", "effect"),
|
|
96
|
+
("mkdir -p build", "effect"),
|
|
97
|
+
# QG B1 (redo 1) — whitespace-class boundary. U+FEFF is NOT
|
|
98
|
+
# whitespace to Python (isspace()=False, re \s no-match): the
|
|
99
|
+
# token "FOO=1<U+FEFF>grep" stays glued → unknown → EFFECT. JS
|
|
100
|
+
# \s/trim/split disagree, so the engine carries an explicit
|
|
101
|
+
# non-ASCII-whitespace guard that forces EFFECT (delegation).
|
|
102
|
+
("FOO=1\ufeffgrep needle file", "effect"),
|
|
103
|
+
("FOO=1\ufeffFOO=2\ufeffgrep x", "effect"),
|
|
104
|
+
("FOO=1 \ufeff grep x", "effect"),
|
|
105
|
+
# NBSP IS whitespace to both sides, so Python classifies this
|
|
106
|
+
# discovery — but the engine's guard still forces EFFECT
|
|
107
|
+
# (delegate), the allowed conservative direction.
|
|
108
|
+
("FOO=1\u00a0grep x", "discovery", "effect"),
|
|
109
|
+
]
|
|
110
|
+
rows = []
|
|
111
|
+
for case in cases:
|
|
112
|
+
row = {"cmd": case[0], "expect": case[1]}
|
|
113
|
+
if len(case) > 2:
|
|
114
|
+
row["engine_expect"] = case[2]
|
|
115
|
+
rows.append(row)
|
|
116
|
+
return rows
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _pre_tool_corpus() -> list[dict]:
|
|
120
|
+
"""Golden PreToolUse routing: fast_allow vs delegate per tool name."""
|
|
121
|
+
cases = [
|
|
122
|
+
("Read", "fast_allow"),
|
|
123
|
+
("Grep", "fast_allow"),
|
|
124
|
+
("Glob", "fast_allow"),
|
|
125
|
+
("TodoWrite", "fast_allow"),
|
|
126
|
+
("ExitPlanMode", "fast_allow"),
|
|
127
|
+
("Agent", "fast_allow"),
|
|
128
|
+
("mcp__obsidian__search_notes", "fast_allow"),
|
|
129
|
+
("mcp__supabase__list_tables", "fast_allow"),
|
|
130
|
+
("", "fast_allow"),
|
|
131
|
+
("WebSearch", "delegate"),
|
|
132
|
+
("WebFetch", "delegate"),
|
|
133
|
+
("mcp__context7__query-docs", "delegate"),
|
|
134
|
+
("mcp__firecrawl__firecrawl_scrape", "delegate"),
|
|
135
|
+
("Write", "delegate"),
|
|
136
|
+
("Edit", "delegate"),
|
|
137
|
+
("MultiEdit", "delegate"),
|
|
138
|
+
("NotebookEdit", "delegate"),
|
|
139
|
+
("Task", "delegate"),
|
|
140
|
+
("Skill", "delegate"),
|
|
141
|
+
]
|
|
142
|
+
return [{"tool": tool, "expect": expect} for tool, expect in cases]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _session_id_corpus() -> list[dict]:
|
|
146
|
+
cases = [
|
|
147
|
+
("abc-123", True),
|
|
148
|
+
("a.b_c-D9", True),
|
|
149
|
+
("a" * 128, True),
|
|
150
|
+
("a" * 129, False),
|
|
151
|
+
(".", False),
|
|
152
|
+
("..", False),
|
|
153
|
+
("...", False),
|
|
154
|
+
("a/../b", False),
|
|
155
|
+
("a b", False),
|
|
156
|
+
("", False),
|
|
157
|
+
("sessão", False),
|
|
158
|
+
]
|
|
159
|
+
return [{"sid": sid, "expect": expect} for sid, expect in cases]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _error_trigger_corpus() -> list[dict]:
|
|
163
|
+
cases = [
|
|
164
|
+
("Error: something broke", True),
|
|
165
|
+
("fatal: not a git repository", True),
|
|
166
|
+
("2 tests failed", True),
|
|
167
|
+
("ENOENT: no such file", True),
|
|
168
|
+
("panic: runtime error", True),
|
|
169
|
+
("all 14 tests passed", False),
|
|
170
|
+
("ok", False),
|
|
171
|
+
("", False),
|
|
172
|
+
]
|
|
173
|
+
return [{"output": output, "expect": expect} for output, expect in cases]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _tool_sets() -> dict:
|
|
177
|
+
from core.hooks import pre_tool_use
|
|
178
|
+
from core.workflow import flow_enforcer, research_gate
|
|
179
|
+
|
|
180
|
+
if pre_tool_use._FLOW_GATED_TOOLS != flow_enforcer.EFFECT_TOOLS_ALWAYS | {"Bash"}:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
"pre_tool_use._FLOW_GATED_TOOLS no longer equals "
|
|
183
|
+
"flow_enforcer.EFFECT_TOOLS_ALWAYS + Bash — re-derive the "
|
|
184
|
+
"shim decision table before regenerating"
|
|
185
|
+
)
|
|
186
|
+
return {
|
|
187
|
+
"flow_gated": sorted(pre_tool_use._FLOW_GATED_TOOLS),
|
|
188
|
+
"effect_always": sorted(flow_enforcer.EFFECT_TOOLS_ALWAYS),
|
|
189
|
+
"research_external": sorted(research_gate.RESEARCH_EXTERNAL_TOOLS),
|
|
190
|
+
"post_delegate_always": ["Agent", "ExitPlanMode", "Task"],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _bash_classifier() -> dict:
|
|
195
|
+
from core.workflow import flow_enforcer
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"discovery_first_tokens": sorted(flow_enforcer._BASH_DISCOVERY_FIRST),
|
|
199
|
+
"effect_patterns": [
|
|
200
|
+
_js_pattern(p) for p in flow_enforcer._BASH_EFFECT_PATTERNS
|
|
201
|
+
],
|
|
202
|
+
"eval_order": "blacklist-then-whitelist",
|
|
203
|
+
"strip_var_prefix": True,
|
|
204
|
+
"unknown_first_token": "effect",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _telemetry_contract() -> dict:
|
|
209
|
+
from core.workflow import flow_enforcer, research_gate
|
|
210
|
+
|
|
211
|
+
kb_template = asdict(
|
|
212
|
+
research_gate.Decision(allow=True, reason="tool-not-gated")
|
|
213
|
+
)
|
|
214
|
+
flow_template = asdict(
|
|
215
|
+
flow_enforcer.Decision(allow=True, reason="tool-not-gated")
|
|
216
|
+
)
|
|
217
|
+
return {
|
|
218
|
+
# datetime.now(timezone.utc).isoformat() — microseconds, +00:00
|
|
219
|
+
# offset. The Node side emits the same shape (ms padded to 6
|
|
220
|
+
# digits); parity test asserts datetime.fromisoformat parses it.
|
|
221
|
+
"ts_format": "python-utc-isoformat",
|
|
222
|
+
"kb_first_prefix_keys": ["ts", "session_id", "tool"],
|
|
223
|
+
"kb_first_template": kb_template,
|
|
224
|
+
"enforcement_prefix_keys": ["ts", "session_id", "tool", "cwd"],
|
|
225
|
+
"enforcement_template": flow_template,
|
|
226
|
+
"mcp_keys": ["ts", "server", "tool", "session"],
|
|
227
|
+
"mcp_prefix": "mcp__",
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _flags_and_budget() -> dict:
|
|
232
|
+
from core.workflow import flow_enforcer
|
|
233
|
+
|
|
234
|
+
if not flow_enforcer.CONFIG_PATH.name == "config.json":
|
|
235
|
+
raise ValueError("flow_enforcer.CONFIG_PATH moved — update manifest")
|
|
236
|
+
return {
|
|
237
|
+
"hardEnforcement": {
|
|
238
|
+
"path": ["hooks", "hardEnforcement"],
|
|
239
|
+
# flow_enforcer._feature_flag_on: missing file → False,
|
|
240
|
+
# corrupt JSON → False, value coerced with PYTHON truthiness
|
|
241
|
+
# ("false" string is ON, [] and {} are OFF — JS Boolean()
|
|
242
|
+
# disagrees on the last two; engine.cjs pythonTruthy() pins it).
|
|
243
|
+
"on_missing_file": False,
|
|
244
|
+
"on_corrupt": False,
|
|
245
|
+
"coercion": "python-truthy",
|
|
246
|
+
},
|
|
247
|
+
"budget": {
|
|
248
|
+
"section": "budget",
|
|
249
|
+
"cap_keys": ["hardCapUsd", "dailyCapUsd"],
|
|
250
|
+
# cost_governor semantics (cost_governor.py:68-86,126-131):
|
|
251
|
+
# missing/corrupt config → {} → no-cap → allow with zero
|
|
252
|
+
# reads/writes; a cap is ACTIVE only when float(value) > 0.
|
|
253
|
+
"on_missing_file": "no-cap",
|
|
254
|
+
"on_corrupt": "no-cap",
|
|
255
|
+
"active_when": "float(value) > 0",
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _state_and_paths() -> dict:
|
|
261
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
262
|
+
from core.workflow import flow_authorization, flow_enforcer, research_gate
|
|
263
|
+
from core.runtime import mcp_telemetry
|
|
264
|
+
|
|
265
|
+
auth_dir = arkaos_temp_dir("arkaos-flow-auth")
|
|
266
|
+
if str(auth_dir.parent) != "/tmp": # pragma: no cover — POSIX only
|
|
267
|
+
raise ValueError("manifest generation is POSIX-only (tmp base)")
|
|
268
|
+
return {
|
|
269
|
+
"state_dirs": {
|
|
270
|
+
"tmp_base_posix": "/tmp",
|
|
271
|
+
"flow_auth_dirname": auth_dir.name,
|
|
272
|
+
"flow_auth_file": "{sid}.json",
|
|
273
|
+
"flow_auth_env_override": "ARKA_FLOW_AUTH_DIR",
|
|
274
|
+
},
|
|
275
|
+
"home_paths": {
|
|
276
|
+
"config": ".arkaos/config.json",
|
|
277
|
+
"telemetry_kb_first": _relative_to_home(research_gate.TELEMETRY_PATH),
|
|
278
|
+
"telemetry_enforcement": _relative_to_home(
|
|
279
|
+
flow_enforcer.TELEMETRY_PATH
|
|
280
|
+
),
|
|
281
|
+
"telemetry_mcp": _relative_to_home(mcp_telemetry.DEFAULT_PATH),
|
|
282
|
+
},
|
|
283
|
+
"numbers": {
|
|
284
|
+
"assistant_window": flow_enforcer.ASSISTANT_WINDOW,
|
|
285
|
+
"grace_cap": flow_authorization.GRACE_CAP,
|
|
286
|
+
"auth_ttl_seconds": flow_authorization.DEFAULT_TTL_SECONDS,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def build_manifest() -> dict:
|
|
292
|
+
"""Assemble the manifest from the real gate constants (import-time)."""
|
|
293
|
+
from core.hooks import post_tool_use, pre_tool_use # noqa: F401
|
|
294
|
+
from core.shared import safe_session_id as ssid
|
|
295
|
+
|
|
296
|
+
state = _state_and_paths()
|
|
297
|
+
if pre_tool_use._ASSISTANT_WINDOW != state["numbers"]["assistant_window"]:
|
|
298
|
+
raise ValueError("ASSISTANT_WINDOW diverged between hook and enforcer")
|
|
299
|
+
return {
|
|
300
|
+
"schema_version": _SCHEMA_VERSION,
|
|
301
|
+
"platform": "posix",
|
|
302
|
+
"source_modules": [
|
|
303
|
+
"core.hooks.post_tool_use",
|
|
304
|
+
"core.hooks.pre_tool_use",
|
|
305
|
+
"core.runtime.cost_governor",
|
|
306
|
+
"core.runtime.mcp_telemetry",
|
|
307
|
+
"core.shared.safe_session_id",
|
|
308
|
+
"core.shared.temp_paths",
|
|
309
|
+
"core.workflow.flow_authorization",
|
|
310
|
+
"core.workflow.flow_enforcer",
|
|
311
|
+
"core.workflow.research_gate",
|
|
312
|
+
],
|
|
313
|
+
"tools": _tool_sets(),
|
|
314
|
+
"bash": _bash_classifier(),
|
|
315
|
+
"post": {
|
|
316
|
+
"error_trigger": _js_pattern(post_tool_use._ERROR_TRIGGER_RE),
|
|
317
|
+
"success_exit_codes": ["0", ""],
|
|
318
|
+
},
|
|
319
|
+
"session_id": {
|
|
320
|
+
**_js_pattern(ssid.SAFE_SESSION_ID_RE),
|
|
321
|
+
"reject_dot_only": True,
|
|
322
|
+
},
|
|
323
|
+
"flags": _flags_and_budget(),
|
|
324
|
+
"telemetry": _telemetry_contract(),
|
|
325
|
+
**state,
|
|
326
|
+
"io_contract": {
|
|
327
|
+
"pre": {"allow_stdout": "", "fail_open_exit": 0},
|
|
328
|
+
"post": {"success_stdout": "{}", "fail_open_stdout": "{}",
|
|
329
|
+
"fail_open_exit": 0},
|
|
330
|
+
},
|
|
331
|
+
"env": {
|
|
332
|
+
"kill_switch": "ARKA_HOOK_FASTPATH",
|
|
333
|
+
"kill_switch_off_value": "0",
|
|
334
|
+
},
|
|
335
|
+
"corpora": {
|
|
336
|
+
"bash": _bash_corpus(),
|
|
337
|
+
"pre_tools": _pre_tool_corpus(),
|
|
338
|
+
"session_ids": _session_id_corpus(),
|
|
339
|
+
"error_trigger": _error_trigger_corpus(),
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def manifest_path() -> Path:
|
|
345
|
+
# Sibling of the .cjs shims (config/hooks/) so the __dirname
|
|
346
|
+
# resolution works identically in-repo and deployed — the shims
|
|
347
|
+
# never resolve ARKAOS_ROOT (design R7).
|
|
348
|
+
return (
|
|
349
|
+
Path(__file__).resolve().parents[2]
|
|
350
|
+
/ "config" / "hooks" / "gate-manifest.json"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def render() -> str:
|
|
355
|
+
return json.dumps(build_manifest(), indent=2, sort_keys=True) + "\n"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def main() -> int:
|
|
359
|
+
path = manifest_path()
|
|
360
|
+
path.write_text(render(), encoding="utf-8")
|
|
361
|
+
print(f"gate-manifest written: {path}")
|
|
362
|
+
return 0
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if __name__ == "__main__":
|
|
366
|
+
raise SystemExit(main())
|
|
@@ -171,10 +171,13 @@ def _record_pattern_stub(tool_output: str, prompt: str) -> None:
|
|
|
171
171
|
try:
|
|
172
172
|
from core.knowledge.pattern_cards import (
|
|
173
173
|
PatternCard,
|
|
174
|
-
query_patterns,
|
|
175
174
|
record_pattern,
|
|
175
|
+
reinforce_pattern,
|
|
176
176
|
)
|
|
177
|
-
|
|
177
|
+
# An APPROVED verdict on an EXISTING card is real usage evidence —
|
|
178
|
+
# reinforce it AND return: falling through to record_pattern would
|
|
179
|
+
# REPLACE an enriched card with this fresh empty stub (data loss).
|
|
180
|
+
if reinforce_pattern(pid):
|
|
178
181
|
return
|
|
179
182
|
ts = datetime.now(timezone.utc).isoformat()
|
|
180
183
|
record_pattern(PatternCard(
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""SessionEnd — consolidated entrypoint (F2-4, Claude Code reform).
|
|
2
|
+
|
|
3
|
+
Fires when a Claude Code session ends cleanly. Writes a final session
|
|
4
|
+
digest immediately — the PreCompact hook only fires on context
|
|
5
|
+
compaction, so a short session that never compacts left no record. Also
|
|
6
|
+
marks the session ended in the session store.
|
|
7
|
+
|
|
8
|
+
WARN/best-effort only: never blocks, always exits 0. The digest is
|
|
9
|
+
sanitized (recipes precedent) and content-hashed for dedup, same shape
|
|
10
|
+
as the PreCompact digest.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from core.hooks._shared import (
|
|
20
|
+
ensure_root_on_path,
|
|
21
|
+
get_str,
|
|
22
|
+
read_stdin_json,
|
|
23
|
+
resolve_arkaos_root,
|
|
24
|
+
safe_session_id,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_DIGEST_DIR = Path.home() / ".arkaos" / "session-digests"
|
|
28
|
+
_TAIL_MSGS = 5
|
|
29
|
+
_MSG_CHARS = 500
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_transcript(path: str) -> str | None:
|
|
33
|
+
if not path:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
# errors="replace" + ValueError: never raise on an invalid-UTF-8
|
|
37
|
+
# transcript or a null-byte path (this hook promises exit 0 always,
|
|
38
|
+
# and write_digest must reach _end_session).
|
|
39
|
+
return Path(path).read_text(encoding="utf-8", errors="replace")
|
|
40
|
+
except (OSError, ValueError):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _last_assistant_texts(transcript_path: str, raw: str | None) -> list[str]:
|
|
45
|
+
try:
|
|
46
|
+
from core.workflow.flow_enforcer import _load_last_assistant_messages
|
|
47
|
+
|
|
48
|
+
return _load_last_assistant_messages(transcript_path, _TAIL_MSGS, raw_text=raw)
|
|
49
|
+
except Exception:
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _sanitize(text: str) -> str:
|
|
54
|
+
try:
|
|
55
|
+
from core.evals.sanitizer import SanitizerConfigMissing, sanitize_text
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
clean, _counts = sanitize_text(text)
|
|
59
|
+
return clean
|
|
60
|
+
except SanitizerConfigMissing:
|
|
61
|
+
return "" # no config => omit the excerpt, keep the digest metadata
|
|
62
|
+
except Exception:
|
|
63
|
+
return ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _digest_id(session_id: str, seed: str) -> str:
|
|
67
|
+
basis = f"{session_id or 'default'}-{seed[:200]}".encode("utf-8", "replace")
|
|
68
|
+
return hashlib.sha256(basis).hexdigest()[:16]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_digest(session_id: str, transcript_path: str) -> Path | None:
|
|
72
|
+
raw = _read_transcript(transcript_path)
|
|
73
|
+
messages = _last_assistant_texts(transcript_path, raw)
|
|
74
|
+
seed = messages[-1] if messages else ""
|
|
75
|
+
digest_id = _digest_id(session_id, seed)
|
|
76
|
+
short = (session_id or "default")[:8]
|
|
77
|
+
sanitized = [_sanitize(m[:_MSG_CHARS]) for m in messages]
|
|
78
|
+
excerpt = "\n\n".join(s for s in sanitized if s) \
|
|
79
|
+
or "(no sanitizable transcript excerpt)"
|
|
80
|
+
body = (
|
|
81
|
+
"---\n"
|
|
82
|
+
"type: session-digest\n"
|
|
83
|
+
f"session_id: {session_id}\n"
|
|
84
|
+
f"digest_id: {digest_id}\n"
|
|
85
|
+
"trigger: session-end\n"
|
|
86
|
+
f"ended_at: {datetime.now(UTC).isoformat()}\n"
|
|
87
|
+
"---\n\n"
|
|
88
|
+
f"# Session Digest — {digest_id}\n\n"
|
|
89
|
+
"## Closing messages (sanitized)\n\n"
|
|
90
|
+
f"{excerpt}\n"
|
|
91
|
+
)
|
|
92
|
+
try:
|
|
93
|
+
_DIGEST_DIR.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
path = _DIGEST_DIR / f"digest-{short}-{digest_id}.md"
|
|
95
|
+
path.write_text(body, encoding="utf-8")
|
|
96
|
+
return path
|
|
97
|
+
except OSError:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _end_session(session_id: str) -> None:
|
|
102
|
+
try:
|
|
103
|
+
from core.memory.session_store import SessionStore
|
|
104
|
+
|
|
105
|
+
SessionStore(session_id).end_session()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(stdin_json: dict | None = None) -> int:
|
|
111
|
+
if stdin_json is None:
|
|
112
|
+
stdin_json, _ = read_stdin_json()
|
|
113
|
+
root = resolve_arkaos_root()
|
|
114
|
+
ensure_root_on_path(root)
|
|
115
|
+
|
|
116
|
+
session_id = get_str(stdin_json, "session_id")
|
|
117
|
+
if session_id and not safe_session_id(session_id):
|
|
118
|
+
return 0
|
|
119
|
+
transcript_path = get_str(stdin_json, "transcript_path")
|
|
120
|
+
|
|
121
|
+
write_digest(session_id, transcript_path)
|
|
122
|
+
_end_session(session_id)
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__": # pragma: no cover
|
|
127
|
+
raise SystemExit(main())
|