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
package/config/mcp-policy.yaml
CHANGED
|
@@ -14,6 +14,7 @@ policies:
|
|
|
14
14
|
active:
|
|
15
15
|
- codebase-memory
|
|
16
16
|
- arka-prompts
|
|
17
|
+
- arka-tools
|
|
17
18
|
- obsidian
|
|
18
19
|
- context7
|
|
19
20
|
- gh-grep
|
|
@@ -46,6 +47,7 @@ policies:
|
|
|
46
47
|
active:
|
|
47
48
|
- codebase-memory
|
|
48
49
|
- arka-prompts
|
|
50
|
+
- arka-tools
|
|
49
51
|
- obsidian
|
|
50
52
|
- context7
|
|
51
53
|
- gh-grep
|
|
@@ -78,6 +80,7 @@ policies:
|
|
|
78
80
|
active:
|
|
79
81
|
- codebase-memory
|
|
80
82
|
- arka-prompts
|
|
83
|
+
- arka-tools
|
|
81
84
|
- obsidian
|
|
82
85
|
- context7
|
|
83
86
|
- gh-grep
|
|
@@ -109,6 +112,7 @@ policies:
|
|
|
109
112
|
stack_includes: [shopify]
|
|
110
113
|
active:
|
|
111
114
|
- arka-prompts
|
|
115
|
+
- arka-tools
|
|
112
116
|
- obsidian
|
|
113
117
|
- context7
|
|
114
118
|
- gh-grep
|
|
@@ -142,6 +146,7 @@ policies:
|
|
|
142
146
|
active:
|
|
143
147
|
- codebase-memory
|
|
144
148
|
- arka-prompts
|
|
149
|
+
- arka-tools
|
|
145
150
|
- obsidian
|
|
146
151
|
- context7
|
|
147
152
|
- gh-grep
|
|
@@ -173,6 +178,7 @@ policies:
|
|
|
173
178
|
ecosystem: marketing
|
|
174
179
|
active:
|
|
175
180
|
- arka-prompts
|
|
181
|
+
- arka-tools
|
|
176
182
|
- obsidian
|
|
177
183
|
- context7
|
|
178
184
|
- canva
|
|
@@ -205,6 +211,7 @@ policies:
|
|
|
205
211
|
ecosystem: content
|
|
206
212
|
active:
|
|
207
213
|
- arka-prompts
|
|
214
|
+
- arka-tools
|
|
208
215
|
- obsidian
|
|
209
216
|
- context7
|
|
210
217
|
- canva
|
|
@@ -237,6 +244,7 @@ policies:
|
|
|
237
244
|
default: true
|
|
238
245
|
active:
|
|
239
246
|
- arka-prompts
|
|
247
|
+
- arka-tools
|
|
240
248
|
- obsidian
|
|
241
249
|
- context7
|
|
242
250
|
- gh-grep
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Curated default skill set — the single source of truth for the F2-7
|
|
2
|
+
# distribution cut (marketplace plugins now, curated installer default in
|
|
3
|
+
# F2-7c). Everything downstream is GENERATED from this file plus the
|
|
4
|
+
# departments/ tree by scripts/marketplace_gen.py — never hand-typed
|
|
5
|
+
# (docs_stats/gate_manifest precedent).
|
|
6
|
+
#
|
|
7
|
+
# The cut: the default install carries the structural surface (main
|
|
8
|
+
# /arka, the 17 department hubs, the 14 meta skills — 32 in total) plus
|
|
9
|
+
# the curated sub-skills below (37) = 69 skills, inside the 60-80 band.
|
|
10
|
+
# Every OTHER sub-skill still ships in the repo and deploys via the
|
|
11
|
+
# per-department marketplace plugins (arkaos-<dept>@arkaos), where the
|
|
12
|
+
# plugin namespace also disambiguates the later-wins collision slugs —
|
|
13
|
+
# which are BANNED from this list (enforced by test, computed from disk).
|
|
14
|
+
#
|
|
15
|
+
# Operator-tunable: edit, then run `python scripts/marketplace_gen.py`
|
|
16
|
+
# and commit the regenerated tree. The drift gate fails CI otherwise.
|
|
17
|
+
version: 1
|
|
18
|
+
structural:
|
|
19
|
+
arka: true # main /arka skill + nested reference bundle
|
|
20
|
+
dept_hubs: all # departments/<dept>/SKILL.md (17)
|
|
21
|
+
meta: all # arka/skills/* (14 — includes flow: NON-NEGOTIABLE)
|
|
22
|
+
curated_subskills:
|
|
23
|
+
brand: [design-review, design-system]
|
|
24
|
+
community: [moderation]
|
|
25
|
+
content: [calendar, hook-write]
|
|
26
|
+
dev: [api-design, architecture-design, clean-code-review, code-review,
|
|
27
|
+
db-design, refactor-plan, runbook, security-audit, spec, tdd-cycle]
|
|
28
|
+
ecom: [cart-recovery, store-audit]
|
|
29
|
+
finance: [financial-model, unit-economics]
|
|
30
|
+
kb: [search-kb, zettelkasten-process]
|
|
31
|
+
landing: [funnel-design, landing-gen]
|
|
32
|
+
leadership: [feedback-give]
|
|
33
|
+
marketing: [growth-loop, seo-audit]
|
|
34
|
+
ops: [sop-create, workflow-automate]
|
|
35
|
+
org: [org-design]
|
|
36
|
+
pm: [roadmap-build, sprint-plan]
|
|
37
|
+
saas: [mvp-build, plg-setup]
|
|
38
|
+
sales: [discovery-call, objection-handle]
|
|
39
|
+
strategy: [five-forces, moat-analysis]
|
package/config/statusline.sh
CHANGED
|
@@ -117,6 +117,61 @@ fi
|
|
|
117
117
|
# ─── Format cost ──────────────────────────────────────────────────────────
|
|
118
118
|
COST_FMT=$(printf '$%.2f' "${COST:-0}")
|
|
119
119
|
|
|
120
|
+
# ─── Workflow gate + budget (F2-5, statusline v3) ─────────────────────────
|
|
121
|
+
# Read ~/.arkaos/workflow-state.json directly (plain JSON, one jq call) —
|
|
122
|
+
# no Python spawn on this hot path (the statusline re-renders continuously).
|
|
123
|
+
# Shows the active workflow, current gate as G<n>/<total>, and violations.
|
|
124
|
+
WF_SEGMENT=""
|
|
125
|
+
WF_STATE="$HOME/.arkaos/workflow-state.json"
|
|
126
|
+
if [ -f "$WF_STATE" ]; then
|
|
127
|
+
WF_LINE=$(jq -r '
|
|
128
|
+
(.phases // {}) as $p
|
|
129
|
+
| ($p | length) as $total
|
|
130
|
+
# No workflow name, or a phase-less (degenerate) state -> no segment
|
|
131
|
+
# (avoids a meaningless "G0/0").
|
|
132
|
+
| if (.workflow // "") == "" or $total == 0 then empty
|
|
133
|
+
else
|
|
134
|
+
([$p[] | select(.status == "completed")] | length) as $done
|
|
135
|
+
# Gate index = position of the in_progress phase (1-based), else
|
|
136
|
+
# done+1 — DERIVED FROM POSITION, never grepped from the name
|
|
137
|
+
# (a "gate-5-x" key in a 3-phase workflow must not render G5/3).
|
|
138
|
+
# Clamped to $total so a fully-completed workflow shows G<n>/<n>,
|
|
139
|
+
# not G<n+1>/<n>.
|
|
140
|
+
| ([$p | to_entries | to_entries[]
|
|
141
|
+
| select(.value.value.status == "in_progress") | .key + 1][0]
|
|
142
|
+
// ([$done + 1, $total] | min)) as $gate
|
|
143
|
+
| (.violations // [] | length) as $v
|
|
144
|
+
# Unit-separator (), NOT tab: an empty field between tabs is
|
|
145
|
+
# IFS-whitespace and collapses under `read`, shifting every later
|
|
146
|
+
# field and SILENTLY DROPPING the violations warning (QG B1). The
|
|
147
|
+
# workflow name is stripped of control bytes so a malformed state
|
|
148
|
+
# file cannot inject ANSI or a newline into the terminal (B4/M1).
|
|
149
|
+
| ((.workflow | gsub("[[:cntrl:]\\\\]"; "")) as $wf
|
|
150
|
+
| [$wf, ($gate|tostring), ($total|tostring), ($v|tostring)] | join("\u001f"))
|
|
151
|
+
end
|
|
152
|
+
' "$WF_STATE" 2>/dev/null)
|
|
153
|
+
if [ -n "$WF_LINE" ]; then
|
|
154
|
+
IFS=$'\x1f' read -r WF_NAME WF_GATE_NUM WF_TOTAL WF_VIOL <<< "$WF_LINE"
|
|
155
|
+
WF_SEGMENT=" ${C_DIM}|${C_RESET} ${C_CYAN}⚑ ${WF_NAME}${C_RESET} ${C_DIM}G${WF_GATE_NUM}/${WF_TOTAL}${C_RESET}"
|
|
156
|
+
if [ "${WF_VIOL:-0}" -gt 0 ] 2>/dev/null; then
|
|
157
|
+
WF_SEGMENT+=" ${C_RED}⚠${WF_VIOL}${C_RESET}"
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Budget: show spend against the per-session cap when one is configured
|
|
163
|
+
# (~/.arkaos/config.json budget.hardCapUsd). Read-only, one jq call.
|
|
164
|
+
# `numbers` coerces the type: a non-numeric cap ("lots") yields empty
|
|
165
|
+
# and the segment is omitted rather than rendering a misleading /$0.00.
|
|
166
|
+
BUDGET_FMT=""
|
|
167
|
+
ARKA_CONFIG="$HOME/.arkaos/config.json"
|
|
168
|
+
if [ -f "$ARKA_CONFIG" ]; then
|
|
169
|
+
CAP=$(jq -r '.budget.hardCapUsd | numbers // empty' "$ARKA_CONFIG" 2>/dev/null)
|
|
170
|
+
if [ -n "$CAP" ]; then
|
|
171
|
+
BUDGET_FMT=$(awk -v cap="$CAP" 'BEGIN{printf "/$%.2f", cap}')
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
|
|
120
175
|
# ─── Build Line 1: Context bar ───────────────────────────────────────────
|
|
121
176
|
LINE1="${C_CYAN}▲ARKA${C_RESET} ${C_WHITE}${DIR_NAME}${C_RESET}"
|
|
122
177
|
|
|
@@ -126,13 +181,14 @@ if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; the
|
|
|
126
181
|
fi
|
|
127
182
|
|
|
128
183
|
LINE1+=" ${C_DIM}|${C_RESET} ${MODEL}"
|
|
184
|
+
LINE1+="$WF_SEGMENT"
|
|
129
185
|
|
|
130
186
|
# ─── Build Line 2: Metrics bar ───────────────────────────────────────────
|
|
131
187
|
LINE2="${C_BAR}${BAR} ${PCT}%${C_RESET}"
|
|
132
188
|
LINE2+=" ${C_DIM}|${C_RESET} ${IN_FMT} in ${OUT_FMT} out"
|
|
133
189
|
LINE2+=" ${C_DIM}|${C_RESET} ${C_GREEN}+${ADDED}${C_RESET} ${C_RED}-${REMOVED}${C_RESET}"
|
|
134
190
|
LINE2+=" ${C_DIM}|${C_RESET} ${TIME_FMT}"
|
|
135
|
-
LINE2+=" ${C_DIM}|${C_RESET} ${COST_FMT}"
|
|
191
|
+
LINE2+=" ${C_DIM}|${C_RESET} ${COST_FMT}${BUDGET_FMT}"
|
|
136
192
|
|
|
137
193
|
# ─── Output ───────────────────────────────────────────────────────────────
|
|
138
194
|
echo -e "$LINE1"
|
|
Binary file
|
|
@@ -16,10 +16,15 @@ class CaptureStore:
|
|
|
16
16
|
"""SQLite-backed store for raw session captures."""
|
|
17
17
|
|
|
18
18
|
def __init__(self, db_path: str) -> None:
|
|
19
|
-
"""Connect to SQLite database and initialize tables.
|
|
19
|
+
"""Connect to SQLite database and initialize tables.
|
|
20
|
+
|
|
21
|
+
A corrupt file triggers ONE self-heal attempt (F1-D1) before
|
|
22
|
+
the error propagates — the original is never destroyed.
|
|
23
|
+
"""
|
|
20
24
|
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
21
25
|
self._db_path = db_path
|
|
22
|
-
|
|
26
|
+
from core.shared.sqlite_recovery import open_with_recovery
|
|
27
|
+
open_with_recovery(db_path, self._init_db)
|
|
23
28
|
|
|
24
29
|
def _conn(self) -> sqlite3.Connection:
|
|
25
30
|
conn = sqlite3.connect(self._db_path)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -23,11 +23,13 @@ CLI (for hooks/skills)::
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
|
+
import importlib.util
|
|
26
27
|
import json
|
|
27
28
|
import re
|
|
28
29
|
import shlex
|
|
29
30
|
import shutil
|
|
30
31
|
import subprocess
|
|
32
|
+
import sys
|
|
31
33
|
import time
|
|
32
34
|
from dataclasses import asdict, dataclass, field
|
|
33
35
|
from pathlib import Path
|
|
@@ -216,6 +218,25 @@ def _labelled(result: CheckResult, label: str) -> CheckResult:
|
|
|
216
218
|
return result
|
|
217
219
|
|
|
218
220
|
|
|
221
|
+
def _tool_cmd(tool: str) -> list[str] | None:
|
|
222
|
+
"""Resolve a Python tool: PATH binary, else the interpreter's module.
|
|
223
|
+
|
|
224
|
+
Operator machines install ruff/pytest into the ArkaOS venv with no
|
|
225
|
+
PATH binary — keying on shutil.which alone silently downgraded
|
|
226
|
+
Python lint to eslint over installer/*.js, a FALSE GREEN on a
|
|
227
|
+
NON-NEGOTIABLE gate (QG findings, F1-B2/F1-C1 reviews).
|
|
228
|
+
"""
|
|
229
|
+
if shutil.which(tool):
|
|
230
|
+
return [tool]
|
|
231
|
+
if importlib.util.find_spec(tool) is not None:
|
|
232
|
+
return [sys.executable, "-m", tool]
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _ruff_cmd() -> list[str] | None:
|
|
237
|
+
return _tool_cmd("ruff")
|
|
238
|
+
|
|
239
|
+
|
|
219
240
|
def _lint_scoped(
|
|
220
241
|
project_dir: Path, changed: list[str], timeout: int,
|
|
221
242
|
) -> CheckResult | None:
|
|
@@ -225,10 +246,11 @@ def _lint_scoped(
|
|
|
225
246
|
the same principle _check_security_grep already applies to its diff
|
|
226
247
|
scope. Returns None when no scoped run applies (caller falls back).
|
|
227
248
|
"""
|
|
228
|
-
|
|
249
|
+
ruff = _ruff_cmd()
|
|
250
|
+
if ruff:
|
|
229
251
|
files = _scoped_files(project_dir, changed, _LINTABLE_PY)
|
|
230
252
|
if files:
|
|
231
|
-
result = _run("lint", [
|
|
253
|
+
result = _run("lint", [*ruff, "check", *files], project_dir, timeout)
|
|
232
254
|
return _labelled(result, f"lint(scoped: {len(files)} file(s))")
|
|
233
255
|
eslint = project_dir / "node_modules" / ".bin" / "eslint"
|
|
234
256
|
if eslint.is_file():
|
|
@@ -261,10 +283,22 @@ def _check_lint(
|
|
|
261
283
|
or _scoped_files(project_dir, changed, _LINTABLE_PHP)
|
|
262
284
|
)
|
|
263
285
|
if not lintable:
|
|
264
|
-
|
|
265
|
-
|
|
286
|
+
# Skipping is only honest when the DIFF has no lintable
|
|
287
|
+
# extensions. When it does but none resolved under
|
|
288
|
+
# project_dir (deleted files, a different checkout/cwd,
|
|
289
|
+
# cross-root paths), "no lintable sources" is a blind gate
|
|
290
|
+
# (QG 2026-07-12: a 2 .py + 6 .js diff got skipped) — fall
|
|
291
|
+
# through to the project-wide lint of what IS on disk.
|
|
292
|
+
all_lintable_exts = _LINTABLE_PY | _LINTABLE_JS | _LINTABLE_PHP
|
|
293
|
+
changed_exts = {s.lower() for s in _suffixes(changed)}
|
|
294
|
+
if not (changed_exts & all_lintable_exts):
|
|
295
|
+
return _skip(
|
|
296
|
+
"lint", "changed files contain no lintable sources"
|
|
297
|
+
)
|
|
298
|
+
ruff = _ruff_cmd()
|
|
299
|
+
if _has_python(project_dir, changed) and ruff:
|
|
266
300
|
return _labelled(
|
|
267
|
-
_run("lint", [
|
|
301
|
+
_run("lint", [*ruff, "check", "."], project_dir, timeout),
|
|
268
302
|
"lint(project-wide)",
|
|
269
303
|
)
|
|
270
304
|
if _package_json_script(project_dir, "lint"):
|
|
@@ -307,8 +341,9 @@ def _check_tests(
|
|
|
307
341
|
) -> CheckResult:
|
|
308
342
|
if test_command:
|
|
309
343
|
return _run("tests", shlex.split(test_command), project_dir, timeout)
|
|
310
|
-
|
|
311
|
-
|
|
344
|
+
pytest_cmd = _tool_cmd("pytest")
|
|
345
|
+
if _has_python(project_dir, changed) and pytest_cmd:
|
|
346
|
+
return _run("tests", [*pytest_cmd, "-q"], project_dir, timeout)
|
|
312
347
|
if _package_json_script(project_dir, "test"):
|
|
313
348
|
return _run(
|
|
314
349
|
"tests", ["npm", "test", "--silent"], project_dir, timeout,
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Routing feedback aggregator (F1-B1 — memory/learning reform).
|
|
2
|
+
|
|
3
|
+
Closes the telemetry loop the ruflo teardown flagged as ArkaOS's G1 gap:
|
|
4
|
+
QG verdicts and gate-judge verdicts are written every review but nothing
|
|
5
|
+
ever read them back. This module aggregates both corpora into
|
|
6
|
+
``~/.arkaos/routing-scores.json`` — per-department approval scores that
|
|
7
|
+
the Synapse L5.5 layer (F1-B2) injects as ``[arka:redo-risk]`` warnings.
|
|
8
|
+
|
|
9
|
+
Scored table, deliberately NOT Q-learning: sample sizes are dozens, not
|
|
10
|
+
thousands (Q would never converge), and the constitution's evidence-flow
|
|
11
|
+
demands citable counts — ``approvals=3/9 (30d)`` is auditable, a Q-table
|
|
12
|
+
weight is not. Recency weighting gives the same "reward changes future
|
|
13
|
+
behaviour" property with zero exploration risk.
|
|
14
|
+
|
|
15
|
+
Sources consumed (both have ``department`` on every record):
|
|
16
|
+
- qg-verdicts.jsonl (QGVerdict: APPROVED/REJECTED + blockers)
|
|
17
|
+
- judge-verdicts.jsonl (JudgeVerdict: PASS/REVISE)
|
|
18
|
+
specialist-dispatch.jsonl is deliberately NOT consumed in v1: its records
|
|
19
|
+
carry tool/file but no department — mapping would require fragile
|
|
20
|
+
file-to-department inference, and a wrong attribution is worse than none.
|
|
21
|
+
|
|
22
|
+
CLI: ``python3 -m core.governance.routing_feedback_cli rebuild|show``
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
import os
|
|
30
|
+
import tempfile
|
|
31
|
+
from collections import Counter
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Optional
|
|
35
|
+
|
|
36
|
+
from pydantic import BaseModel, Field
|
|
37
|
+
|
|
38
|
+
SCORES_ENV = "ARKA_ROUTING_SCORES_PATH"
|
|
39
|
+
_WINDOW_DAYS = 90 # records older than this are ignored entirely
|
|
40
|
+
_HALF_LIFE_DAYS = 30.0 # recency weight halves every 30 days
|
|
41
|
+
_REDO_WINDOW_DAYS = 7 # >=2 REJECTED same deliverable+department within 7d
|
|
42
|
+
_TOP_BLOCKERS = 3
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RoutingScore(BaseModel):
|
|
46
|
+
"""Per-department evidence the router can cite verbatim."""
|
|
47
|
+
|
|
48
|
+
department: str
|
|
49
|
+
approvals: int = 0 # raw QG counts inside the window (citable)
|
|
50
|
+
rejections: int = 0
|
|
51
|
+
smoothed_approval: float = Field(
|
|
52
|
+
default=0.5,
|
|
53
|
+
description="Laplace (a+1)/(a+r+2) over recency-weighted QG counts",
|
|
54
|
+
)
|
|
55
|
+
judge_passes: int = 0 # gate-judge signal, kept separate from QG
|
|
56
|
+
judge_revises: int = 0
|
|
57
|
+
redo_count: int = 0
|
|
58
|
+
top_blocker_patterns: list[str] = Field(default_factory=list)
|
|
59
|
+
samples: int = 0 # QG verdicts backing smoothed_approval
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RoutingScores(BaseModel):
|
|
63
|
+
version: int = 1
|
|
64
|
+
computed_at: str = ""
|
|
65
|
+
window_days: int = _WINDOW_DAYS
|
|
66
|
+
sources: list[str] = Field(default_factory=list)
|
|
67
|
+
scores: list[RoutingScore] = Field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def scores_path() -> Path:
|
|
71
|
+
override = os.environ.get(SCORES_ENV, "").strip()
|
|
72
|
+
if override:
|
|
73
|
+
return Path(override)
|
|
74
|
+
return Path.home() / ".arkaos" / "routing-scores.json"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_ts(ts: str) -> Optional[datetime]:
|
|
78
|
+
"""ONE timestamp parser for the whole module — naive stamps are
|
|
79
|
+
normalized to UTC everywhere, so mixed naive+aware corpora can
|
|
80
|
+
never poison a later comparison/sort with a TypeError."""
|
|
81
|
+
try:
|
|
82
|
+
parsed = datetime.fromisoformat(ts)
|
|
83
|
+
except (ValueError, TypeError):
|
|
84
|
+
return None
|
|
85
|
+
if parsed.tzinfo is None:
|
|
86
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
87
|
+
return parsed
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _age_days(ts: str, now: datetime) -> Optional[float]:
|
|
91
|
+
parsed = _parse_ts(ts)
|
|
92
|
+
if parsed is None:
|
|
93
|
+
return None
|
|
94
|
+
return max(0.0, (now - parsed).total_seconds() / 86400.0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _recency_weight(age_days: float) -> float:
|
|
98
|
+
return math.pow(0.5, age_days / _HALF_LIFE_DAYS)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _windowed(records: list[dict], now: datetime) -> list[tuple[dict, float]]:
|
|
102
|
+
"""Pair each in-window record with its recency weight; drop the rest."""
|
|
103
|
+
kept: list[tuple[dict, float]] = []
|
|
104
|
+
for record in records:
|
|
105
|
+
age = _age_days(str(record.get("ts") or ""), now)
|
|
106
|
+
if age is None or age > _WINDOW_DAYS:
|
|
107
|
+
continue
|
|
108
|
+
department = str(record.get("department") or "").strip()
|
|
109
|
+
if not department:
|
|
110
|
+
continue # unattributable — wrong attribution is worse than none
|
|
111
|
+
kept.append((record, _recency_weight(age)))
|
|
112
|
+
return kept
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _count_redos(qg: list[tuple[dict, float]]) -> Counter:
|
|
116
|
+
""">=2 REJECTED for the same deliverable+department within 7 days.
|
|
117
|
+
|
|
118
|
+
Records with an EMPTY deliverable are skipped: the live pipeline
|
|
119
|
+
defaults deliverable to "" (record_cli), and an empty string cannot
|
|
120
|
+
establish "same deliverable" — counting it would flag a department
|
|
121
|
+
for two unrelated rejections (noise sold as signal).
|
|
122
|
+
"""
|
|
123
|
+
rejected_ts: dict[tuple[str, str], list[datetime]] = {}
|
|
124
|
+
for record, _w in qg:
|
|
125
|
+
if str(record.get("verdict")).upper() != "REJECTED":
|
|
126
|
+
continue
|
|
127
|
+
deliverable = str(record.get("deliverable") or "").strip()
|
|
128
|
+
if not deliverable:
|
|
129
|
+
continue
|
|
130
|
+
when = _parse_ts(str(record.get("ts")))
|
|
131
|
+
if when is None:
|
|
132
|
+
continue
|
|
133
|
+
key = (str(record.get("department")), deliverable)
|
|
134
|
+
rejected_ts.setdefault(key, []).append(when)
|
|
135
|
+
redos: Counter = Counter()
|
|
136
|
+
for (department, _deliverable), stamps in rejected_ts.items():
|
|
137
|
+
stamps.sort() # all aware-UTC via _parse_ts — sort cannot raise
|
|
138
|
+
for earlier, later in zip(stamps, stamps[1:]):
|
|
139
|
+
if (later - earlier).total_seconds() <= _REDO_WINDOW_DAYS * 86400:
|
|
140
|
+
redos[department] += 1
|
|
141
|
+
return redos
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _blocker_patterns(records: list[dict]) -> list[str]:
|
|
145
|
+
counts: Counter = Counter()
|
|
146
|
+
for record in records:
|
|
147
|
+
for blocker in record.get("blockers") or []:
|
|
148
|
+
check = str((blocker or {}).get("check") or "").strip()
|
|
149
|
+
if check:
|
|
150
|
+
counts[check] += 1
|
|
151
|
+
return [check for check, _n in counts.most_common(_TOP_BLOCKERS)]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _department_score(
|
|
155
|
+
department: str,
|
|
156
|
+
qg: list[tuple[dict, float]],
|
|
157
|
+
judge: list[tuple[dict, float]],
|
|
158
|
+
redos: Counter,
|
|
159
|
+
) -> RoutingScore:
|
|
160
|
+
dept_qg = [(r, w) for r, w in qg if r.get("department") == department]
|
|
161
|
+
approvals_w = sum(w for r, w in dept_qg if str(r.get("verdict")).upper() == "APPROVED")
|
|
162
|
+
rejections_w = sum(w for r, w in dept_qg if str(r.get("verdict")).upper() == "REJECTED")
|
|
163
|
+
dept_judge = [(r, w) for r, w in judge if r.get("department") == department]
|
|
164
|
+
rejected_records = [
|
|
165
|
+
r for r, _w in dept_qg if str(r.get("verdict")).upper() == "REJECTED"
|
|
166
|
+
]
|
|
167
|
+
return RoutingScore(
|
|
168
|
+
department=department,
|
|
169
|
+
approvals=sum(1 for r, _ in dept_qg if str(r.get("verdict")).upper() == "APPROVED"),
|
|
170
|
+
rejections=len(rejected_records),
|
|
171
|
+
smoothed_approval=(approvals_w + 1.0) / (approvals_w + rejections_w + 2.0),
|
|
172
|
+
judge_passes=sum(1 for r, _ in dept_judge if str(r.get("verdict")).upper() == "PASS"),
|
|
173
|
+
judge_revises=sum(1 for r, _ in dept_judge if str(r.get("verdict")).upper() == "REVISE"),
|
|
174
|
+
redo_count=redos.get(department, 0),
|
|
175
|
+
top_blocker_patterns=_blocker_patterns(rejected_records),
|
|
176
|
+
samples=len(dept_qg),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def rebuild(now: datetime | None = None) -> RoutingScores:
|
|
181
|
+
"""Aggregate both corpora and atomically write routing-scores.json."""
|
|
182
|
+
from core.evals.verdict_labels import load_judge_labels, load_verdict_labels
|
|
183
|
+
|
|
184
|
+
current = now or datetime.now(timezone.utc)
|
|
185
|
+
qg = _windowed(load_verdict_labels(), current)
|
|
186
|
+
judge = _windowed(load_judge_labels(), current)
|
|
187
|
+
departments = sorted(
|
|
188
|
+
{str(r.get("department")) for r, _ in qg}
|
|
189
|
+
| {str(r.get("department")) for r, _ in judge}
|
|
190
|
+
)
|
|
191
|
+
redos = _count_redos(qg)
|
|
192
|
+
result = RoutingScores(
|
|
193
|
+
computed_at=current.isoformat(),
|
|
194
|
+
sources=["qg-verdicts.jsonl", "judge-verdicts.jsonl"],
|
|
195
|
+
scores=[_department_score(d, qg, judge, redos) for d in departments],
|
|
196
|
+
)
|
|
197
|
+
_atomic_write(scores_path(), result)
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _atomic_write(target: Path, payload: RoutingScores) -> None:
|
|
202
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
fd, tmp_name = tempfile.mkstemp(dir=target.parent, prefix=f"{target.name}.", suffix=".tmp")
|
|
204
|
+
try:
|
|
205
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
206
|
+
fh.write(payload.model_dump_json(indent=2))
|
|
207
|
+
os.replace(tmp_name, target)
|
|
208
|
+
except OSError:
|
|
209
|
+
Path(tmp_name).unlink(missing_ok=True)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def load_scores() -> Optional[RoutingScores]:
|
|
213
|
+
"""Read the scores file; None when absent/invalid (consumer stays inert)."""
|
|
214
|
+
try:
|
|
215
|
+
data: dict[str, Any] = json.loads(scores_path().read_text(encoding="utf-8"))
|
|
216
|
+
if data.get("version") != 1:
|
|
217
|
+
return None
|
|
218
|
+
return RoutingScores(**data)
|
|
219
|
+
except Exception: # noqa: BLE001 — consumers must never break on this file
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def stale(max_age_seconds: int = 3600) -> bool:
|
|
224
|
+
"""True when the scores file is missing or older than the threshold."""
|
|
225
|
+
try:
|
|
226
|
+
mtime = scores_path().stat().st_mtime
|
|
227
|
+
except OSError:
|
|
228
|
+
return True
|
|
229
|
+
return (datetime.now(timezone.utc).timestamp() - mtime) > max_age_seconds
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""CLI for the routing feedback aggregator (F1-B1).
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python3 -m core.governance.routing_feedback_cli rebuild
|
|
5
|
+
python3 -m core.governance.routing_feedback_cli show
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from core.governance.routing_feedback import load_scores, rebuild, scores_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _show() -> int:
|
|
16
|
+
scores = load_scores()
|
|
17
|
+
if scores is None:
|
|
18
|
+
print(f"no routing scores at {scores_path()} — run: rebuild")
|
|
19
|
+
return 1
|
|
20
|
+
print(f"routing scores @ {scores.computed_at} (window {scores.window_days}d)")
|
|
21
|
+
print(f"sources: {', '.join(scores.sources)}")
|
|
22
|
+
for score in scores.scores:
|
|
23
|
+
blockers = ", ".join(score.top_blocker_patterns) or "-"
|
|
24
|
+
print(
|
|
25
|
+
f" {score.department}: {score.approvals}/{score.samples} approved"
|
|
26
|
+
f" (smoothed {score.smoothed_approval:.2f}),"
|
|
27
|
+
f" judge {score.judge_passes}P/{score.judge_revises}R,"
|
|
28
|
+
f" redos {score.redo_count}, top blockers: {blockers}"
|
|
29
|
+
)
|
|
30
|
+
if not scores.scores:
|
|
31
|
+
print(" (no attributable verdicts in window)")
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main(argv: list[str] | None = None) -> int:
|
|
36
|
+
args = argv if argv is not None else sys.argv[1:]
|
|
37
|
+
command = args[0] if args else "show"
|
|
38
|
+
if command == "rebuild":
|
|
39
|
+
result = rebuild()
|
|
40
|
+
print(f"rebuilt {scores_path()} — {len(result.scores)} department(s)")
|
|
41
|
+
return 0
|
|
42
|
+
if command == "show":
|
|
43
|
+
return _show()
|
|
44
|
+
print("usage: python3 -m core.governance.routing_feedback_cli rebuild|show")
|
|
45
|
+
return 2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__": # pragma: no cover
|
|
49
|
+
raise SystemExit(main())
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|