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.
Files changed (151) hide show
  1. package/VERSION +1 -1
  2. package/arka/SKILL.md +16 -0
  3. package/bin/arka-doctor +20 -1
  4. package/config/cognition/schedules.yaml +16 -0
  5. package/config/hooks/_lib/fastpath/engine.cjs +337 -0
  6. package/config/hooks/gate-manifest.json +741 -0
  7. package/config/hooks/post-tool-use.cjs +113 -0
  8. package/config/hooks/pre-tool-use.cjs +109 -0
  9. package/config/hooks/session-end.ps1 +40 -0
  10. package/config/hooks/session-end.sh +55 -0
  11. package/config/hooks/session-start.sh +25 -189
  12. package/config/hooks/subagent-stop.ps1 +40 -0
  13. package/config/hooks/subagent-stop.sh +56 -0
  14. package/config/mcp-policy.yaml +8 -0
  15. package/config/skills-curated.yaml +39 -0
  16. package/config/statusline.sh +57 -1
  17. package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
  18. package/core/cognition/capture/store.py +7 -2
  19. package/core/evals/__pycache__/__init__.cpython-314.pyc +0 -0
  20. package/core/evals/__pycache__/sanitizer.cpython-314.pyc +0 -0
  21. package/core/evals/__pycache__/schema.cpython-314.pyc +0 -0
  22. package/core/evals/__pycache__/verdict_labels.cpython-314.pyc +0 -0
  23. package/core/forge/__pycache__/complexity.cpython-314.pyc +0 -0
  24. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  25. package/core/governance/__pycache__/judge.cpython-314.pyc +0 -0
  26. package/core/governance/__pycache__/leak_scanner.cpython-314.pyc +0 -0
  27. package/core/governance/__pycache__/qg_verdict.cpython-314.pyc +0 -0
  28. package/core/governance/__pycache__/routing_feedback.cpython-313.pyc +0 -0
  29. package/core/governance/__pycache__/routing_feedback.cpython-314.pyc +0 -0
  30. package/core/governance/__pycache__/routing_feedback_cli.cpython-313.pyc +0 -0
  31. package/core/governance/evidence_checks.py +42 -7
  32. package/core/governance/routing_feedback.py +229 -0
  33. package/core/governance/routing_feedback_cli.py +49 -0
  34. package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
  35. package/core/hooks/__pycache__/gate_manifest.cpython-313.pyc +0 -0
  36. package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
  37. package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
  38. package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
  39. package/core/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  40. package/core/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  41. package/core/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  42. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  43. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  44. package/core/hooks/__pycache__/subagent_stop.cpython-313.pyc +0 -0
  45. package/core/hooks/__pycache__/subagent_stop.cpython-314.pyc +0 -0
  46. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  47. package/core/hooks/__pycache__/user_prompt_submit.cpython-314.pyc +0 -0
  48. package/core/hooks/gate_manifest.py +366 -0
  49. package/core/hooks/post_tool_use.py +5 -2
  50. package/core/hooks/session_end.py +127 -0
  51. package/core/hooks/session_start.py +322 -0
  52. package/core/hooks/stop.py +69 -0
  53. package/core/hooks/subagent_stop.py +186 -0
  54. package/core/hooks/user_prompt_submit.py +10 -6
  55. package/core/knowledge/__pycache__/embedding_backends.cpython-313.pyc +0 -0
  56. package/core/knowledge/__pycache__/embedding_backends.cpython-314.pyc +0 -0
  57. package/core/knowledge/__pycache__/pattern_cards.cpython-313.pyc +0 -0
  58. package/core/knowledge/__pycache__/pattern_cards.cpython-314.pyc +0 -0
  59. package/core/knowledge/__pycache__/recipes.cpython-314.pyc +0 -0
  60. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  61. package/core/knowledge/__pycache__/vector_store.cpython-314.pyc +0 -0
  62. package/core/knowledge/embedding_backends.py +278 -0
  63. package/core/knowledge/pattern_cards.py +49 -1
  64. package/core/knowledge/vector_store.py +14 -2
  65. package/core/memory/__pycache__/semantic_store.cpython-312.pyc +0 -0
  66. package/core/memory/__pycache__/semantic_store.cpython-313.pyc +0 -0
  67. package/core/memory/__pycache__/semantic_store.cpython-314.pyc +0 -0
  68. package/core/memory/__pycache__/turn_capture.cpython-313.pyc +0 -0
  69. package/core/memory/__pycache__/turn_capture.cpython-314.pyc +0 -0
  70. package/core/memory/semantic_store.py +301 -0
  71. package/core/memory/turn_capture.py +277 -0
  72. package/core/registry/__pycache__/generator.cpython-312.pyc +0 -0
  73. package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
  74. package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
  75. package/core/runtime/__pycache__/cost_governor.cpython-314.pyc +0 -0
  76. package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
  77. package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
  78. package/core/runtime/__pycache__/mcp_telemetry.cpython-312.pyc +0 -0
  79. package/core/shared/__pycache__/decay.cpython-313.pyc +0 -0
  80. package/core/shared/__pycache__/decay.cpython-314.pyc +0 -0
  81. package/core/shared/__pycache__/sqlite_recovery.cpython-312.pyc +0 -0
  82. package/core/shared/__pycache__/sqlite_recovery.cpython-313.pyc +0 -0
  83. package/core/shared/__pycache__/sqlite_recovery.cpython-314.pyc +0 -0
  84. package/core/shared/decay.py +94 -0
  85. package/core/shared/sqlite_recovery.py +266 -0
  86. package/core/synapse/__pycache__/agent_experiences_layer.cpython-313.pyc +0 -0
  87. package/core/synapse/__pycache__/agent_experiences_layer.cpython-314.pyc +0 -0
  88. package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
  89. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  90. package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
  91. package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
  92. package/core/synapse/__pycache__/pattern_library_layer.cpython-314.pyc +0 -0
  93. package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
  94. package/core/synapse/__pycache__/recipe_layer.cpython-314.pyc +0 -0
  95. package/core/synapse/__pycache__/routing_feedback_layer.cpython-313.pyc +0 -0
  96. package/core/synapse/__pycache__/routing_feedback_layer.cpython-314.pyc +0 -0
  97. package/core/synapse/__pycache__/session_memory_layer.cpython-312.pyc +0 -0
  98. package/core/synapse/__pycache__/session_memory_layer.cpython-313.pyc +0 -0
  99. package/core/synapse/__pycache__/session_memory_layer.cpython-314.pyc +0 -0
  100. package/core/synapse/agent_experiences_layer.py +42 -3
  101. package/core/synapse/engine.py +13 -0
  102. package/core/synapse/recipe_layer.py +27 -4
  103. package/core/synapse/routing_feedback_layer.py +132 -0
  104. package/core/synapse/session_memory_layer.py +152 -0
  105. package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/core/workflow/__pycache__/design_authorization.cpython-314.pyc +0 -0
  107. package/core/workflow/__pycache__/frontend_gate.cpython-314.pyc +0 -0
  108. package/core/workflow/__pycache__/specialist_enforcer.cpython-314.pyc +0 -0
  109. package/installer/adapters/claude-code.js +37 -0
  110. package/installer/claude-plugins.js +6 -0
  111. package/installer/cli.js +23 -2
  112. package/installer/config-seed.js +25 -16
  113. package/installer/doctor.js +116 -1
  114. package/installer/hook-lib.js +28 -0
  115. package/installer/index.js +48 -153
  116. package/installer/mcp-runner.js +108 -0
  117. package/installer/skill-deploy.js +189 -0
  118. package/installer/skills-mode.js +64 -0
  119. package/installer/update.js +55 -79
  120. package/knowledge/commands-registry.json.bak +171 -4
  121. package/knowledge/skills-manifest.json +2337 -0
  122. package/mcps/arka-prompts/commands.py +451 -0
  123. package/mcps/arka-prompts/pyproject.toml +15 -0
  124. package/mcps/arka-prompts/server.py +156 -0
  125. package/mcps/arka-tools/__pycache__/server.cpython-313.pyc +0 -0
  126. package/mcps/arka-tools/pyproject.toml +10 -0
  127. package/mcps/arka-tools/server.py +319 -0
  128. package/mcps/profiles/base.json +19 -0
  129. package/mcps/profiles/brand.json +19 -0
  130. package/mcps/profiles/comms.json +13 -0
  131. package/mcps/profiles/content.json +20 -0
  132. package/mcps/profiles/ecommerce.json +13 -0
  133. package/mcps/profiles/full-stack.json +15 -0
  134. package/mcps/profiles/laravel.json +12 -0
  135. package/mcps/profiles/nextjs.json +13 -0
  136. package/mcps/profiles/nuxt.json +13 -0
  137. package/mcps/profiles/react.json +12 -0
  138. package/mcps/profiles/vue.json +12 -0
  139. package/mcps/registry.json +423 -0
  140. package/mcps/scripts/apply-mcps.sh +282 -0
  141. package/mcps/stacks/laravel-packages.json +49 -0
  142. package/mcps/stacks/react-packages.json +58 -0
  143. package/package.json +2 -1
  144. package/pyproject.toml +9 -1
  145. package/scripts/__pycache__/marketplace_gen.cpython-313.pyc +0 -0
  146. package/scripts/__pycache__/synapse-bridge.cpython-314.pyc +0 -0
  147. package/scripts/marketplace_gen.py +310 -0
  148. package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
  149. package/scripts/tools/__pycache__/skill_budget.cpython-313.pyc +0 -0
  150. package/scripts/tools/docs_stats.py +6 -4
  151. 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())
@@ -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
- output, code = module.run_bridge(
225
- {"user_input": user_input, "session_id": session_id},
226
- Path(root),
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(root, user_input, session_id)
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: