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
@@ -6,7 +6,9 @@ import { fileURLToPath } from "node:url";
6
6
  import { getRuntimeConfig } from "./detect-runtime.js";
7
7
  import { findSystemPython, ensureVenv, getArkaosPython, getArkaosPip, pipInstall } from "./python-resolver.js";
8
8
  import { IS_WINDOWS, HOOK_EXT } from "./platform.js";
9
- import { copyHookLib } from "./hook-lib.js";
9
+ import { copyHookLib, copyHookAssets } from "./hook-lib.js";
10
+ import { deploySkills } from "./skill-deploy.js";
11
+ import { resolveSkillsMode } from "./skills-mode.js";
10
12
  import { deployCoreSnapshot } from "./core-snapshot.js";
11
13
 
12
14
  const __filename = fileURLToPath(import.meta.url);
@@ -677,6 +679,8 @@ function installHooks(installDir) {
677
679
  "cwd-changed",
678
680
  "pre-tool-use",
679
681
  "stop",
682
+ "subagent-stop",
683
+ "session-end",
680
684
  ];
681
685
  const hookExt = HOOK_EXT;
682
686
 
@@ -712,171 +716,51 @@ function installHooks(installDir) {
712
716
  if (copyHookLib(srcHooksDir, hooksDir)) {
713
717
  ok("Hook lib: _lib/");
714
718
  }
715
- }
716
-
717
- // Safe directory iteration. Returns an empty array instead of throwing
718
- // when the target does not exist, which lets us skip missing sources
719
- // (fresh repo checkouts may omit optional department resource dirs).
720
- function listSubdirs(parent) {
721
- if (!existsSync(parent)) return [];
722
- try {
723
- return readdirSync(parent, { withFileTypes: true })
724
- .filter((e) => e.isDirectory())
725
- .map((e) => e.name);
726
- } catch {
727
- return [];
728
- }
729
- }
730
719
 
