arkaos 4.13.2 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) 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/package.json +1 -1
  123. package/pyproject.toml +9 -1
  124. package/scripts/__pycache__/marketplace_gen.cpython-313.pyc +0 -0
  125. package/scripts/__pycache__/synapse-bridge.cpython-314.pyc +0 -0
  126. package/scripts/marketplace_gen.py +310 -0
  127. package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
  128. package/scripts/tools/__pycache__/skill_budget.cpython-313.pyc +0 -0
  129. package/scripts/tools/docs_stats.py +6 -4
  130. package/scripts/tools/skill_budget.py +209 -0
@@ -0,0 +1,152 @@
1
+ """Synapse L9.5 — session semantic memory retrieval (F1-A3).
2
+
3
+ Closes the memory loop opened by F1-A2: reads the per-session cache the
4
+ detached turn-capture worker precomputed (semantic neighbours, labeled
5
+ ``ranked@HH:MMZ`` with the cache's own computed_at — or ``pre-ranked``
6
+ when no timestamp exists; the label states WHEN the ranking happened,
7
+ never claims it was for this prompt) plus a live keyword LIKE over the
8
+ store for the current prompt.
9
+
10
+ Honesty contract (identical to L2.5 / vector_store.py): semantic hits
11
+ carry their cosine score; keyword hits carry ``score=None`` and are
12
+ labeled ``keyword — NOT semantic similarity``. The layer NEVER embeds —
13
+ zero embedding cost on the prompt hot path, ever.
14
+
15
+ Flag: ``synapse.l95SessionMemory`` (default ON) + env ``ARKA_BYPASS_L95``.
16
+ Inert (zero tokens) when the store is empty or the flag is off.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import time
24
+ from pathlib import Path
25
+
26
+ from core.synapse.layers import Layer, LayerResult, PromptContext
27
+
28
+ _MAX_ITEMS = 5
29
+ _SUMMARY_CHARS = 160
30
+ _LIVE_KEYWORD_K = 2
31
+ _CACHE_SEMANTIC_K = 3
32
+ _CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
33
+
34
+
35
+ def _l95_feature_flag_on() -> bool:
36
+ if os.environ.get("ARKA_BYPASS_L95", "").strip() == "1":
37
+ return False
38
+ try:
39
+ data = json.loads(_CONFIG_PATH.read_text(encoding="utf-8"))
40
+ except (OSError, json.JSONDecodeError):
41
+ return True
42
+ synapse_cfg = data.get("synapse") or {}
43
+ return bool(synapse_cfg.get("l95SessionMemory", True))
44
+
45
+
46
+ def _read_session_cache(session_id: str) -> tuple[list[dict], str]:
47
+ """Precomputed neighbours + the timestamp they were ranked at."""
48
+ if not session_id:
49
+ return [], ""
50
+ path = Path.home() / ".arkaos" / "context-cache" / f"session-memory-{session_id}.json"
51
+ try:
52
+ payload = json.loads(path.read_text(encoding="utf-8"))
53
+ except (OSError, json.JSONDecodeError):
54
+ return [], ""
55
+ if payload.get("version") != 1:
56
+ return [], ""
57
+ items = payload.get("items")
58
+ ranked_at = str(payload.get("computed_at") or "")
59
+ if not isinstance(items, list):
60
+ return [], ""
61
+ return items[:_CACHE_SEMANTIC_K], ranked_at
62
+
63
+
64
+ def _format_item(item: dict, ranked_at: str = "") -> str:
65
+ from core.memory.semantic_store import neutralize_summary
66
+
67
+ summary = neutralize_summary(item.get("summary") or "")[:_SUMMARY_CHARS]
68
+ if not summary:
69
+ return ""
70
+ score = item.get("score")
71
+ if item.get("retrieval") == "semantic" and isinstance(score, (int, float)):
72
+ # Provenance is the payload's own computed_at (UTC isoformat) —
73
+ # the detached worker may lag a turn, so the label states WHEN
74
+ # it ranked, never claims "last turn". Z marks UTC explicitly.
75
+ stamp = f" ranked@{ranked_at[11:16]}Z" if len(ranked_at) >= 16 else " pre-ranked"
76
+ label = f"semantic {score:.2f}{stamp}"
77
+ else:
78
+ label = "keyword — NOT semantic similarity"
79
+ project = neutralize_summary(item.get("project_name") or "")
80
+ suffix = f" ({project})" if project else ""
81
+ return f"- [{label}] {summary}{suffix}"
82
+
83
+
84
+ class SessionMemoryLayer(Layer):
85
+ """L9.5: cross-session turn memory — cache read + live keyword only."""
86
+
87
+ @property
88
+ def id(self) -> str:
89
+ return "L9.5"
90
+
91
+ @property
92
+ def name(self) -> str:
93
+ return "SessionSemanticMemory"
94
+
95
+ @property
96
+ def cache_ttl(self) -> int:
97
+ # Best-effort freshness: the cache write is detached and may lag
98
+ # a turn — recomputing here is one file read + one indexed LIKE.
99
+ return 0
100
+
101
+ @property
102
+ def priority(self) -> int:
103
+ return 91 # right after L9 SessionContext (90)
104
+
105
+ def compute(self, ctx: PromptContext) -> LayerResult:
106
+ start = time.time()
107
+ content = ""
108
+ try:
109
+ if ctx.user_input and _l95_feature_flag_on():
110
+ content = self._retrieve(ctx)
111
+ except Exception: # noqa: BLE001 — layer must never break the prompt
112
+ content = ""
113
+ elapsed = int((time.time() - start) * 1000)
114
+ if not content:
115
+ return LayerResult(layer_id=self.id, tag="", content="",
116
+ tokens_est=0, compute_ms=elapsed, cached=False)
117
+ count = content.count("\n- ") + (1 if content.startswith("- ") else 0)
118
+ return LayerResult(
119
+ layer_id=self.id,
120
+ tag=f"[session-memory:{count}]",
121
+ content=content,
122
+ tokens_est=len(content.split()),
123
+ compute_ms=elapsed,
124
+ cached=False,
125
+ )
126
+
127
+ def _retrieve(self, ctx: PromptContext) -> str:
128
+ session_id = str((ctx.extra or {}).get("session_id") or "")
129
+ lines: list[str] = []
130
+ seen: set[str] = set()
131
+ items, ranked_at = _read_session_cache(session_id)
132
+ for item in items:
133
+ line = _format_item(item, ranked_at)
134
+ if line and item.get("summary") not in seen:
135
+ seen.add(item.get("summary"))
136
+ lines.append(line)
137
+ # Scope-or-skip: an unscoped LIKE would surface one client's
138
+ # turns inside another client's prompt (QG blocker B2 —
139
+ # v2.18.0 confidentiality precedent). No resolvable project
140
+ # scope ⇒ NO live search, never a silently-global one.
141
+ project = ctx.project_name or (Path(ctx.cwd).name if ctx.cwd else "")
142
+ from core.memory.semantic_store import SessionMemoryStore, default_db_path
143
+ if project and default_db_path().is_file():
144
+ hits = SessionMemoryStore().keyword_search(
145
+ ctx.user_input, project, top_k=_LIVE_KEYWORD_K
146
+ )
147
+ for hit in hits:
148
+ line = _format_item(hit)
149
+ if line and hit.get("summary") not in seen:
150
+ seen.add(hit.get("summary"))
151
+ lines.append(line)
152
+ return "\n".join(lines[:_MAX_ITEMS])
@@ -79,6 +79,15 @@ export function deployProjectAgents(projectDir, sourceRoot = ARKAOS_ROOT) {
79
79
  * `shell: "powershell"` field and letting Claude Code handle the
80
80
  * PowerShell invocation itself.
81
81
  */
82
+ // F2-6: hooks with a Node fast-path shim. POSIX-only — Windows keeps
83
+ // the .ps1 -> Python chain untouched (the .cjs stdin contract is only
84
+ // validated against bash-spawned hooks). Registration is conditional on
85
+ // the deployed .cjs actually existing, so an older package or a partial
86
+ // deploy degrades to the .sh chain instead of a dead command. Install-
87
+ // time opt-out: ARKA_INSTALL_FASTPATH=0 registers the .sh as before
88
+ // (runtime kill-switch is ARKA_HOOK_FASTPATH=0, read by the shim).
89
+ const FASTPATH_HOOKS = new Set(["pre-tool-use", "post-tool-use"]);
90
+
82
91
  function hookEntry(hooksDir, name, timeout) {
83
92
  if (IS_WINDOWS) {
84
93
  return {
@@ -88,6 +97,21 @@ function hookEntry(hooksDir, name, timeout) {
88
97
  timeout,
89
98
  };
90
99
  }
100
+ if (
101
+ FASTPATH_HOOKS.has(name) &&
102
+ process.env.ARKA_INSTALL_FASTPATH !== "0" &&
103
+ existsSync(join(hooksDir, `${name}.cjs`)) &&
104
+ // QG B3: a split deploy (.cjs without _lib/fastpath) must never be
105
+ // registered — the shim would only ever delegate (it degrades
106
+ // safely, but registering the .sh directly is strictly better).
107
+ existsSync(join(hooksDir, "_lib", "fastpath", "engine.cjs"))
108
+ ) {
109
+ return {
110
+ type: "command",
111
+ command: join(hooksDir, `${name}.cjs`),
112
+ timeout,
113
+ };
114
+ }
91
115
  return {
92
116
  type: "command",
93
117
  command: join(hooksDir, `${name}.sh`),
@@ -151,6 +175,19 @@ export default {
151
175
  { hooks: [hookEntry(hooksDir, "stop", 5)] },
152
176
  ];
153
177
 
178
+ // SubagentStop — persist each subagent's output + warn-only honesty
179
+ // QA (phantom-action, meta-tag), nudging deliverable output toward the
180
+ // Quality Gate. 10s: transcript read + sanitize + persist (F2-4).
181
+ settings.hooks.SubagentStop = [
182
+ { hooks: [hookEntry(hooksDir, "subagent-stop", 10)] },
183
+ ];
184
+
185
+ // SessionEnd — final session digest (PreCompact only fires on
186
+ // compaction) + mark session ended. 15s (F2-4).
187
+ settings.hooks.SessionEnd = [
188
+ { hooks: [hookEntry(hooksDir, "session-end", 15)] },
189
+ ];
190
+
154
191
  // PreCompact — Session digest
155
192
  settings.hooks.PreCompact = [
156
193
  { hooks: [hookEntry(hooksDir, "pre-compact", 30)] },
@@ -21,6 +21,12 @@ import { join } from "node:path";
21
21
  // installed. Each entry is a GitHub `owner/repo` shorthand.
22
22
  export const DEFAULT_CLAUDE_MARKETPLACES = [
23
23
  "nextlevelbuilder/ui-ux-pro-max-skill",
24
+ // F2-7b: the ArkaOS marketplace itself — registered by GitHub
25
+ // owner/repo (NEVER a directory source anchored on the volatile npx
26
+ // cache). Registration costs zero context; the 16 per-department
27
+ // plugins stay opt-in pure (`/plugin install arkaos-<dept>@arkaos`),
28
+ // so DEFAULT_CLAUDE_PLUGINS deliberately does NOT change.
29
+ "andreagroferreira/arka-os",
24
30
  ];
25
31
 
26
32
  // Each entry is "name@marketplace" matching the `claude plugin install`
package/installer/cli.js CHANGED
@@ -31,6 +31,13 @@ const { values, positionals } = parseArgs({
31
31
  // Fusion — `npx arkaos fusion [--save|--show] "question"`
32
32
  save: { type: "boolean" },
33
33
  show: { type: "boolean" },
34
+ // F2-7a — `npx arkaos mcp start [--write]`. Declared so it lands in
35
+ // `values.write` instead of a free positional under strict:false
36
+ // (the documented --fix lesson).
37
+ write: { type: "boolean" },
38
+ // F2-7c — `npx arkaos update --skills <curated|full>` (same
39
+ // strict:false declaration requirement).
40
+ skills: { type: "string" },
34
41
  },
35
42
  allowPositionals: true,
36
43
  strict: false,
@@ -59,6 +66,8 @@ Usage:
59
66
  npx arkaos keys Manage API keys (OpenAI, fal.ai, etc.)
60
67
  npx arkaos models Model Fabric: which model runs each role
61
68
  npx arkaos models set <role> <provider>/<model> Re-route a role
69
+ npx arkaos mcp start Start the arka-tools MCP server (stdio; --write enables writes)
70
+ npx arkaos update --skills <curated|full> Choose the deployed skill set (default: curated on fresh installs)
62
71
  npx arkaos doctor Run health checks
63
72
  npx arkaos uninstall Remove ArkaOS
64
73
 
@@ -110,10 +119,11 @@ async function main() {
110
119
  break;
111
120
  }
112
121
 
113
- case "update":
122
+ case "update": {
114
123
  const { update } = await import("./update.js");
115
- await update();
124
+ await update({ skillsFlag: values.skills || "" });
116
125
  break;
126
+ }
117
127
 
118
128
  case "autostart": {
119
129
  const { autostart } = await import("./autostart.js");
@@ -230,6 +240,17 @@ async function main() {
230
240
  break;
231
241
  }
232
242
 
243
+ case "mcp": {
244
+ const verb = positionals[1];
245
+ if (verb !== "start") {
246
+ console.error('Usage: npx arkaos mcp start [--write]');
247
+ process.exit(1);
248
+ }
249
+ const { startServer } = await import("./mcp-runner.js");
250
+ process.exit(startServer({ write: Boolean(values.write) }));
251
+ break;
252
+ }
253
+
233
254
  default:
234
255
  console.error(`Unknown command: ${command}`);
235
256
  console.error('Run "npx arkaos help" for usage information.');
@@ -7,6 +7,7 @@
7
7
  // Seeded template keys (operator decisions):
8
8
  // hooks.hardEnforcement = true (PR19 v2.41.0)
9
9
  // hooks.kbFirst = true (PR-3 v4.1 — KB-first ON out of the box)
10
+ // memory.sessionMemory = true (F1-A2 — session semantic memory ON)
10
11
  //
11
12
  // Returns a status object:
12
13
  // { action: "created" | "added-key" | "noop"
@@ -21,13 +22,19 @@ import {
21
22
  import { homedir } from "node:os";
22
23
  import { join, dirname } from "node:path";
23
24
 
24
- // Template: every hooks.* key seeded to `true` when unset.
25
- const SEEDED_HOOK_KEYS = ["hardEnforcement", "kbFirst"];
25
+ // Template: every listed key seeded to `true` when unset, per section.
26
+ const SEEDED_SECTIONS = {
27
+ hooks: ["hardEnforcement", "kbFirst"],
28
+ memory: ["sessionMemory"],
29
+ };
26
30
 
27
31
  function defaultConfig() {
28
- const hooks = {};
29
- for (const key of SEEDED_HOOK_KEYS) hooks[key] = true;
30
- return { hooks };
32
+ const config = {};
33
+ for (const [section, keys] of Object.entries(SEEDED_SECTIONS)) {
34
+ config[section] = {};
35
+ for (const key of keys) config[section][key] = true;
36
+ }
37
+ return config;
31
38
  }
32
39
 
33
40
  export function seedArkaosConfig({ home = homedir() } = {}) {
@@ -52,20 +59,22 @@ export function seedArkaosConfig({ home = homedir() } = {}) {
52
59
  return { action: "rewrote-corrupt", path: cfgPath, backup };
53
60
  }
54
61
 
55
- config.hooks = config.hooks && typeof config.hooks === "object" ? config.hooks : {};
56
-
57
62
  let added = false;
58
63
  let preservedFalse = false;
59
- for (const key of SEEDED_HOOK_KEYS) {
60
- const current = config.hooks[key];
61
- if (current === true) continue;
62
- if (current === false) {
63
- preservedFalse = true; // explicit user choice — never overwrite
64
- continue;
64
+ for (const [section, keys] of Object.entries(SEEDED_SECTIONS)) {
65
+ const existing = config[section];
66
+ config[section] = existing && typeof existing === "object" ? existing : {};
67
+ for (const key of keys) {
68
+ const current = config[section][key];
69
+ if (current === true) continue;
70
+ if (current === false) {
71
+ preservedFalse = true; // explicit user choice — never overwrite
72
+ continue;
73
+ }
74
+ // Key unset (undefined, null, or any non-boolean) — set to true.
75
+ config[section][key] = true;
76
+ added = true;
65
77
  }
66
- // Key unset (undefined, null, or any non-boolean) — set to true.
67
- config.hooks[key] = true;
68
- added = true;
69
78
  }
70
79
 
71
80
  if (added) {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { execSync } from "node:child_process";
@@ -34,6 +34,20 @@ export function hyperframesSkillsInstalled(
34
34
  );
35
35
  }
36
36
 
37
+ // SQLite self-heal (F1-D1) leaves the corrupt original as
38
+ // <db>.corrupt-<ts>.bak next to the recovered file. Their presence means
39
+ // a store healed itself — surface it so the operator can inspect/delete.
40
+ // baseDir is injectable for tests.
41
+ export function corruptDbBackups(baseDir = INSTALL_DIR) {
42
+ try {
43
+ return readdirSync(baseDir, { recursive: true })
44
+ .map(String)
45
+ .filter((name) => /\.corrupt-\d+\.bak$/.test(name));
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
37
51
  export const checks = [
38
52
  {
39
53
  name: "install-dir",
@@ -95,12 +109,106 @@ export const checks = [
95
109
  "cwd-changed",
96
110
  "pre-tool-use",
97
111
  "stop",
112
+ "subagent-stop",
113
+ "session-end",
98
114
  ];
99
115
  const hooksDir = join(INSTALL_DIR, "config", "hooks");
100
116
  return required.every((h) => existsSync(join(hooksDir, `${h}${HOOK_EXT}`)));
101
117
  },
102
118
  fix: () => "Run: npx arkaos install --force",
103
119
  },
120
+ {
121
+ name: "hook-fastpath",
122
+ description: "Hook fast-path shims consistent (F2-6)",
123
+ severity: "fail",
124
+ check: () => {
125
+ // Only meaningful when the .cjs shims are deployed (POSIX installs
126
+ // from v4.14+). If they are, the manifest + engine + a node binary
127
+ // must exist, or every PreToolUse/PostToolUse fires a dead command
128
+ // that silently fails open — governance off with a green doctor.
129
+ if (IS_WINDOWS) return true;
130
+ const hooksDir = join(INSTALL_DIR, "config", "hooks");
131
+ const shims = ["pre-tool-use.cjs", "post-tool-use.cjs"]
132
+ .map((f) => join(hooksDir, f));
133
+ if (!shims.some((p) => existsSync(p))) return true; // .sh-only install
134
+ if (!shims.every((p) => existsSync(p))) return false; // partial deploy
135
+ if (!existsSync(join(hooksDir, "gate-manifest.json"))) return false;
136
+ if (!existsSync(join(hooksDir, "_lib", "fastpath", "engine.cjs"))) {
137
+ return false;
138
+ }
139
+ return commandExists("node");
140
+ },
141
+ fix: () =>
142
+ "Run: npx arkaos@latest update (or export ARKA_HOOK_FASTPATH=0 to force the bash chain)",
143
+ },
144
+ {
145
+ name: "arka-tools-runner",
146
+ description: "arka-tools MCP server runnable (uv or venv with mcp SDK)",
147
+ severity: "warn",
148
+ check: () => {
149
+ // Deploy dir + at least one runner. WARN, never FAIL: uv OR a
150
+ // venv with the mcp extra is enough, and neither is mandatory
151
+ // for the core install (F2-7a, `npx arkaos mcp start`).
152
+ const toolsDir = join(
153
+ homedir(), ".claude", "skills", "arka", "mcp-tools");
154
+ if (!existsSync(join(toolsDir, "server.py"))) return false;
155
+ if (commandExists("uv")) return true;
156
+ const py = getArkaosPython();
157
+ if (!py) return false;
158
+ try {
159
+ // timeout parity with mcp-runner.js::venvHasMcp — a hung
160
+ // interpreter must not stall `npx arkaos doctor` (QG 7a M3).
161
+ execSync(`"${py}" -c "import mcp"`, { stdio: "ignore", timeout: 15000 });
162
+ return true;
163
+ } catch {
164
+ return false;
165
+ }
166
+ },
167
+ fix: () =>
168
+ "Install uv (https://docs.astral.sh/uv) or: ~/.arkaos/venv/bin/pip install 'mcp[cli]>=1.2.0' — then: npx arkaos mcp start",
169
+ },
170
+ {
171
+ name: "skills-surface",
172
+ description: "Deployed skill set matches the chosen mode (curated/full)",
173
+ severity: "warn",
174
+ check: () => {
175
+ // WARN-only classifier — the doctor NEVER deletes skills. Repo
176
+ // sub-skills outside the curated cut are "plugin-eligible"
177
+ // leftovers on a curated-mode machine; anything not in the
178
+ // generated skills-manifest (ecosystem skills like user project
179
+ // packs) is unknown/user and untouchable by construction.
180
+ let mode = "full";
181
+ try {
182
+ mode = JSON.parse(readFileSync(
183
+ join(INSTALL_DIR, "skills-mode.json"), "utf-8")).mode || "full";
184
+ } catch {}
185
+ if (mode !== "curated") return true;
186
+ const repoRoot = getRepoRoot();
187
+ if (!repoRoot) return true;
188
+ let manifest;
189
+ try {
190
+ manifest = JSON.parse(readFileSync(
191
+ join(repoRoot, "knowledge", "skills-manifest.json"), "utf-8"));
192
+ } catch {
193
+ return true; // older core without the manifest — nothing to judge
194
+ }
195
+ const skillsBase = join(homedir(), ".claude", "skills");
196
+ let leftovers = 0;
197
+ try {
198
+ for (const dir of readdirSync(skillsBase)) {
199
+ if (!dir.startsWith("arka-")) continue;
200
+ const slug = dir.slice("arka-".length);
201
+ const entry = manifest.skills ? manifest.skills[slug] : null;
202
+ if (entry && !entry.curated) leftovers++;
203
+ }
204
+ } catch {
205
+ return true;
206
+ }
207
+ return leftovers === 0;
208
+ },
209
+ fix: () =>
210
+ "Curated mode with plugin-eligible leftovers deployed (harmless, uses context budget). Install packs a la carte (/plugin install arkaos-<dept>@arkaos) or keep everything: npx arkaos update --skills full",
211
+ },
104
212
  {
105
213
  name: "constitution",
106
214
  description: "Constitution YAML present",
@@ -226,6 +334,13 @@ export const checks = [
226
334
  check: () => commandExists("codebase-memory-mcp"),
227
335
  fix: () => "Install codebase-memory-mcp (see mcps/registry.json entry for the one-liner), then /arka update to activate per project",
228
336
  },
337
+ {
338
+ name: "sqlite-corrupt-backups",
339
+ description: "No self-healed SQLite stores awaiting inspection",
340
+ severity: "warn",
341
+ check: () => corruptDbBackups().length === 0,
342
+ fix: () => "Inspect then delete ~/.arkaos/**/*.corrupt-*.bak",
343
+ },
229
344
  {
230
345
  name: "magic-api-key",
231
346
  description: "Magic API key configured (frontend UI/UX — Magic MCP)",
@@ -23,3 +23,31 @@ export function copyHookLib(srcHooksDir, destHooksDir) {
23
23
  } catch {}
24
24
  return true;
25
25
  }
26
+
27
+ // F2-6 fast-path assets: the Node shims + the generated gate manifest
28
+ // (engine.cjs travels inside _lib/ via copyHookLib). The list lives HERE,
29
+ // in the single shared deploy function, so index.js and update.js cannot
30
+ // drift the way the v4.3.2 hook-lib regression did. The .sh/.ps1 hooks
31
+ // keep deploying regardless — they are the delegation target and the
32
+ // ARKA_HOOK_FASTPATH=0 kill-switch path. POSIX-only feature: the assets
33
+ // still copy on Windows (harmless), but the adapter never registers them.
34
+ export const HOOK_ASSETS = [
35
+ "pre-tool-use.cjs",
36
+ "post-tool-use.cjs",
37
+ "gate-manifest.json",
38
+ ];
39
+
40
+ export function copyHookAssets(srcHooksDir, destHooksDir) {
41
+ let copied = 0;
42
+ for (const name of HOOK_ASSETS) {
43
+ const srcPath = join(srcHooksDir, name);
44
+ if (!existsSync(srcPath)) continue;
45
+ mkdirSync(destHooksDir, { recursive: true });
46
+ cpSync(srcPath, join(destHooksDir, name));
47
+ if (name.endsWith(".cjs")) {
48
+ try { chmodSync(join(destHooksDir, name), 0o755); } catch {}
49
+ }
50
+ copied += 1;
51
+ }
52
+ return copied;
53
+ }