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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "4.13.2",
3
+ "version": "4.14.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "4.13.2"
3
+ version = "4.14.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -32,6 +32,14 @@ knowledge = [
32
32
  "fastembed>=0.8.0",
33
33
  "sqlite-vec>=0.1.1",
34
34
  ]
35
+ mcp = [
36
+ # The arka-tools MCP server runs under its own `uv` env in production
37
+ # (mcps/arka-tools/pyproject.toml); this extra lets the CORE test suite
38
+ # import the FastMCP decorators so the arka-tools security tests
39
+ # (OWASP LLM01 neutralization, write-gating) run on every Python
40
+ # version, not just where mcp happens to be installed.
41
+ "mcp[cli]>=1.2.0",
42
+ ]
35
43
  dashboard = [
36
44
  "fastapi>=0.115.0",
37
45
  "uvicorn>=0.32.0",
@@ -0,0 +1,310 @@
1
+ """Marketplace generator — per-department plugins from the curated cut.
2
+
3
+ F2-7b (ruflo-teardown campaign, distribution endgame): the ArkaOS
4
+ marketplace answers ruflo's 38-plugin distribution surface WITHOUT its
5
+ 344-SKILL.md context blowout. The curated core stays on the installer
6
+ channel (multi-runtime); every sub-skill OUTSIDE the curated set ships
7
+ à la carte in a per-department plugin, where the plugin namespace
8
+ (``arkaos-<dept>:<skill>``) also disambiguates the later-wins collision
9
+ slugs natively.
10
+
11
+ Single source of truth: ``config/skills-curated.yaml`` + the
12
+ ``departments/`` tree. This script GENERATES (never hand-typed —
13
+ docs_stats/gate_manifest precedent):
14
+
15
+ - ``plugins/arkaos-<dept>/`` — one plugin per department with a
16
+ non-empty complement (dept sub-skills minus curated), skills exported
17
+ with the marketplace transformations;
18
+ - ``.claude-plugin/marketplace.json`` — EXTENDED, never replaced: the
19
+ ``name``/``owner`` fields and the legacy ``arkaos-dev-skills`` entry
20
+ are preserved verbatim (that plugin may already be installed on user
21
+ machines and its 10-skill list is locked by
22
+ ``test_marketplace_export.py``); top-level ``version`` tracks the
23
+ VERSION file (kills the stale ``metadata.version`` for good);
24
+ - ``knowledge/skills-manifest.json`` — slug -> {depts, curated,
25
+ plugins, collision} map consumed by /do install hints, the doctor and
26
+ the F2-7c skill-budget linter.
27
+
28
+ Regenerate + commit after touching the YAML, any SKILL.md set, or
29
+ VERSION:
30
+
31
+ python scripts/marketplace_gen.py
32
+
33
+ ``tests/python/test_marketplace_gen.py`` drift-gates the committed tree
34
+ against a fresh in-memory run, byte for byte.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import re
41
+ import shutil
42
+ import sys
43
+ from pathlib import Path
44
+
45
+ import yaml
46
+
47
+ REPO_ROOT = Path(__file__).resolve().parent.parent
48
+ if str(REPO_ROOT / "scripts") not in sys.path:
49
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
50
+
51
+ from marketplace_export import _convert # noqa: E402
52
+
53
+ CURATED_YAML = REPO_ROOT / "config" / "skills-curated.yaml"
54
+ DEPARTMENTS_DIR = REPO_ROOT / "departments"
55
+ PLUGINS_DIR = REPO_ROOT / "plugins"
56
+ MARKETPLACE_JSON = REPO_ROOT / ".claude-plugin" / "marketplace.json"
57
+ SKILLS_MANIFEST_JSON = REPO_ROOT / "knowledge" / "skills-manifest.json"
58
+ VERSION = (REPO_ROOT / "VERSION").read_text(encoding="utf-8").strip()
59
+
60
+ MARKETPLACE_NAME = "arkaos"
61
+ LEGACY_PLUGIN = "arkaos-dev-skills"
62
+ _RESOURCE_DIRS = ("scripts", "references", "assets")
63
+
64
+ # The export _convert strips slash-command suffixes but lets markdown
65
+ # links to ArkaOS commands through (observed leak:
66
+ # "[Browser Integration Pattern](/arka)" in marketplace/skills/
67
+ # code-review). Fifth transformation: unwrap them to plain text.
68
+ _ARKA_LINK_PATTERN = re.compile(r"\[([^\]]+)\]\(/arka[^)]*\)")
69
+
70
+ _DEPT_LABELS = {
71
+ "brand": "Brand & Design",
72
+ "community": "Communities & Groups",
73
+ "content": "Content & Viralization",
74
+ "dev": "Development",
75
+ "ecom": "E-Commerce",
76
+ "finance": "Finance & Investment",
77
+ "kb": "Knowledge Management",
78
+ "landing": "Landing Pages & Funnels",
79
+ "leadership": "Leadership & People",
80
+ "marketing": "Marketing & Growth",
81
+ "ops": "Operations & Automation",
82
+ "org": "Organization & Teams",
83
+ "pm": "Project Management",
84
+ "quality": "Quality",
85
+ "saas": "SaaS & Micro-SaaS",
86
+ "sales": "Sales & Negotiation",
87
+ "strategy": "Strategy & Innovation",
88
+ }
89
+
90
+
91
+ def load_curated() -> dict[str, list[str]]:
92
+ config = yaml.safe_load(CURATED_YAML.read_text(encoding="utf-8"))
93
+ return {
94
+ dept: sorted(slugs)
95
+ for dept, slugs in config["curated_subskills"].items()
96
+ }
97
+
98
+
99
+ def dept_subskills() -> dict[str, list[str]]:
100
+ """All sub-skill slugs per department, from disk."""
101
+ result: dict[str, list[str]] = {}
102
+ for dept_dir in sorted(DEPARTMENTS_DIR.iterdir()):
103
+ skills_dir = dept_dir / "skills"
104
+ if not skills_dir.is_dir():
105
+ continue
106
+ slugs = sorted(
107
+ p.name for p in skills_dir.iterdir()
108
+ if (p / "SKILL.md").is_file()
109
+ )
110
+ if slugs:
111
+ result[dept_dir.name] = slugs
112
+ return result
113
+
114
+
115
+ def collision_slugs(subskills: dict[str, list[str]]) -> dict[str, list[str]]:
116
+ """Slugs owned by more than one department (later-wins on deploy)."""
117
+ owners: dict[str, list[str]] = {}
118
+ for dept, slugs in subskills.items():
119
+ for slug in slugs:
120
+ owners.setdefault(slug, []).append(dept)
121
+ return {s: sorted(d) for s, d in owners.items() if len(d) > 1}
122
+
123
+
124
+ _NAME_LINE_PATTERN = re.compile(r"^name:\s*.*$", flags=re.MULTILINE)
125
+
126
+
127
+ def convert_skill(body: str, slug: str) -> str:
128
+ """The 4 marketplace_export transformations + two plugin-specific
129
+ ones: unwrap markdown links to ArkaOS commands, and FORCE the
130
+ frontmatter name to the directory slug — the in-tree files carry
131
+ `name: <dept>/<name>` (235 of 265) and sometimes a name that is not
132
+ the dir slug at all (`logo` in `logo-brief/`); the plugin runtime
133
+ rejects name/dir mismatches."""
134
+ out = _ARKA_LINK_PATTERN.sub(r"\1", _convert(body))
135
+ return _NAME_LINE_PATTERN.sub(f"name: {slug}", out, count=1)
136
+
137
+
138
+ def _plugin_description(dept: str, slugs: list[str]) -> str:
139
+ label = _DEPT_LABELS.get(dept, dept.title())
140
+ shown = ", ".join(slugs[:8])
141
+ more = f", +{len(slugs) - 8} more" if len(slugs) > 8 else ""
142
+ return (
143
+ f"ArkaOS {label} skills, à la carte — {len(slugs)} framework-backed "
144
+ f"skills beyond the curated core: {shown}{more}. Generated by "
145
+ f"scripts/marketplace_gen.py from departments/{dept}/skills/."
146
+ )
147
+
148
+
149
+ def _emit_skill(src_dir: Path, dest_dir: Path) -> None:
150
+ dest_dir.mkdir(parents=True, exist_ok=True)
151
+ body = (src_dir / "SKILL.md").read_text(encoding="utf-8")
152
+ (dest_dir / "SKILL.md").write_text(
153
+ convert_skill(body, dest_dir.name), encoding="utf-8")
154
+ for resource in _RESOURCE_DIRS:
155
+ src = src_dir / resource
156
+ if src.is_dir():
157
+ shutil.copytree(src, dest_dir / resource, dirs_exist_ok=True)
158
+
159
+
160
+ def build_plugins() -> dict[str, list[str]]:
161
+ """Write plugins/arkaos-<dept>/ trees. Returns dept -> plugin slugs."""
162
+ curated = load_curated()
163
+ subskills = dept_subskills()
164
+ if PLUGINS_DIR.exists():
165
+ shutil.rmtree(PLUGINS_DIR)
166
+ emitted: dict[str, list[str]] = {}
167
+ for dept, slugs in subskills.items():
168
+ complement = [s for s in slugs if s not in set(curated.get(dept, []))]
169
+ if not complement:
170
+ continue
171
+ plugin_dir = PLUGINS_DIR / f"arkaos-{dept}"
172
+ for slug in complement:
173
+ _emit_skill(
174
+ DEPARTMENTS_DIR / dept / "skills" / slug,
175
+ plugin_dir / "skills" / slug,
176
+ )
177
+ _write_plugin_manifest(plugin_dir, dept, complement)
178
+ emitted[dept] = complement
179
+ return emitted
180
+
181
+
182
+ def _write_plugin_manifest(
183
+ plugin_dir: Path, dept: str, slugs: list[str]
184
+ ) -> None:
185
+ manifest_dir = plugin_dir / ".claude-plugin"
186
+ manifest_dir.mkdir(parents=True, exist_ok=True)
187
+ plugin_json = {
188
+ "name": f"arkaos-{dept}",
189
+ "description": _plugin_description(dept, slugs),
190
+ # Version lives ONLY here (plugin.json wins silently over a
191
+ # marketplace-entry version per the plugin spec — never both).
192
+ "version": VERSION,
193
+ "author": {"name": "WizardingCode", "email": "hello@wizardingcode.io"},
194
+ "license": "MIT",
195
+ }
196
+ (manifest_dir / "plugin.json").write_text(
197
+ json.dumps(plugin_json, indent=2, ensure_ascii=False) + "\n",
198
+ encoding="utf-8",
199
+ )
200
+
201
+
202
+ def build_marketplace(emitted: dict[str, list[str]]) -> dict:
203
+ """Extend the existing manifest — name/owner/legacy entry verbatim."""
204
+ manifest = json.loads(MARKETPLACE_JSON.read_text(encoding="utf-8"))
205
+ if manifest.get("name") != MARKETPLACE_NAME:
206
+ raise ValueError(
207
+ f"marketplace name changed ({manifest.get('name')!r}) — the "
208
+ f"registered name is load-bearing on user machines; refusing"
209
+ )
210
+ legacy = [p for p in manifest["plugins"] if p.get("name") == LEGACY_PLUGIN]
211
+ if len(legacy) != 1:
212
+ raise ValueError("legacy arkaos-dev-skills entry missing — refusing")
213
+ # Top-level version tracks VERSION (metadata.version is deprecated
214
+ # upstream; the metadata KEY stays one release for backwards
215
+ # compatibility, but its value tracks VERSION too — the stale 2.71.0
216
+ # it carried was exactly the hand-typed drift this generator kills).
217
+ manifest["version"] = VERSION
218
+ if isinstance(manifest.get("metadata"), dict):
219
+ manifest["metadata"]["version"] = VERSION
220
+ dept_entries = [
221
+ {
222
+ "name": f"arkaos-{dept}",
223
+ "source": f"./plugins/arkaos-{dept}",
224
+ "category": dept,
225
+ "description": _plugin_description(dept, slugs),
226
+ }
227
+ for dept, slugs in sorted(emitted.items())
228
+ ]
229
+ manifest["plugins"] = legacy + dept_entries
230
+ return manifest
231
+
232
+
233
+ def build_skills_manifest(emitted: dict[str, list[str]]) -> dict:
234
+ curated = load_curated()
235
+ subskills = dept_subskills()
236
+ collisions = collision_slugs(subskills)
237
+ skills: dict[str, dict] = {}
238
+ for dept, slugs in subskills.items():
239
+ for slug in slugs:
240
+ entry = skills.setdefault(slug, {
241
+ "depts": [], "curated": False, "plugins": [],
242
+ "collision": slug in collisions,
243
+ })
244
+ entry["depts"].append(dept)
245
+ if slug in set(curated.get(dept, [])):
246
+ entry["curated"] = True
247
+ elif dept in emitted and slug in emitted[dept]:
248
+ entry["plugins"].append(f"arkaos-{dept}@{MARKETPLACE_NAME}")
249
+ for entry in skills.values():
250
+ entry["depts"].sort()
251
+ entry["plugins"].sort()
252
+ return {
253
+ "_meta": {
254
+ "generator": "scripts/marketplace_gen.py",
255
+ "version": VERSION,
256
+ "marketplace": MARKETPLACE_NAME,
257
+ },
258
+ "structural": _structural_surface(),
259
+ "skills": dict(sorted(skills.items())),
260
+ }
261
+
262
+
263
+ def _structural_surface() -> dict:
264
+ return {
265
+ "main": "arka",
266
+ "hubs": sorted(
267
+ f"arka-{d.name}" for d in DEPARTMENTS_DIR.iterdir()
268
+ if (d / "SKILL.md").is_file()
269
+ ),
270
+ "meta": sorted(
271
+ f"arka-{p.name}" for p in (REPO_ROOT / "arka" / "skills").iterdir()
272
+ if (p / "SKILL.md").is_file()
273
+ ),
274
+ }
275
+
276
+
277
+ def generate() -> dict:
278
+ """Full generation. Returns summary counts for the CLI output."""
279
+ emitted = build_plugins()
280
+ marketplace = build_marketplace(emitted)
281
+ MARKETPLACE_JSON.write_text(
282
+ json.dumps(marketplace, indent=2, ensure_ascii=False) + "\n",
283
+ encoding="utf-8",
284
+ )
285
+ skills_manifest = build_skills_manifest(emitted)
286
+ SKILLS_MANIFEST_JSON.write_text(
287
+ json.dumps(skills_manifest, indent=2, ensure_ascii=False) + "\n",
288
+ encoding="utf-8",
289
+ )
290
+ return {
291
+ "plugins": len(emitted),
292
+ "plugin_skills": sum(len(s) for s in emitted.values()),
293
+ "curated": sum(len(s) for s in load_curated().values()),
294
+ "collisions": len(collision_slugs(dept_subskills())),
295
+ }
296
+
297
+
298
+ def main() -> int:
299
+ summary = generate()
300
+ print(
301
+ f"marketplace generated: {summary['plugins']} dept plugins, "
302
+ f"{summary['plugin_skills']} plugin skills, "
303
+ f"{summary['curated']} curated in core, "
304
+ f"{summary['collisions']} collision slugs (plugin-namespaced)"
305
+ )
306
+ return 0
307
+
308
+
309
+ if __name__ == "__main__":
310
+ raise SystemExit(main())
@@ -20,13 +20,12 @@ import re
20
20
  import subprocess
21
21
  import sys
22
22
  from pathlib import Path
23
- from typing import Optional
24
23
 
25
24
  _TEST_DEF_RE = re.compile(r"^\s*(?:async\s+)?def\s+test_\w+", re.MULTILINE)
26
25
  _COLLECTED_RE = re.compile(r"(\d+)\s+tests?\s+collected")
27
26
 
28
27
 
29
- def repo_root(start: Optional[Path] = None) -> Path:
28
+ def repo_root(start: Path | None = None) -> Path:
30
29
  """Find the repo root by walking up to a dir with VERSION + departments/."""
31
30
  cur = (start or Path(__file__).resolve()).resolve()
32
31
  candidates = [cur, *cur.parents] if cur.is_dir() else [cur.parent, *cur.parents]
@@ -64,8 +63,11 @@ def count_skills(root: Path) -> dict:
64
63
  return len(list(base.rglob("SKILL.md"))) if base.is_dir() else 0
65
64
 
66
65
  dept, arka, market = _n("departments"), _n("arka"), _n("marketplace")
66
+ # plugins/ is a GENERATED export-copy (scripts/marketplace_gen.py)
67
+ # of non-curated department skills — informative only, never part
68
+ # of the core headline (the sources remain under departments/).
67
69
  return {"departments": dept, "arka": arka, "marketplace": market,
68
- "core": dept + arka}
70
+ "plugins": _n("plugins"), "core": dept + arka}
69
71
 
70
72
 
71
73
  def count_adrs(root: Path) -> int:
@@ -83,7 +85,7 @@ def count_test_functions(root: Path) -> int:
83
85
  for f in tdir.rglob("test_*.py"))
84
86
 
85
87
 
86
- def collect_pytest_cases(root: Path) -> Optional[int]:
88
+ def collect_pytest_cases(root: Path) -> int | None:
87
89
  """Authoritative pytest case count via --collect-only. None on failure."""
88
90
  try:
89
91
  out = subprocess.run(
@@ -0,0 +1,209 @@
1
+ """Skill-budget linter — the curated default must stay small (F2-7c).
2
+
3
+ Measures the ALWAYS-ON context surface of the default skill install:
4
+ the frontmatter descriptions of every skill the curated cut deploys
5
+ (main /arka + 17 hubs + 14 meta + curated sub-skills, resolved from
6
+ ``knowledge/skills-manifest.json`` — the generated classification the
7
+ marketplace generator emits). Skill bodies are invoke-only and do not
8
+ count; descriptions are what the runtime can inject per session.
9
+
10
+ Honesty note, printed with every report: live sessions currently
11
+ observe a smaller footprint (~2.3k tokens) because the runtime
12
+ truncates most entries to name-only — but that truncation is
13
+ NON-CONTRACTUAL runtime behavior. This budget sizes the GUARANTEED
14
+ surface (chars of description; tokens estimated at chars/4).
15
+
16
+ Thresholds (calibrated against the measured 2026-07-12 baseline:
17
+ 69 skills, 34,929 chars):
18
+
19
+ skills WARN > 72 (90% of cap) FAIL > 80
20
+ total WARN > 36,000 chars FAIL > 40,000 (~10k tokens)
21
+ per-skill WARN > 800 chars FAIL > 1,024
22
+ structural completeness FAIL (flow & co. must be default)
23
+ curated slug in a collision set FAIL
24
+
25
+ CI gate: ``tests/python/test_skill_budget.py`` (repo-only, no install
26
+ needed). CLI: ``python scripts/tools/skill_budget.py [--json]`` —
27
+ exit 1 when any FAIL finding exists. stdlib only (docs_stats pattern).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ import re
35
+ import sys
36
+ from dataclasses import asdict, dataclass
37
+ from pathlib import Path
38
+
39
+ REPO_ROOT = Path(__file__).resolve().parents[2]
40
+ MANIFEST_PATH = REPO_ROOT / "knowledge" / "skills-manifest.json"
41
+
42
+ MAX_SKILLS_FAIL = 80
43
+ MAX_SKILLS_WARN = 72
44
+ TOTAL_CHARS_FAIL = 40_000
45
+ TOTAL_CHARS_WARN = 36_000
46
+ PER_SKILL_FAIL = 1_024
47
+ PER_SKILL_WARN = 800
48
+
49
+ _FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
50
+ _DESCRIPTION_RE = re.compile(r"^description:\s*(.*)$", re.MULTILINE)
51
+ _ROOT_KEY_RE = re.compile(r"^[A-Za-z_-]+:")
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Finding:
56
+ level: str # "FAIL" | "WARN"
57
+ check: str
58
+ detail: str
59
+
60
+
61
+ def frontmatter_description(skill_md: Path) -> str:
62
+ """Extract the description value, including folded (>) blocks."""
63
+ match = _FRONTMATTER_RE.match(skill_md.read_text(encoding="utf-8"))
64
+ if not match:
65
+ return ""
66
+ frontmatter = match.group(1)
67
+ desc = _DESCRIPTION_RE.search(frontmatter)
68
+ if not desc:
69
+ return ""
70
+ value = desc.group(1).strip()
71
+ if value not in (">", "|", ">-", "|-"):
72
+ return value
73
+ folded: list[str] = []
74
+ for line in frontmatter[desc.end():].split("\n"):
75
+ if line.startswith((" ", "\t")):
76
+ folded.append(line.strip())
77
+ elif _ROOT_KEY_RE.match(line):
78
+ break
79
+ return " ".join(folded).strip()
80
+
81
+
82
+ def default_surface(manifest: dict) -> dict[str, Path]:
83
+ """Deployed-name -> source SKILL.md for the curated default install."""
84
+ surface: dict[str, Path] = {"arka": REPO_ROOT / "arka" / "SKILL.md"}
85
+ for hub in manifest["structural"]["hubs"]:
86
+ dept = hub.removeprefix("arka-")
87
+ surface[hub] = REPO_ROOT / "departments" / dept / "SKILL.md"
88
+ for meta in manifest["structural"]["meta"]:
89
+ slug = meta.removeprefix("arka-")
90
+ surface[meta] = REPO_ROOT / "arka" / "skills" / slug / "SKILL.md"
91
+ for slug, entry in manifest["skills"].items():
92
+ if entry["curated"]:
93
+ dept = entry["depts"][0]
94
+ surface[f"arka-{slug}"] = (
95
+ REPO_ROOT / "departments" / dept / "skills" / slug / "SKILL.md"
96
+ )
97
+ return surface
98
+
99
+
100
+ def _threshold_findings(count: int, total: int) -> list[Finding]:
101
+ findings: list[Finding] = []
102
+ if count > MAX_SKILLS_FAIL:
103
+ findings.append(Finding(
104
+ "FAIL", "skill-count",
105
+ f"default install carries {count} skills (cap {MAX_SKILLS_FAIL})"))
106
+ elif count > MAX_SKILLS_WARN:
107
+ findings.append(Finding(
108
+ "WARN", "skill-count",
109
+ f"{count} skills — approaching the {MAX_SKILLS_FAIL} cap"))
110
+ if total > TOTAL_CHARS_FAIL:
111
+ findings.append(Finding(
112
+ "FAIL", "total-description-chars",
113
+ f"{total} chars of always-on descriptions "
114
+ f"(cap {TOTAL_CHARS_FAIL} ~= {TOTAL_CHARS_FAIL // 4} tokens)"))
115
+ elif total > TOTAL_CHARS_WARN:
116
+ findings.append(Finding(
117
+ "WARN", "total-description-chars",
118
+ f"{total} chars — approaching the {TOTAL_CHARS_FAIL} cap"))
119
+ return findings
120
+
121
+
122
+ def _structural_findings(manifest: dict) -> list[Finding]:
123
+ findings: list[Finding] = []
124
+ meta = set(manifest["structural"]["meta"])
125
+ for required in ("arka-flow", "arka-forge", "arka-fusion"):
126
+ if required not in meta:
127
+ findings.append(Finding(
128
+ "FAIL", "structural-completeness",
129
+ f"{required} missing from the structural default — "
130
+ f"evidence-flow and planning are NON-NEGOTIABLE"))
131
+ for slug, entry in manifest["skills"].items():
132
+ if entry["curated"] and entry["collision"]:
133
+ findings.append(Finding(
134
+ "FAIL", "curated-collision",
135
+ f"curated slug {slug!r} is a later-wins collision — it "
136
+ f"must live only in plugins"))
137
+ return findings
138
+
139
+
140
+ def audit(manifest_path: Path | None = None) -> dict:
141
+ """Full audit. Returns {summary, findings, note}.
142
+
143
+ ``manifest_path`` resolves the module attribute at CALL time so
144
+ tests can monkeypatch ``MANIFEST_PATH`` (a def-time default would
145
+ freeze the original binding).
146
+ """
147
+ manifest_path = manifest_path or MANIFEST_PATH
148
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
149
+ surface = default_surface(manifest)
150
+ sizes = {
151
+ name: len(frontmatter_description(path))
152
+ for name, path in surface.items() if path.is_file()
153
+ }
154
+ missing = sorted(set(surface) - set(sizes))
155
+ total = sum(sizes.values())
156
+ findings = _threshold_findings(len(surface), total)
157
+ findings.extend(_structural_findings(manifest))
158
+ for name in missing:
159
+ findings.append(Finding(
160
+ "FAIL", "missing-source", f"{name}: source SKILL.md not found"))
161
+ for name, size in sorted(sizes.items(), key=lambda kv: -kv[1]):
162
+ if size > PER_SKILL_FAIL:
163
+ findings.append(Finding(
164
+ "FAIL", "per-skill-description",
165
+ f"{name}: {size} chars (cap {PER_SKILL_FAIL})"))
166
+ elif size > PER_SKILL_WARN:
167
+ findings.append(Finding(
168
+ "WARN", "per-skill-description",
169
+ f"{name}: {size} chars (warn over {PER_SKILL_WARN})"))
170
+ return {
171
+ "summary": {
172
+ "skills": len(surface),
173
+ "total_description_chars": total,
174
+ "estimated_tokens": total // 4,
175
+ "fails": sum(1 for f in findings if f.level == "FAIL"),
176
+ "warns": sum(1 for f in findings if f.level == "WARN"),
177
+ },
178
+ "findings": [asdict(f) for f in findings],
179
+ "note": (
180
+ "Budget sizes the GUARANTEED always-on surface (description "
181
+ "chars). Live sessions may observe less (~2.3k tokens) because "
182
+ "the runtime truncates entries to name-only — that truncation "
183
+ "is non-contractual and not relied upon."
184
+ ),
185
+ }
186
+
187
+
188
+ def main(argv: list[str] | None = None) -> int:
189
+ parser = argparse.ArgumentParser(
190
+ prog="python scripts/tools/skill_budget.py",
191
+ description="Audit the curated default skill surface. Exit 1 on FAIL.",
192
+ )
193
+ parser.add_argument("--json", action="store_true", dest="as_json")
194
+ args = parser.parse_args(argv)
195
+ report = audit()
196
+ if args.as_json:
197
+ print(json.dumps(report, indent=2))
198
+ else:
199
+ s = report["summary"]
200
+ print(f"skills: {s['skills']} | always-on description chars: "
201
+ f"{s['total_description_chars']} (~{s['estimated_tokens']} tokens)")
202
+ for f in report["findings"]:
203
+ print(f" [{f['level']}] {f['check']}: {f['detail']}")
204
+ print(report["note"])
205
+ return 1 if report["summary"]["fails"] else 0
206
+
207
+
208
+ if __name__ == "__main__":
209
+ sys.exit(main())