731
- // Port of the "Resources" copy loop from install.sh:399-403. Copies any
732
- // of scripts/references/assets that exist next to a skill's SKILL.md
733
- // into the deployed skill directory. Silent no-op when a resource dir
734
- // does not exist. Each copy is wrapped in a try/catch so a partial
735
- // failure on one resource doesn't block the rest of the deployment.
736
- function copySkillResources(skillSrcDir, skillDestDir) {
737
- const resources = ["scripts", "references", "assets"];
738
- for (const res of resources) {
739
- const src = join(skillSrcDir, res);
740
- if (!existsSync(src)) continue;
741
- try {
742
- cpSync(src, join(skillDestDir, res), { recursive: true });
743
- } catch {
744
- // Best-effort — a single missing resource dir shouldn't break install.
745
- }
720
+ // F2-6 fast-path shims + gate manifest (shared deploy the asset
721
+ // list lives in hook-lib.js so this path and update.js cannot drift).
722
+ const assetCount = copyHookAssets(srcHooksDir, hooksDir);
723
+ if (assetCount > 0) {
724
+ ok(`Hook fast-path assets: ${assetCount}`);
746
725
  }
747
726
  }
748
727
 
749
- // Deploy one skill directory as a top-level `arka-<name>/` under the
750
- // Claude Code skills base. Mirrors install.sh lines 396-405 and 414-425.
751
- // Returns true if SKILL.md was deployed, false otherwise.
752
- function deployTopLevelSkill(skillSrcDir, arkaName, skillsBase) {
753
- const skillMd = join(skillSrcDir, "SKILL.md");
754
- if (!existsSync(skillMd)) return false;
755
- const dest = join(skillsBase, arkaName);
756
- ensureDir(dest);
757
- copyFileSync(skillMd, join(dest, "SKILL.md"));
758
- copySkillResources(skillSrcDir, dest);
759
- return true;
760
- }
761
-
762
728
  function installSkill(config, installDir) {
763
- // ── Main /arka skill ────────────────────────────────────────────────
764
- const skillSrc = join(ARKAOS_ROOT, "arka", "SKILL.md");
729
+ // Single shared deployment (installer/skill-deploy.js) — main /arka
730
+ // skill + nested reference bundle + department hubs + sub-skills +
731
+ // meta skills + agent personas. update.js §6 calls the SAME function,
732
+ // so install and update can no longer drift (F2-7c-pre: update-only
733
+ // machines were missing the 14 meta skills, including arka-flow).
765
734
  const skillsBase = config.skillsDir || join(homedir(), ".claude", "skills");
766
735
  const skillDest = join(skillsBase, "arka");
767
-
768
- ensureDir(skillDest);
769
-
770
- if (existsSync(skillSrc)) {
771
- copyFileSync(skillSrc, join(skillDest, "SKILL.md"));
772
- writeFileSync(join(skillDest, ".repo-path"), ARKAOS_ROOT);
773
- writeFileSync(join(skillDest, "VERSION"), VERSION);
774
- // Nested arka/skills/ (conclave, human-writing) is copied as a
775
- // reference bundle under the main /arka skill, preserving the
776
- // old behaviour for anything that reads from that path.
777
- const nestedSkillsSrc = join(ARKAOS_ROOT, "arka", "skills");
778
- if (existsSync(nestedSkillsSrc)) {
779
- const nestedSkillsOut = join(skillDest, "skills");
780
- ensureDir(nestedSkillsOut);
781
- try {
782
- cpSync(nestedSkillsSrc, nestedSkillsOut, { recursive: true });
783
- } catch {
784
- // Silent — these are bundled references, not user-facing skills.
785
- }
786
- }
736
+ // Fresh installs default to the curated cut (F2-7c); a machine that
737
+ // already chose a mode keeps it (install --force over an existing
738
+ // setup must not flip the operator's choice).
739
+ const skillsMode = resolveSkillsMode({ fresh: true });
740
+ const skillCounts = deploySkills({
741
+ repoRoot: ARKAOS_ROOT,
742
+ skillsBase,
743
+ agentsBase: join(homedir(), ".claude", "agents"),
744
+ version: VERSION,
745
+ mode: skillsMode.mode,
746
+ });
747
+ ok(`skill set mode: ${skillsMode.mode}`);
748
+ if (skillCounts.main) {
787
749
  ok("/arka skill installed");
788
750
  } else {
789
751
  warn("arka/SKILL.md not found in package");
790
752
  }
791
-
792
- // ── Department skills ───────────────────────────────────────────────
793
- // For each `departments/<dept>/SKILL.md`, deploy as top-level
794
- // `~/.claude/skills/arka-<dept>/`. Mirrors install.sh:391-406, but
795
- // iterates the source directory dynamically instead of hardcoding a
796
- // department list (the bash list is stale — ships 8 of the 18 real
797
- // departments on disk, so half are silently skipped on macOS/Linux
798
- // too until a user edits install.sh).
799
- const deptRoot = join(ARKAOS_ROOT, "departments");
800
- let deptDeployed = 0;
801
- for (const dept of listSubdirs(deptRoot)) {
802
- if (deployTopLevelSkill(join(deptRoot, dept), `arka-${dept}`, skillsBase)) {
803
- deptDeployed++;
804
- }
805
- }
806
- if (deptDeployed > 0) {
807
- ok(`${deptDeployed} department skills installed (/arka-<dept>)`);
808
- }
809
-
810
- // ── Sub-skills (departments/*/skills/*) ─────────────────────────────
811
- // For each `departments/<dept>/skills/<skill>/SKILL.md`, deploy as
812
- // top-level `~/.claude/skills/arka-<skill>/`. Mirrors install.sh:411-425.
813
- // Collisions between departments (two depts defining the same
814
- // sub-skill name) are resolved by "later wins", same semantics as
815
- // the bash loop — in practice the repo has no collisions today.
816
- let subSkillDeployed = 0;
817
- for (const dept of listSubdirs(deptRoot)) {
818
- const deptSkillsDir = join(deptRoot, dept, "skills");
819
- for (const subSkill of listSubdirs(deptSkillsDir)) {
820
- const subSkillSrc = join(deptSkillsDir, subSkill);
821
- if (deployTopLevelSkill(subSkillSrc, `arka-${subSkill}`, skillsBase)) {
822
- subSkillDeployed++;
823
- }
824
- }
753
+ if (skillCounts.depts > 0) {
754
+ ok(`${skillCounts.depts} department skills installed (/arka-<dept>)`);
825
755
  }
826
- if (subSkillDeployed > 0) {
827
- ok(`${subSkillDeployed} sub-skills installed (/arka-<skill>)`);
756
+ if (skillCounts.subs > 0) {
757
+ ok(`${skillCounts.subs} sub-skills installed (/arka-<skill>)`);
828
758
  }
829
-
830
- // ── Meta skills (arka/skills/*) ─────────────────────────────────────
831
- // For each `arka/skills/<skill>/SKILL.md`, deploy as top-level
832
- // `~/.claude/skills/arka-<skill>/`. These are cross-cutting meta
833
- // orchestrators (e.g. forge, platform-arka) that don't belong to a
834
- // single department. They ALSO remain copied as bundled references
835
- // under `~/.claude/skills/arka/skills/` by the nested block above —
836
- // this additional deployment makes them invocable as `arka-<skill>`.
837
- const arkaSkillsDir = join(ARKAOS_ROOT, "arka", "skills");
838
- let metaSkillDeployed = 0;
839
- for (const metaSkill of listSubdirs(arkaSkillsDir)) {
840
- const metaSkillSrc = join(arkaSkillsDir, metaSkill);
841
- if (deployTopLevelSkill(metaSkillSrc, `arka-${metaSkill}`, skillsBase)) {
842
- metaSkillDeployed++;
843
- }
759
+ if (skillCounts.meta > 0) {
760
+ ok(`${skillCounts.meta} meta skills installed (/arka-<skill>)`);
844
761
  }
845
- if (metaSkillDeployed > 0) {
846
- ok(`${metaSkillDeployed} meta skills installed (/arka-<skill>)`);
847
- }
848
-
849
- // ── Agent personas ──────────────────────────────────────────────────
850
- // For each `departments/<dept>/agents/<name>.md`, deploy to
851
- // `~/.claude/agents/arka-<name>.md`. Mirrors install.sh:436-443.
852
- // The agents dir parallels skillsBase — Claude Code reads it
853
- // separately from the skills tree.
854
- const agentsBase = join(homedir(), ".claude", "agents");
855
- ensureDir(agentsBase);
856
- let agentDeployed = 0;
857
- for (const dept of listSubdirs(deptRoot)) {
858
- const agentsSrc = join(deptRoot, dept, "agents");
859
- if (!existsSync(agentsSrc)) continue;
860
- try {
861
- for (const agentFile of readdirSync(agentsSrc)) {
862
- if (!agentFile.endsWith(".md")) continue;
863
- const srcFile = join(agentsSrc, agentFile);
864
- // Safety: don't deploy anything that's not a regular file.
865
- try {
866
- if (!statSync(srcFile).isFile()) continue;
867
- } catch {
868
- continue;
869
- }
870
- const baseName = agentFile.replace(/\.md$/, "");
871
- copyFileSync(srcFile, join(agentsBase, `arka-${baseName}.md`));
872
- agentDeployed++;
873
- }
874
- } catch {
875
- // Silently skip departments with unreadable agents dirs.
876
- }
877
- }
878
- if (agentDeployed > 0) {
879
- ok(`${agentDeployed} agent personas installed (~/.claude/agents/arka-*.md)`);
762
+ if (skillCounts.agents > 0) {
763
+ ok(`${skillCounts.agents} agent personas installed (~/.claude/agents/arka-*.md)`);
880
764
  }
881
765
 
882
766
  // ── MCP infrastructure ──────────────────────────────────────────────
@@ -922,7 +806,18 @@ function installSkill(config, installDir) {
922
806
  }
923
807
  }
924
808
 
925
- ok("MCP infrastructure deployed (profiles, stacks, scripts, arka-prompts server)");
809
+ // Deploy arka-tools server to mcp-tools/ (F2-3: programmatic tools)
810
+ const mcpToolsSrc = join(mcpsSrc, "arka-tools");
811
+ const mcpToolsDest = join(skillsBase, "arka", "mcp-tools");
812
+ if (existsSync(mcpToolsSrc)) {
813
+ ensureDir(mcpToolsDest);
814
+ for (const f of ["server.py", "pyproject.toml"]) {
815
+ const src = join(mcpToolsSrc, f);
816
+ if (existsSync(src)) copyFileSync(src, join(mcpToolsDest, f));
817
+ }
818
+ }
819
+
820
+ ok("MCP infrastructure deployed (profiles, stacks, scripts, arka-prompts + arka-tools servers)");
926
821
 
927
822
  // Higgsfield MCP is registered but requires an account + API key to connect.
928
823
  // Non-blocking warning: the install succeeds even without a Higgsfield account.
@@ -0,0 +1,108 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ import { getArkaosPython, getRepoRoot } from "./python-resolver.js";
7
+
8
+ // F2-7a — `npx arkaos mcp start`: run the arka-tools MCP server (stdio)
9
+ // outside Claude Code (other runtimes, debugging, CI smoke tests).
10
+ //
11
+ // Resolution order mirrors production first: mcps/registry.json runs the
12
+ // server via `uv --directory <toolsDir> run server.py`, so when uv is on
13
+ // PATH we replicate that exact invocation. Fallback is the ArkaOS venv
14
+ // python — but NO installer pip path installs `mcp[cli]` (the pyproject
15
+ // [mcp] extra exists for tests only), so the fallback preflights
16
+ // `import mcp` and fails HONESTLY with both remediations instead of a
17
+ // stack trace at first tool call.
18
+
19
+ export function toolsDir(home = homedir()) {
20
+ return join(home, ".claude", "skills", "arka", "mcp-tools");
21
+ }
22
+
23
+ function uvAvailable(spawn = spawnSync) {
24
+ const probe = spawn("uv", ["--version"], { stdio: "ignore", timeout: 5000 });
25
+ return !probe.error && probe.status === 0;
26
+ }
27
+
28
+ function venvHasMcp(py, spawn = spawnSync) {
29
+ const probe = spawn(py, ["-c", "import mcp"], {
30
+ stdio: "ignore",
31
+ timeout: 15000,
32
+ });
33
+ return !probe.error && probe.status === 0;
34
+ }
35
+
36
+ /**
37
+ * Resolve how to launch the arka-tools server.
38
+ * Returns {cmd, args, kind: "uv"|"venv"} or {error: string}.
39
+ * Injectable deps keep this unit-testable without real binaries.
40
+ */
41
+ export function resolveRunner({
42
+ home = homedir(),
43
+ spawn = spawnSync,
44
+ pythonResolver = getArkaosPython,
45
+ } = {}) {
46
+ const dir = toolsDir(home);
47
+ if (!existsSync(join(dir, "server.py"))) {
48
+ return {
49
+ error:
50
+ "arka-tools is not deployed (missing " + join(dir, "server.py") +
51
+ ").\nRun: npx arkaos install",
52
+ };
53
+ }
54
+ if (uvAvailable(spawn)) {
55
+ // Exact replica of the mcps/registry.json invocation Claude Code uses.
56
+ return { kind: "uv", cmd: "uv", args: ["--directory", dir, "run", "server.py"] };
57
+ }
58
+ const py = pythonResolver();
59
+ if (!py) {
60
+ return { error: "No Python found. Run: npx arkaos install" };
61
+ }
62
+ if (!venvHasMcp(py, spawn)) {
63
+ return {
64
+ error:
65
+ "arka-tools needs the MCP SDK and neither runner is ready. Fix one of:\n" +
66
+ " 1. Install uv (recommended): https://docs.astral.sh/uv\n" +
67
+ " 2. ~/.arkaos/venv/bin/pip install 'mcp[cli]>=1.2.0'",
68
+ };
69
+ }
70
+ return { kind: "venv", cmd: py, args: [join(dir, "server.py")] };
71
+ }
72
+
73
+ /**
74
+ * Start the server in the foreground (stdio transport — the MCP client
75
+ * owns the lifecycle; there is no daemon to manage). Returns the exit
76
+ * code to propagate.
77
+ */
78
+ export function startServer({
79
+ write = false,
80
+ home = homedir(),
81
+ spawn = spawnSync,
82
+ pythonResolver = getArkaosPython,
83
+ repoRootResolver = getRepoRoot,
84
+ log = console.error,
85
+ } = {}) {
86
+ const runner = resolveRunner({ home, spawn, pythonResolver });
87
+ if (runner.error) {
88
+ log(runner.error);
89
+ return 1;
90
+ }
91
+ const env = { ...process.env };
92
+ // repoRootResolver walks .repo-path/install-manifest — NEVER anchor on
93
+ // the npx cache dir, which `npm cache clean` purges at any time.
94
+ // Injectable so tests assert the positive ARKA_OS path (QG 7a M1).
95
+ const repoRoot = repoRootResolver();
96
+ if (repoRoot) env.ARKA_OS = repoRoot;
97
+ if (write) {
98
+ env.ARKA_TOOLS_WRITE = "1"; // server.py requires the exact value "1"
99
+ }
100
+ log(`arka-tools MCP server starting (${runner.kind}, stdio)` +
101
+ (write ? " — WRITES ENABLED (ARKA_TOOLS_WRITE=1)" : " — read-only"));
102
+ const result = spawn(runner.cmd, runner.args, { stdio: "inherit", env });
103
+ if (result.error) {
104
+ log(`Failed to start: ${result.error.message}`);
105
+ return 1;
106
+ }
107
+ return result.status ?? 0;
108
+ }
@@ -0,0 +1,189 @@
1
+ import {
2
+ copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync,
3
+ statSync, writeFileSync,
4
+ } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ import { resolveFile } from "./path-resolver.js";
8
+
9
+ // Single skill-deployment implementation shared by the fresh-install
10
+ // path (index.js::installSkill) and the update path (update.js §6) —
11
+ // the same single-source pattern as hook-lib.js::copyHookLib, and for
12
+ // the same reason: the two loops HAD drifted. update.js §6 deployed
13
+ // hubs + sub-skills + agents but never the 14 meta skills
14
+ // (arka/skills/* — including arka-flow, the evidence-flow
15
+ // NON-NEGOTIABLE) nor the nested reference bundle, so any update-only
16
+ // machine was silently missing them (F2-7c-pre).
17
+ //
18
+ // Markdown semantics: safeResolveMarkdown everywhere — resolveFile
19
+ // substitutes ${VAR} path tokens and falls back to a raw copy when
20
+ // resolution fails. This is the update.js behavior; index.js used to
21
+ // raw-copy, so unifying on the resolving superset fixes install too.
22
+
23
+ function listSubdirs(parent) {
24
+ if (!existsSync(parent)) return [];
25
+ try {
26
+ return readdirSync(parent, { withFileTypes: true })
27
+ .filter((e) => e.isDirectory())
28
+ .map((e) => e.name);
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ function safeResolveMarkdown(src, dst) {
35
+ try {
36
+ resolveFile(src, dst);
37
+ } catch {
38
+ copyFileSync(src, dst);
39
+ }
40
+ }
41
+
42
+ export function copySkillResources(skillSrcDir, skillDestDir) {
43
+ for (const res of ["scripts", "references", "assets"]) {
44
+ const src = join(skillSrcDir, res);
45
+ if (!existsSync(src)) continue;
46
+ try {
47
+ cpSync(src, join(skillDestDir, res), { recursive: true });
48
+ } catch {
49
+ // Best-effort — a missing resource dir must not break deploy.
50
+ }
51
+ }
52
+ }
53
+
54
+ function deployTopLevelSkill(skillSrcDir, arkaName, skillsBase) {
55
+ const skillMd = join(skillSrcDir, "SKILL.md");
56
+ if (!existsSync(skillMd)) return false;
57
+ const dest = join(skillsBase, arkaName);
58
+ mkdirSync(dest, { recursive: true });
59
+ safeResolveMarkdown(skillMd, join(dest, "SKILL.md"));
60
+ copySkillResources(skillSrcDir, dest);
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Deploy the full skill surface: main /arka skill (+ nested reference
66
+ * bundle), department hubs, sub-skills, meta skills and agent personas.
67
+ * Copy-only — never deletes anything already deployed.
68
+ *
69
+ * Returns per-category counts so both callers can keep their own log
70
+ * style. `version`/`repoRoot` stamp the main skill's VERSION and
71
+ * .repo-path exactly as both callers did before.
72
+ */
73
+ export function deploySkills({
74
+ repoRoot,
75
+ skillsBase,
76
+ agentsBase = null,
77
+ version = "",
78
+ mode = "full",
79
+ log = () => {},
80
+ }) {
81
+ const counts = { main: 0, bundle: 0, depts: 0, subs: 0, meta: 0, agents: 0 };
82
+ // Curated mode (F2-7c): sub-skills outside the curated cut stay in
83
+ // the repo and ship via the marketplace plugins instead. The curated
84
+ // classification comes from the GENERATED knowledge/skills-manifest
85
+ // (drift-locked to config/skills-curated.yaml). Manifest unreadable
86
+ // -> deploy everything (fail-open: never silently shrink an install
87
+ // because a file was missing).
88
+ const curatedFilter = mode === "curated"
89
+ ? loadCuratedFilter(repoRoot)
90
+ : null;
91
+
92
+ // ── Main /arka skill + nested reference bundle ──────────────────────
93
+ const skillSrc = join(repoRoot, "arka", "SKILL.md");
94
+ const skillDest = join(skillsBase, "arka");
95
+ mkdirSync(skillDest, { recursive: true });
96
+ if (existsSync(skillSrc)) {
97
+ safeResolveMarkdown(skillSrc, join(skillDest, "SKILL.md"));
98
+ writeFileSync(join(skillDest, ".repo-path"), repoRoot);
99
+ if (version) writeFileSync(join(skillDest, "VERSION"), version);
100
+ counts.main = 1;
101
+ const nestedSrc = join(repoRoot, "arka", "skills");
102
+ if (existsSync(nestedSrc)) {
103
+ try {
104
+ cpSync(nestedSrc, join(skillDest, "skills"), { recursive: true });
105
+ counts.bundle = 1;
106
+ } catch {
107
+ // Bundled references only — never fail the deploy over them.
108
+ }
109
+ }
110
+ }
111
+
112
+ // ── Department hubs + sub-skills ────────────────────────────────────
113
+ const deptRoot = join(repoRoot, "departments");
114
+ for (const dept of listSubdirs(deptRoot)) {
115
+ if (deployTopLevelSkill(join(deptRoot, dept), `arka-${dept}`, skillsBase)) {
116
+ counts.depts++;
117
+ }
118
+ const deptSkillsDir = join(deptRoot, dept, "skills");
119
+ for (const sub of listSubdirs(deptSkillsDir)) {
120
+ if (curatedFilter && !curatedFilter.has(sub)) continue;
121
+ if (deployTopLevelSkill(
122
+ join(deptSkillsDir, sub), `arka-${sub}`, skillsBase)) {
123
+ counts.subs++;
124
+ }
125
+ }
126
+ }
127
+
128
+ // ── Meta skills (arka/skills/* as invocable top-level arka-<skill>) ─
129
+ const metaRoot = join(repoRoot, "arka", "skills");
130
+ for (const meta of listSubdirs(metaRoot)) {
131
+ if (deployTopLevelSkill(join(metaRoot, meta), `arka-${meta}`, skillsBase)) {
132
+ counts.meta++;
133
+ }
134
+ }
135
+
136
+ // ── Agent personas ──────────────────────────────────────────────────
137
+ if (agentsBase) {
138
+ counts.agents = deployAgents(deptRoot, agentsBase);
139
+ }
140
+
141
+ log(counts);
142
+ return counts;
143
+ }
144
+
145
+ function loadCuratedFilter(repoRoot) {
146
+ try {
147
+ const manifest = JSON.parse(readFileSync(
148
+ join(repoRoot, "knowledge", "skills-manifest.json"), "utf-8"));
149
+ const curated = new Set();
150
+ for (const [slug, entry] of Object.entries(manifest.skills || {})) {
151
+ if (entry.curated) curated.add(slug);
152
+ }
153
+ return curated.size > 0 ? curated : null;
154
+ } catch {
155
+ return null; // fail-open: full deploy beats a silently empty one
156
+ }
157
+ }
158
+
159
+ function deployAgents(deptRoot, agentsBase) {
160
+ mkdirSync(agentsBase, { recursive: true });
161
+ let deployed = 0;
162
+ for (const dept of listSubdirs(deptRoot)) {
163
+ const agentsSrc = join(deptRoot, dept, "agents");
164
+ if (!existsSync(agentsSrc)) continue;
165
+ try {
166
+ deployed += deployDeptAgents(agentsSrc, agentsBase);
167
+ } catch {
168
+ // Unreadable agents dir — skip the department, not the deploy.
169
+ }
170
+ }
171
+ return deployed;
172
+ }
173
+
174
+ function deployDeptAgents(agentsSrc, agentsBase) {
175
+ let deployed = 0;
176
+ for (const file of readdirSync(agentsSrc)) {
177
+ if (!file.endsWith(".md")) continue;
178
+ const srcFile = join(agentsSrc, file);
179
+ try {
180
+ if (!statSync(srcFile).isFile()) continue;
181
+ } catch {
182
+ continue;
183
+ }
184
+ copyFileSync(
185
+ srcFile, join(agentsBase, `arka-${file.replace(/\.md$/, "")}.md`));
186
+ deployed++;
187
+ }
188
+ return deployed;
189
+ }
@@ -0,0 +1,64 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ // Persisted skills-deploy mode (F2-7c deprecation window, user-data
6
+ // pattern of v2.19): fresh installs default to the curated cut; update
7
+ // preserves whatever the machine already chose; machines predating the
8
+ // mode file stay on "full" WITH a deprecation notice — an update must
9
+ // never silently shrink an installed surface. Phase 2 (curated default
10
+ // for everyone + whitelist-only sweep) ships separately, gated on
11
+ // doctor telemetry about how many installs remain "full".
12
+
13
+ const MODE_FILE = () => join(homedir(), ".arkaos", "skills-mode.json");
14
+ const VALID_MODES = new Set(["curated", "full"]);
15
+
16
+ export function readSkillsMode() {
17
+ try {
18
+ const data = JSON.parse(readFileSync(MODE_FILE(), "utf-8"));
19
+ return VALID_MODES.has(data.mode) ? data.mode : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export function writeSkillsMode(mode) {
26
+ if (!VALID_MODES.has(mode)) return false;
27
+ try {
28
+ mkdirSync(join(homedir(), ".arkaos"), { recursive: true });
29
+ writeFileSync(MODE_FILE(), JSON.stringify({ mode }) + "\n");
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Resolve the mode for THIS run. `flag` is the --skills CLI value
38
+ * (persisted when valid); `fresh` marks a fresh install (no mode file
39
+ * -> curated). Returns { mode, deprecated } where `deprecated` flags a
40
+ * legacy full-mode machine that should see the migration notice.
41
+ */
42
+ export function resolveSkillsMode({ flag = "", fresh = false } = {}) {
43
+ if (VALID_MODES.has(flag)) {
44
+ writeSkillsMode(flag);
45
+ return { mode: flag, deprecated: false };
46
+ }
47
+ const persisted = readSkillsMode();
48
+ if (persisted) return { mode: persisted, deprecated: false };
49
+ if (fresh) {
50
+ writeSkillsMode("curated");
51
+ return { mode: "curated", deprecated: false };
52
+ }
53
+ return { mode: "full", deprecated: true };
54
+ }
55
+
56
+ export function deprecationNotice() {
57
+ return (
58
+ "[arka:deprecated] Full skill set (274) is deprecated; the default "
59
+ + "becomes the curated 69 in a future release. Switch now: "
60
+ + "npx arkaos update --skills curated (a la carte packs: "
61
+ + "/plugin install arkaos-<dept>@arkaos). Opt out permanently: "
62
+ + "npx arkaos update --skills full"
63
+ );
64
+ }