arkaos 4.22.0 → 4.24.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 (256) hide show
  1. package/README.md +1 -1
  2. package/THE-ARKAOS-GUIDE.md +72 -0
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +29 -7
  5. package/arka/skills/bootstrap-agent/SKILL.md +3 -2
  6. package/arka/skills/conclave/SKILL.md +11 -0
  7. package/arka/skills/flow/SKILL.md +3 -2
  8. package/arka/skills/forge/SKILL.md +3 -2
  9. package/arka/skills/recipes/SKILL.md +3 -2
  10. package/arka/skills/refine/SKILL.md +3 -2
  11. package/arka/skills/research/SKILL.md +3 -2
  12. package/bin/arka-doctor +8 -0
  13. package/core/cognition/insights/store.py +15 -4
  14. package/core/forge/persistence.py +27 -9
  15. package/core/forge/schema.py +45 -22
  16. package/core/governance/evidence_checks.py +101 -9
  17. package/core/governance/kb_cite_check.py +16 -6
  18. package/core/governance/stop_lint.py +38 -11
  19. package/core/governance/stop_lint_telemetry.py +11 -2
  20. package/core/hooks/user_prompt_submit.py +21 -23
  21. package/core/synapse/engine.py +46 -16
  22. package/core/synapse/graph_context_layer.py +7 -2
  23. package/core/synapse/kb_cache.py +63 -16
  24. package/core/synapse/layers.py +93 -30
  25. package/core/synapse/layers_base.py +16 -0
  26. package/core/synapse/layers_kb.py +325 -19
  27. package/dashboard/app/layouts/default.vue +7 -0
  28. package/dashboard/app/pages/plan-canvas.vue +630 -0
  29. package/departments/brand/SKILL.md +3 -2
  30. package/departments/brand/skills/archetype-finder/SKILL.md +3 -2
  31. package/departments/brand/skills/colors/SKILL.md +3 -2
  32. package/departments/brand/skills/design-system/SKILL.md +3 -2
  33. package/departments/brand/skills/identity-system/SKILL.md +3 -2
  34. package/departments/brand/skills/logo-brief/SKILL.md +3 -2
  35. package/departments/brand/skills/mockup-generate/SKILL.md +3 -2
  36. package/departments/brand/skills/naming-evaluate/SKILL.md +3 -2
  37. package/departments/brand/skills/positioning-statement/SKILL.md +3 -2
  38. package/departments/brand/skills/primal-audit/SKILL.md +3 -2
  39. package/departments/brand/skills/ux-audit/SKILL.md +3 -2
  40. package/departments/brand/skills/voice-guide/SKILL.md +3 -2
  41. package/departments/brand/skills/wireframe/SKILL.md +3 -2
  42. package/departments/community/SKILL.md +3 -2
  43. package/departments/community/skills/ai-community/SKILL.md +3 -2
  44. package/departments/community/skills/betting-setup/SKILL.md +3 -2
  45. package/departments/community/skills/business-model/SKILL.md +3 -2
  46. package/departments/community/skills/content-calendar/SKILL.md +3 -2
  47. package/departments/community/skills/events-plan/SKILL.md +3 -2
  48. package/departments/community/skills/gamification-design/SKILL.md +3 -2
  49. package/departments/community/skills/growth-plan/SKILL.md +3 -2
  50. package/departments/community/skills/metrics-track/SKILL.md +3 -2
  51. package/departments/community/skills/moderation/SKILL.md +3 -2
  52. package/departments/community/skills/monetize-plan/SKILL.md +3 -2
  53. package/departments/community/skills/niche-setup/SKILL.md +3 -2
  54. package/departments/community/skills/onboarding-flow/SKILL.md +3 -2
  55. package/departments/community/skills/platform-select/SKILL.md +3 -2
  56. package/departments/content/SKILL.md +3 -2
  57. package/departments/content/skills/ai-workflow/SKILL.md +3 -2
  58. package/departments/content/skills/analytics/SKILL.md +3 -2
  59. package/departments/content/skills/calendar/SKILL.md +3 -2
  60. package/departments/content/skills/content-system/SKILL.md +3 -2
  61. package/departments/content/skills/monetization-plan/SKILL.md +3 -2
  62. package/departments/content/skills/newsletter-write/SKILL.md +3 -2
  63. package/departments/content/skills/platform-optimize/SKILL.md +3 -2
  64. package/departments/content/skills/repurpose-plan/SKILL.md +3 -2
  65. package/departments/content/skills/research-compile/SKILL.md +3 -2
  66. package/departments/content/skills/script-structure/SKILL.md +3 -2
  67. package/departments/content/skills/short-form/SKILL.md +3 -2
  68. package/departments/content/skills/thumbnail-package/SKILL.md +3 -2
  69. package/departments/content/skills/trend-hunt/SKILL.md +3 -2
  70. package/departments/content/skills/video-produce/SKILL.md +3 -2
  71. package/departments/content/skills/video-setup/SKILL.md +3 -2
  72. package/departments/content/skills/viral-design/SKILL.md +3 -2
  73. package/departments/content/skills/youtube-strategy/SKILL.md +3 -2
  74. package/departments/dev/SKILL.md +3 -2
  75. package/departments/dev/skills/ai-assisted-dev/SKILL.md +3 -2
  76. package/departments/dev/skills/animated-website/SKILL.md +7 -0
  77. package/departments/dev/skills/architecture-design/SKILL.md +3 -2
  78. package/departments/dev/skills/code-review/SKILL.md +3 -2
  79. package/departments/dev/skills/db-design/SKILL.md +3 -2
  80. package/departments/dev/skills/ddd-model/SKILL.md +3 -2
  81. package/departments/dev/skills/demo-gif/SKILL.md +3 -2
  82. package/departments/dev/skills/deploy/SKILL.md +3 -2
  83. package/departments/dev/skills/devops-pipeline/SKILL.md +3 -2
  84. package/departments/dev/skills/docs/SKILL.md +3 -2
  85. package/departments/dev/skills/evaluator-build-loop/SKILL.md +93 -0
  86. package/departments/dev/skills/mcp/SKILL.md +3 -2
  87. package/departments/dev/skills/performance-audit/SKILL.md +3 -2
  88. package/departments/dev/skills/refactor-plan/SKILL.md +3 -2
  89. package/departments/dev/skills/research/SKILL.md +3 -2
  90. package/departments/dev/skills/security-compliance/SKILL.md +3 -2
  91. package/departments/dev/skills/spec-miner/SKILL.md +86 -0
  92. package/departments/dev/skills/stack-check/SKILL.md +3 -2
  93. package/departments/ecom/SKILL.md +3 -2
  94. package/departments/ecom/skills/analytics/SKILL.md +3 -2
  95. package/departments/ecom/skills/browse-competitor/SKILL.md +3 -2
  96. package/departments/ecom/skills/cart-recovery/SKILL.md +3 -2
  97. package/departments/ecom/skills/cro-optimize/SKILL.md +3 -2
  98. package/departments/ecom/skills/customer-journey/SKILL.md +3 -2
  99. package/departments/ecom/skills/fulfillment-plan/SKILL.md +3 -2
  100. package/departments/ecom/skills/marketplace-manage/SKILL.md +3 -2
  101. package/departments/ecom/skills/pricing-strategy/SKILL.md +3 -2
  102. package/departments/ecom/skills/product-launch/SKILL.md +3 -2
  103. package/departments/ecom/skills/rfm-segment/SKILL.md +3 -2
  104. package/departments/ecom/skills/social-commerce/SKILL.md +3 -2
  105. package/departments/ecom/skills/store-audit/SKILL.md +3 -2
  106. package/departments/ecom/skills/subscription-model/SKILL.md +3 -2
  107. package/departments/finance/SKILL.md +3 -2
  108. package/departments/finance/skills/budget-plan/SKILL.md +3 -2
  109. package/departments/finance/skills/cashflow-forecast/SKILL.md +3 -2
  110. package/departments/finance/skills/ciso-advisor/SKILL.md +3 -2
  111. package/departments/finance/skills/financial-model/SKILL.md +3 -2
  112. package/departments/finance/skills/pitch-deck/SKILL.md +3 -2
  113. package/departments/finance/skills/scenario-analysis/SKILL.md +3 -2
  114. package/departments/finance/skills/unit-economics/SKILL.md +3 -2
  115. package/departments/finance/skills/valuation-model/SKILL.md +3 -2
  116. package/departments/kb/SKILL.md +3 -2
  117. package/departments/kb/skills/ai-research/SKILL.md +3 -2
  118. package/departments/kb/skills/competitive-intel/SKILL.md +3 -2
  119. package/departments/kb/skills/doc-extraction/SKILL.md +3 -2
  120. package/departments/kb/skills/doc-redaction/SKILL.md +3 -2
  121. package/departments/kb/skills/knowledge-ops/SKILL.md +3 -2
  122. package/departments/kb/skills/knowledge-review/SKILL.md +3 -2
  123. package/departments/kb/skills/learn-content/SKILL.md +3 -2
  124. package/departments/kb/skills/moc-create/SKILL.md +3 -2
  125. package/departments/kb/skills/persona-build/SKILL.md +3 -2
  126. package/departments/kb/skills/research-plan/SKILL.md +3 -2
  127. package/departments/kb/skills/search-kb/SKILL.md +3 -2
  128. package/departments/kb/skills/source-evaluate/SKILL.md +3 -2
  129. package/departments/kb/skills/taxonomy-manage/SKILL.md +3 -2
  130. package/departments/kb/skills/write-as-persona/SKILL.md +3 -2
  131. package/departments/landing/SKILL.md +3 -2
  132. package/departments/landing/skills/ab-test/SKILL.md +3 -2
  133. package/departments/landing/skills/affiliate-bridge/SKILL.md +3 -2
  134. package/departments/landing/skills/awareness-diagnose/SKILL.md +3 -2
  135. package/departments/landing/skills/email-sequence/SKILL.md +3 -2
  136. package/departments/landing/skills/funnel-metrics/SKILL.md +3 -2
  137. package/departments/landing/skills/headline-write/SKILL.md +3 -2
  138. package/departments/landing/skills/launch-sequence/SKILL.md +3 -2
  139. package/departments/landing/skills/offer-create/SKILL.md +3 -2
  140. package/departments/landing/skills/optimize-page/SKILL.md +3 -2
  141. package/departments/landing/skills/page-architect/SKILL.md +3 -2
  142. package/departments/landing/skills/persuasion-apply/SKILL.md +3 -2
  143. package/departments/landing/skills/webinar-funnel/SKILL.md +3 -2
  144. package/departments/leadership/SKILL.md +3 -2
  145. package/departments/leadership/skills/change-manage/SKILL.md +3 -2
  146. package/departments/leadership/skills/conflict-resolve/SKILL.md +3 -2
  147. package/departments/leadership/skills/culture-audit/SKILL.md +3 -2
  148. package/departments/leadership/skills/delegation-matrix/SKILL.md +3 -2
  149. package/departments/leadership/skills/disc-assess/SKILL.md +3 -2
  150. package/departments/leadership/skills/feedback-give/SKILL.md +3 -2
  151. package/departments/leadership/skills/hiring-plan/SKILL.md +3 -2
  152. package/departments/leadership/skills/performance-review/SKILL.md +3 -2
  153. package/departments/marketing/SKILL.md +3 -2
  154. package/departments/marketing/skills/ab-test/SKILL.md +3 -2
  155. package/departments/marketing/skills/analytics-report/SKILL.md +3 -2
  156. package/departments/marketing/skills/audience-segment/SKILL.md +3 -2
  157. package/departments/marketing/skills/calendar-plan/SKILL.md +3 -2
  158. package/departments/marketing/skills/competitor-analysis/SKILL.md +3 -2
  159. package/departments/marketing/skills/content-audit/SKILL.md +3 -2
  160. package/departments/marketing/skills/email-sequence/SKILL.md +3 -2
  161. package/departments/marketing/skills/growth-loop/SKILL.md +3 -2
  162. package/departments/marketing/skills/marketing-automation/SKILL.md +3 -2
  163. package/departments/marketing/skills/paid-campaign/SKILL.md +3 -2
  164. package/departments/marketing/skills/programmatic-seo/SKILL.md +3 -2
  165. package/departments/marketing/skills/seo-audit/SKILL.md +3 -2
  166. package/departments/marketing/skills/social-strategy/SKILL.md +3 -2
  167. package/departments/ops/SKILL.md +3 -2
  168. package/departments/ops/skills/bottleneck-find/SKILL.md +3 -2
  169. package/departments/ops/skills/dashboard-build/SKILL.md +3 -2
  170. package/departments/ops/skills/gdpr-compliance/SKILL.md +3 -2
  171. package/departments/ops/skills/github-ops/SKILL.md +3 -2
  172. package/departments/ops/skills/gtd-setup/SKILL.md +3 -2
  173. package/departments/ops/skills/integration-design/SKILL.md +3 -2
  174. package/departments/ops/skills/iso27001/SKILL.md +3 -2
  175. package/departments/ops/skills/lean-audit/SKILL.md +3 -2
  176. package/departments/ops/skills/metrics-dashboard/SKILL.md +3 -2
  177. package/departments/ops/skills/n8n-flow/SKILL.md +3 -2
  178. package/departments/ops/skills/quality-management/SKILL.md +3 -2
  179. package/departments/ops/skills/risk-management/SKILL.md +3 -2
  180. package/departments/ops/skills/soc2-compliance/SKILL.md +3 -2
  181. package/departments/ops/skills/sop-create/SKILL.md +3 -2
  182. package/departments/ops/skills/terminal-ops/SKILL.md +3 -2
  183. package/departments/ops/skills/workflow-automate/SKILL.md +3 -2
  184. package/departments/ops/skills/zapier-flow/SKILL.md +3 -2
  185. package/departments/org/SKILL.md +3 -2
  186. package/departments/org/skills/compensation-plan/SKILL.md +3 -2
  187. package/departments/org/skills/culture-define/SKILL.md +3 -2
  188. package/departments/org/skills/decision-framework/SKILL.md +3 -2
  189. package/departments/org/skills/hiring-plan/SKILL.md +3 -2
  190. package/departments/org/skills/meeting-optimize/SKILL.md +3 -2
  191. package/departments/org/skills/onboarding-design/SKILL.md +3 -2
  192. package/departments/org/skills/org-design/SKILL.md +3 -2
  193. package/departments/org/skills/remote-setup/SKILL.md +3 -2
  194. package/departments/org/skills/sop-process/SKILL.md +3 -2
  195. package/departments/org/skills/team-assess/SKILL.md +3 -2
  196. package/departments/pm/SKILL.md +3 -2
  197. package/departments/pm/skills/backlog-groom/SKILL.md +3 -2
  198. package/departments/pm/skills/discovery-plan/SKILL.md +3 -2
  199. package/departments/pm/skills/estimate-forecast/SKILL.md +3 -2
  200. package/departments/pm/skills/impact-map/SKILL.md +3 -2
  201. package/departments/pm/skills/kanban-setup/SKILL.md +3 -2
  202. package/departments/pm/skills/risk-register/SKILL.md +3 -2
  203. package/departments/pm/skills/roadmap-build/SKILL.md +3 -2
  204. package/departments/pm/skills/sprint-plan/SKILL.md +3 -2
  205. package/departments/pm/skills/stakeholder-map/SKILL.md +3 -2
  206. package/departments/pm/skills/standup-run/SKILL.md +3 -2
  207. package/departments/pm/skills/story-write/SKILL.md +3 -2
  208. package/departments/saas/SKILL.md +3 -2
  209. package/departments/saas/skills/benchmark-compare/SKILL.md +3 -2
  210. package/departments/saas/skills/churn-analysis/SKILL.md +3 -2
  211. package/departments/saas/skills/customer-success/SKILL.md +3 -2
  212. package/departments/saas/skills/growth-plan/SKILL.md +3 -2
  213. package/departments/saas/skills/gtm-strategy/SKILL.md +3 -2
  214. package/departments/saas/skills/launch-execute/SKILL.md +3 -2
  215. package/departments/saas/skills/metrics-dashboard/SKILL.md +3 -2
  216. package/departments/saas/skills/micro-saas-stack/SKILL.md +3 -2
  217. package/departments/saas/skills/mvp-build/SKILL.md +3 -2
  218. package/departments/saas/skills/niche-evaluate/SKILL.md +3 -2
  219. package/departments/saas/skills/onboarding-optimize/SKILL.md +3 -2
  220. package/departments/saas/skills/plg-setup/SKILL.md +3 -2
  221. package/departments/saas/skills/pricing-strategy/SKILL.md +3 -2
  222. package/departments/saas/skills/validate-idea/SKILL.md +3 -2
  223. package/departments/sales/SKILL.md +3 -2
  224. package/departments/sales/skills/challenger-sell/SKILL.md +3 -2
  225. package/departments/sales/skills/deal-qualify/SKILL.md +3 -2
  226. package/departments/sales/skills/discovery-call/SKILL.md +3 -2
  227. package/departments/sales/skills/forecast-revenue/SKILL.md +3 -2
  228. package/departments/sales/skills/negotiate-plan/SKILL.md +3 -2
  229. package/departments/sales/skills/objection-handle/SKILL.md +3 -2
  230. package/departments/sales/skills/pipeline-manage/SKILL.md +3 -2
  231. package/departments/sales/skills/pricing-negotiate/SKILL.md +3 -2
  232. package/departments/strategy/SKILL.md +3 -2
  233. package/departments/strategy/skills/blue-ocean/SKILL.md +3 -2
  234. package/departments/strategy/skills/bmc/SKILL.md +3 -2
  235. package/departments/strategy/skills/board-advisor/SKILL.md +3 -2
  236. package/departments/strategy/skills/cto-advisor/SKILL.md +3 -2
  237. package/departments/strategy/skills/extract-data/SKILL.md +3 -2
  238. package/departments/strategy/skills/five-forces/SKILL.md +3 -2
  239. package/departments/strategy/skills/growth-strategy/SKILL.md +3 -2
  240. package/departments/strategy/skills/moat-analysis/SKILL.md +3 -2
  241. package/departments/strategy/skills/position/SKILL.md +3 -2
  242. package/departments/strategy/skills/scenario-plan/SKILL.md +3 -2
  243. package/installer/config-seed.js +81 -25
  244. package/installer/graphify.js +340 -0
  245. package/installer/index.js +8 -1
  246. package/installer/keys.js +18 -3
  247. package/installer/update.js +8 -1
  248. package/knowledge/skills-manifest.json +27 -1
  249. package/mcps/registry.json +9 -9
  250. package/mcps/scripts/apply-mcps.sh +6 -2
  251. package/package.json +2 -1
  252. package/pyproject.toml +1 -1
  253. package/scripts/dashboard-api.py +336 -124
  254. package/scripts/guide_gen.py +139 -0
  255. package/scripts/migrate_skills_kb_first.py +3 -2
  256. package/scripts/synapse-bridge.py +9 -1
@@ -23,6 +23,8 @@ CLI (for hooks/skills)::
23
23
  from __future__ import annotations
24
24
 
25
25
  import argparse
26
+ import configparser
27
+ import fnmatch
26
28
  import importlib.util
27
29
  import json
28
30
  import re
@@ -31,7 +33,7 @@ import shutil
31
33
  import subprocess
32
34
  import sys
33
35
  import time
34
- from dataclasses import asdict, dataclass, field
36
+ from dataclasses import asdict, dataclass, field, replace
35
37
  from pathlib import Path
36
38
 
37
39
  from core.shared.test_evidence import coverage_percent_from_xml
@@ -218,18 +220,22 @@ def _labelled(result: CheckResult, label: str) -> CheckResult:
218
220
  return result
219
221
 
220
222
 
221
- def _tool_cmd(tool: str) -> list[str] | None:
223
+ def _tool_cmd(tool: str, module: str | None = None) -> list[str] | None:
222
224
  """Resolve a Python tool: PATH binary, else the interpreter's module.
223
225
 
224
226
  Operator machines install ruff/pytest into the ArkaOS venv with no
225
227
  PATH binary — keying on shutil.which alone silently downgraded
226
228
  Python lint to eslint over installer/*.js, a FALSE GREEN on a
227
229
  NON-NEGOTIABLE gate (QG findings, F1-B2/F1-C1 reviews).
230
+
231
+ ``module`` covers tools whose import name differs from the command
232
+ (codespell ships ``codespell_lib``); without it the module fallback
233
+ misses and the check skips forever on a venv install.
228
234
  """
229
235
  if shutil.which(tool):
230
236
  return [tool]
231
- if importlib.util.find_spec(tool) is not None:
232
- return [sys.executable, "-m", tool]
237
+ if importlib.util.find_spec(module or tool) is not None:
238
+ return [sys.executable, "-m", module or tool]
233
239
  return None
234
240
 
235
241
 
@@ -335,15 +341,59 @@ def _check_typecheck(
335
341
  return _skip("typecheck", "no typecheck configuration detected")
336
342
 
337
343
 
344
+ def _project_pytest(project_dir: Path) -> list[str] | None:
345
+ """pytest belonging to the target project's own environment."""
346
+ for rel in (".venv/bin/pytest", "venv/bin/pytest"):
347
+ candidate = project_dir / rel
348
+ if candidate.is_file():
349
+ return [str(candidate)]
350
+ return None
351
+
352
+
353
+ def _foreign_pytest_can_collect(
354
+ pytest_cmd: list[str], project_dir: Path, timeout: int,
355
+ ) -> bool:
356
+ """A PATH/venv-foreign pytest must prove it can import the project.
357
+
358
+ Running a foreign-env pytest blind produced false FAILs on projects
359
+ whose dependencies live in their own venv (import errors read as
360
+ test failures — issue #283, a blind gate on mandatory-qa). Exit 5
361
+ ("no tests collected") still counts as a working import path.
362
+ """
363
+ try:
364
+ probe = subprocess.run(
365
+ [*pytest_cmd, "--collect-only", "-q"], cwd=project_dir,
366
+ capture_output=True, text=True, timeout=min(timeout, 60),
367
+ )
368
+ except (OSError, subprocess.TimeoutExpired):
369
+ return False
370
+ return probe.returncode in (0, 5)
371
+
372
+
338
373
  def _check_tests(
339
374
  project_dir: Path, changed: list[str] | None,
340
375
  test_command: str | None, timeout: int,
341
376
  ) -> CheckResult:
342
377
  if test_command:
343
378
  return _run("tests", shlex.split(test_command), project_dir, timeout)
344
- pytest_cmd = _tool_cmd("pytest")
345
- if _has_python(project_dir, changed) and pytest_cmd:
346
- return _run("tests", [*pytest_cmd, "-q"], project_dir, timeout)
379
+ if _has_python(project_dir, changed):
380
+ local_pytest = _project_pytest(project_dir)
381
+ if local_pytest:
382
+ return _labelled(
383
+ _run("tests", [*local_pytest, "-q"], project_dir, timeout),
384
+ "tests(project-venv)",
385
+ )
386
+ pytest_cmd = _tool_cmd("pytest")
387
+ if pytest_cmd and _foreign_pytest_can_collect(
388
+ pytest_cmd, project_dir, timeout
389
+ ):
390
+ return _run("tests", [*pytest_cmd, "-q"], project_dir, timeout)
391
+ if pytest_cmd:
392
+ return _skip(
393
+ "tests",
394
+ "pytest resolved outside the project env cannot import "
395
+ "it — pin --test-command",
396
+ )
347
397
  if _package_json_script(project_dir, "test"):
348
398
  return _run(
349
399
  "tests", ["npm", "test", "--silent"], project_dir, timeout,
@@ -501,12 +551,54 @@ def _check_spellcheck(
501
551
  project_dir: Path, changed: list[str] | None,
502
552
  test_command: str | None, timeout: int,
503
553
  ) -> CheckResult:
504
- if not shutil.which("codespell"):
554
+ # Resolve like lint/tests do. A bare shutil.which lookup left this check
555
+ # dark on venv installs (the binary is not on the harness PATH), so three
556
+ # consecutive QG rounds shipped with NO machine spellcheck signal.
557
+ cmd = _tool_cmd("codespell", module="codespell_lib")
558
+ if cmd is None:
505
559
  return _skip("spellcheck", "codespell not installed")
506
560
  md_files = [f for f in changed or [] if f.endswith(".md")]
507
561
  if not md_files:
508
562
  return _skip("spellcheck", "no changed .md files")
509
- return _run("spellcheck", ["codespell", *md_files], project_dir, timeout)
563
+ result = _run("spellcheck", [*cmd, *md_files], project_dir, timeout)
564
+ # codespell honours .codespellrc `skip` even for explicitly listed files,
565
+ # so a PASS here can cover far fewer files than were handed in. Reporting
566
+ # the raw count as coverage is how a narrowed scope reads as a full green
567
+ # (QG finding: 8 of 220 inspected, reported as 220). State both.
568
+ inspected = _spellcheck_inspected_count(project_dir, md_files)
569
+ scope = f"{inspected} of {len(md_files)} changed .md inspected"
570
+ if inspected < len(md_files):
571
+ scope += f"; {len(md_files) - inspected} excluded by .codespellrc skip"
572
+ summary = f"{scope}\n{result.summary}" if result.summary else scope
573
+ return replace(result, summary=summary)
574
+
575
+
576
+ def _spellcheck_inspected_count(project_dir: Path, md_files: list[str]) -> int:
577
+ """How many of ``md_files`` codespell actually reads after `skip`.
578
+
579
+ Asks codespell itself (``--count`` on a per-file basis is too slow; a
580
+ skipped file simply produces no output for any planted probe), so instead
581
+ we replay its own glob semantics from the config.
582
+ """
583
+ patterns = _codespell_skip_globs(project_dir)
584
+ if not patterns:
585
+ return len(md_files)
586
+ return sum(
587
+ 1 for f in md_files
588
+ if not any(fnmatch.fnmatch(f, p) or fnmatch.fnmatch(f"./{f}", p) for p in patterns)
589
+ )
590
+
591
+
592
+ def _codespell_skip_globs(project_dir: Path) -> list[str]:
593
+ """`skip` globs from .codespellrc, or [] when unreadable/absent."""
594
+ cfg = project_dir / ".codespellrc"
595
+ try:
596
+ parser = configparser.ConfigParser()
597
+ parser.read(cfg, encoding="utf-8")
598
+ raw = parser.get("codespell", "skip", fallback="")
599
+ except (OSError, configparser.Error):
600
+ return []
601
+ return [p.strip() for p in raw.split(",") if p.strip()]
510
602
 
511
603
 
512
604
  def _check_ui_screenshot(
@@ -20,6 +20,15 @@ from dataclasses import dataclass
20
20
 
21
21
  _WIKILINK_PATTERN: re.Pattern[str] = re.compile(r"\[\[([^\[\]]+)\]\]")
22
22
  _KNOWLEDGE_PATTERN: re.Pattern[str] = re.compile(r"\[knowledge:", re.IGNORECASE)
23
+ # Graphify knowledge-graph citation: the `[graph:` marker ONLY (parallel to
24
+ # `[knowledge:`), which stands for a node actually returned by the graph.
25
+ # The tool NAME is deliberately excluded: naming a tool is narration, not
26
+ # evidence, and the literal `mcp__graphify__query_graph` appears in the
27
+ # KB-first pointer of every migrated SKILL.md — crediting it would let any
28
+ # response that merely echoes the doctrine self-certify. Same asymmetry as
29
+ # `mcp__obsidian`, which is a topic keyword and earns credit only via a real
30
+ # `[[wikilink]]`.
31
+ _GRAPHIFY_PATTERN: re.Pattern[str] = re.compile(r"\[graph:", re.IGNORECASE)
23
32
  # File:line — requires at least one path separator. Quantifiers are bounded
24
33
  # to prevent catastrophic backtracking on pathological input (PR18 security
25
34
  # review: unbounded variant blew past Stop's 5s budget by ~8x on 100KB of
@@ -41,8 +50,8 @@ _ARKAOS_KEYWORDS: frozenset[str] = frozenset({
41
50
  "ines", "rafael", "beatriz", "miguel", "rodrigo",
42
51
  "non-negotiable", "tier 0", "squad lead",
43
52
  "core/governance", "core/synapse", "core/cognition",
44
- "core/workflow", "mcp__obsidian", "kb-first",
45
- "departments/", "agent yaml", "obsidian vault",
53
+ "core/workflow", "mcp__obsidian", "mcp__graphify", "kb-first",
54
+ "departments/", "agent yaml", "obsidian vault", "graphify",
46
55
  })
47
56
 
48
57
  _BYPASS_DEFAULTS: tuple[str, ...] = ("[arka:trivial]",)
@@ -50,8 +59,8 @@ _TRIVIAL_WORD_THRESHOLD: int = 15
50
59
  _TOPIC_THRESHOLD: float = 0.4
51
60
  _SUGGESTION_TEXT: str = (
52
61
  "KB-first — last response had no citation on ArkaOS topic. "
53
- "Use @[[note-name]], /kb search, or mcp__obsidian__search_notes "
54
- "to ground the next answer."
62
+ "Use @[[note-name]], /kb search, mcp__obsidian__search_notes, or "
63
+ "mcp__graphify__query_graph to ground the next answer."
55
64
  )
56
65
 
57
66
 
@@ -111,13 +120,14 @@ def _is_trivial_length(text: str) -> bool:
111
120
  def _count_citations(text: str) -> int:
112
121
  # Truncate to _MAX_SCAN_CHARS first; the file-line regex is the only
113
122
  # backtracking-prone pattern and the bounded quantifiers (capped at 8
114
- # segments × 64 chars each) combined with this slice make all three
123
+ # segments x 64 chars each) combined with this slice make all three
115
124
  # patterns safe to run unconditionally below.
116
125
  scan = text if len(text) <= _MAX_SCAN_CHARS else text[:_MAX_SCAN_CHARS]
117
126
  wikilinks = len(_WIKILINK_PATTERN.findall(scan))
118
127
  knowledge = 1 if _KNOWLEDGE_PATTERN.search(scan) else 0
128
+ graphify = 1 if _GRAPHIFY_PATTERN.search(scan) else 0
119
129
  files = len(_FILE_LINE_PATTERN.findall(scan))
120
- return wikilinks + knowledge + files
130
+ return wikilinks + knowledge + graphify + files
121
131
 
122
132
 
123
133
  def _compute_topic_score(text: str, keywords: frozenset[str]) -> float:
@@ -84,6 +84,29 @@ def _git(args: list[str], cwd: Path) -> subprocess.CompletedProcess | None:
84
84
  return None
85
85
 
86
86
 
87
+ def _porcelain_entries(stdout: str) -> list[tuple[str, str]]:
88
+ """(status, path) pairs from ``git status --porcelain -z`` output.
89
+
90
+ ``-z`` terminates entries with NUL and never quotes paths (the
91
+ line-based form octal-escapes non-ASCII and quoted names, which
92
+ broke path matching). Rename/copy entries carry a second
93
+ NUL-separated field — the original path — which is skipped.
94
+ """
95
+ entries: list[tuple[str, str]] = []
96
+ tokens = stdout.split("\0")
97
+ index = 0
98
+ while index < len(tokens):
99
+ token = tokens[index]
100
+ index += 1
101
+ if len(token) < 4:
102
+ continue
103
+ status, path = token[:2], token[3:]
104
+ if status[0] in "RC":
105
+ index += 1 # skip the original-path field
106
+ entries.append((status, path))
107
+ return entries
108
+
109
+
87
110
  def changed_files(project_dir: Path) -> list[str]:
88
111
  """Repo-relative files changed vs the merge-base, plus untracked."""
89
112
  try:
@@ -93,15 +116,15 @@ def changed_files(project_dir: Path) -> list[str]:
93
116
  if base is None:
94
117
  return []
95
118
  out: list[str] = []
96
- diff = _git(["diff", "--name-only", base], project_dir)
119
+ diff = _git(["diff", "--name-only", "-z", base], project_dir)
97
120
  if diff is not None and diff.returncode == 0:
98
- out.extend(f.strip() for f in diff.stdout.splitlines() if f.strip())
99
- status = _git(["status", "--porcelain"], project_dir)
121
+ out.extend(f for f in diff.stdout.split("\0") if f.strip())
122
+ status = _git(["status", "--porcelain", "-z"], project_dir)
100
123
  if status is not None and status.returncode == 0:
101
124
  out.extend(
102
- line[3:].strip()
103
- for line in status.stdout.splitlines()
104
- if line.startswith("??") and line[3:].strip()
125
+ path
126
+ for entry_status, path in _porcelain_entries(status.stdout)
127
+ if entry_status == "??" and path.strip()
105
128
  )
106
129
  seen: set[str] = set()
107
130
  return [f for f in out if not (f in seen or seen.add(f))]
@@ -109,7 +132,7 @@ def changed_files(project_dir: Path) -> list[str]:
109
132
 
110
133
  def _fingerprint(project_dir: Path) -> str | None:
111
134
  head = _git(["rev-parse", "HEAD"], project_dir)
112
- status = _git(["status", "--porcelain"], project_dir)
135
+ status = _git(["status", "--porcelain", "-z"], project_dir)
113
136
  if head is None or head.returncode != 0 or status is None:
114
137
  return None
115
138
  digest = hashlib.sha1()
@@ -117,9 +140,9 @@ def _fingerprint(project_dir: Path) -> str | None:
117
140
  # Porcelain alone only says WHICH files are dirty — editing an
118
141
  # already-dirty file leaves it unchanged. Fold in mtime+size so
119
142
  # content edits re-arm the worker.
120
- for line in sorted(status.stdout.splitlines()):
121
- digest.update(line.encode("utf-8", "replace"))
122
- digest.update(_stat_token(project_dir / line[3:].strip()))
143
+ for entry_status, path in sorted(_porcelain_entries(status.stdout)):
144
+ digest.update(f"{entry_status} {path}".encode("utf-8", "replace"))
145
+ digest.update(_stat_token(project_dir / path))
123
146
  return digest.hexdigest()
124
147
 
125
148
 
@@ -151,9 +174,13 @@ def _remember_fingerprint(state_file: Path, fingerprint: str) -> None:
151
174
  prev_umask = os.umask(0o077)
152
175
  try:
153
176
  state_file.parent.mkdir(parents=True, exist_ok=True)
154
- state_file.write_text(
177
+ # Atomic tmp+replace: a crash mid-write must never leave a
178
+ # truncated state file behind (the reorganizer report pattern).
179
+ tmp_file = state_file.with_suffix(f".tmp-{os.getpid()}")
180
+ tmp_file.write_text(
155
181
  json.dumps({"fingerprint": fingerprint}), encoding="utf-8"
156
182
  )
183
+ os.replace(tmp_file, state_file)
157
184
  except OSError:
158
185
  pass
159
186
  finally:
@@ -34,7 +34,9 @@ class StopLintSummary:
34
34
  runs: int
35
35
  skipped_runs: int
36
36
  lint_pass_rate: float
37
- typecheck_pass_rate: float
37
+ # None when no run observed the check (typecheck is opt-in) —
38
+ # distinct from 0.0, which means "observed and failing".
39
+ typecheck_pass_rate: float | None
38
40
  would_block_rate: float
39
41
  corrupt_line_count: int = 0
40
42
 
@@ -119,7 +121,7 @@ def _build_summary(
119
121
  runs=len(rows),
120
122
  skipped_runs=len(rows) - len(observed),
121
123
  lint_pass_rate=_true_rate(observed, "lint_passed"),
122
- typecheck_pass_rate=_true_rate(observed, "typecheck_passed"),
124
+ typecheck_pass_rate=_true_rate_or_none(observed, "typecheck_passed"),
123
125
  would_block_rate=_true_rate(observed, "would_block"),
124
126
  corrupt_line_count=corrupt,
125
127
  )
@@ -130,3 +132,10 @@ def _true_rate(rows: list[dict[str, Any]], key: str) -> float:
130
132
  if not observed:
131
133
  return 0.0
132
134
  return sum(1 for r in observed if r[key] is True) / len(observed)
135
+
136
+
137
+ def _true_rate_or_none(rows: list[dict[str, Any]], key: str) -> float | None:
138
+ """Like ``_true_rate`` but honest about zero observations."""
139
+ if not any(isinstance(r.get(key), bool) for r in rows):
140
+ return None
141
+ return _true_rate(rows, key)
@@ -36,7 +36,6 @@ import subprocess
36
36
  import time
37
37
  from pathlib import Path
38
38
 
39
- from core.shared.temp_paths import arkaos_temp_dir
40
39
  from core.hooks._shared import (
41
40
  ensure_root_on_path,
42
41
  get_str,
@@ -45,6 +44,7 @@ from core.hooks._shared import (
45
44
  resolve_arkaos_root,
46
45
  safe_session_id,
47
46
  )
47
+ from core.shared.temp_paths import arkaos_temp_dir
48
48
 
49
49
  _CACHE_DIR = arkaos_temp_dir("arkaos-context-cache")
50
50
  _CACHE_TTL = 300 # Constitution cache: 5 minutes
@@ -89,13 +89,14 @@ _WF_VERB_PATTERN = (
89
89
  )
90
90
  _WF_VERB_RE = re.compile(rf"\b{_WF_VERB_PATTERN}\b", re.IGNORECASE)
91
91
 
92
- _STOPWORDS = frozenset(
93
- "the a an and or but if then of for to in on at by with from is are was "
94
- "were be been being do does did have has had this that these those it "
95
- "its as i you we they he she them my your our their so not no yes can "
96
- "will would could should may might must need want fix make use get set "
97
- "add remove".split()
98
- )
92
+ _STOPWORDS = frozenset([
93
+ "the", "a", "an", "and", "or", "but", "if", "then", "of", "for", "to", "in", "on",
94
+ "at", "by", "with", "from", "is", "are", "was", "were", "be", "been", "being", "do",
95
+ "does", "did", "have", "has", "had", "this", "that", "these", "those", "it", "its",
96
+ "as", "i", "you", "we", "they", "he", "she", "them", "my", "your", "our", "their",
97
+ "so", "not", "no", "yes", "can", "will", "would", "could", "should", "may", "might",
98
+ "must", "need", "want", "fix", "make", "use", "get", "set", "add", "remove"
99
+ ])
99
100
 
100
101
  _VAGUE_PHRASES = (
101
102
  "fix the bug", "that file", "the error",
@@ -228,7 +229,14 @@ def _run_bridge(root: str, user_input: str, session_id: str, cwd: str = "") -> s
228
229
  payload["cwd"] = cwd
229
230
  output, code = module.run_bridge(payload, Path(root))
230
231
  if code == 0:
231
- return str(output.get("context_string", ""))
232
+ parts = [str(output.get("context_string", ""))]
233
+ # Full-text blocks follow the compact tag line. A tag such as
234
+ # `[kb-context:5 +graph]` announces an injection; without these
235
+ # the announcement was all the model ever received.
236
+ blocks = output.get("content_blocks") or []
237
+ if isinstance(blocks, list):
238
+ parts.extend(str(b) for b in blocks if b)
239
+ return "\n".join(p for p in parts if p)
232
240
  except Exception:
233
241
  pass
234
242
  return ""
@@ -251,10 +259,6 @@ def _workflow_tag() -> str:
251
259
  workflow = str(state.get("workflow", "") or "")
252
260
  branch = str(state.get("branch", "") or "")
253
261
  violations = len(state.get("violations", []) or [])
254
- total = len(phases)
255
- completed = sum(
256
- 1 for p in phases.values() if p.get("status") == "completed"
257
- )
258
262
  current = next(
259
263
  (k for k, p in phases.items() if p.get("status") == "in_progress"),
260
264
  "none",
@@ -334,10 +338,8 @@ def _l0_constitution() -> str:
334
338
  return cache_file.read_text(encoding="utf-8")
335
339
  except OSError:
336
340
  pass
337
- try:
341
+ with contextlib.suppress(OSError):
338
342
  cache_file.write_text(_L0_FALLBACK, encoding="utf-8")
339
- except OSError:
340
- pass
341
343
  return _L0_FALLBACK
342
344
 
343
345
 
@@ -501,10 +503,8 @@ def _one_shot_nudge(subdir: str, session_id: str) -> str:
501
503
  nudge = f"[arka:suggest] {suggestion}"
502
504
  except (OSError, json.JSONDecodeError, ValueError):
503
505
  pass
504
- try:
506
+ with contextlib.suppress(OSError):
505
507
  nudge_file.unlink(missing_ok=True)
506
- except OSError:
507
- pass
508
508
  return nudge
509
509
 
510
510
 
@@ -548,10 +548,8 @@ def main(stdin_json: dict | None = None, raw: str = "") -> int:
548
548
 
549
549
  root = resolve_arkaos_root()
550
550
  ensure_root_on_path(root)
551
- try:
551
+ with contextlib.suppress(OSError):
552
552
  _CACHE_DIR.mkdir(parents=True, exist_ok=True)
553
- except OSError:
554
- pass
555
553
 
556
554
  # Claude Code sends the user's text under "prompt" (UserPromptSubmit
557
555
  # hook schema); "userInput"/"message" cover other runtimes. Missing
@@ -683,4 +681,4 @@ if __name__ == "__main__":
683
681
  except Exception:
684
682
  # Fail open with the minimal constitution context.
685
683
  print(json.dumps({"additionalContext": _L0_FALLBACK}))
686
- raise SystemExit(0)
684
+ raise SystemExit(0) from None
@@ -13,8 +13,8 @@ import time
13
13
  from dataclasses import dataclass, field
14
14
  from typing import Any
15
15
 
16
- from core.synapse.layers import Layer, LayerResult, PromptContext
17
16
  from core.synapse.cache import LayerCache
17
+ from core.synapse.layers import Layer, LayerResult, PromptContext
18
18
 
19
19
 
20
20
  @dataclass
@@ -27,6 +27,12 @@ class SynapseResult:
27
27
  total_tokens_est: int # Estimated total tokens injected
28
28
  cache_stats: dict # Cache hit/miss statistics
29
29
  layers_skipped: int # Layers that returned empty results
30
+ # Full-text blocks from layers whose content is richer than their tag
31
+ # (L2.5 KB context, graph context). Kept separate from context_string so
32
+ # the compact tag line stays compact; the hook appends these after it.
33
+ # Before this existed, LayerResult.content had NO consumer — every block a
34
+ # layer built was discarded while its tag still advertised the injection.
35
+ content_blocks: list[str] = field(default_factory=list)
30
36
 
31
37
 
32
38
  class SynapseEngine:
@@ -44,11 +50,11 @@ class SynapseEngine:
44
50
  def register_layer(self, layer: Layer) -> None:
45
51
  """Register a context layer. Layers execute in priority order."""
46
52
  self._layers.append(layer)
47
- self._layers.sort(key=lambda l: l.priority)
53
+ self._layers.sort(key=lambda x: x.priority)
48
54
 
49
55
  def remove_layer(self, layer_id: str) -> None:
50
56
  """Remove a layer by ID."""
51
- self._layers = [l for l in self._layers if l.id != layer_id]
57
+ self._layers = [x for x in self._layers if x.id != layer_id]
52
58
 
53
59
  def get_layer(self, layer_id: str) -> Layer | None:
54
60
  """Get a layer by ID."""
@@ -70,10 +76,13 @@ class SynapseEngine:
70
76
  results: list[LayerResult] = []
71
77
  skipped = 0
72
78
 
79
+ block_layers: set[str] = set()
73
80
  for layer in self._layers:
74
81
  result = self._compute_layer(layer, ctx)
75
82
  if result.tag or result.content:
76
83
  results.append(result)
84
+ if layer.emits_block:
85
+ block_layers.add(layer.id)
77
86
  else:
78
87
  skipped += 1
79
88
 
@@ -81,6 +90,15 @@ class SynapseEngine:
81
90
  tags = [r.tag for r in results if r.tag]
82
91
  context_string = " ".join(tags)
83
92
 
93
+ # Full-text blocks — OPT-IN via Layer.emits_block. Collecting on
94
+ # `content != tag` instead swept up every layer whose content is its
95
+ # tag's value ("dev", "active", "feat/plan-canvas"), injecting stray
96
+ # unlabeled lines into every prompt for zero added information.
97
+ content_blocks = [
98
+ r.content for r in results
99
+ if r.content and r.layer_id in block_layers
100
+ ]
101
+
84
102
  total_tokens = sum(r.tokens_est for r in results)
85
103
  total_ms = int((time.time() - start) * 1000)
86
104
 
@@ -105,6 +123,7 @@ class SynapseEngine:
105
123
  total_tokens_est=total_tokens,
106
124
  cache_stats=self._cache.stats,
107
125
  layers_skipped=skipped,
126
+ content_blocks=content_blocks,
108
127
  )
109
128
 
110
129
  def _compute_layer(self, layer: Layer, ctx: PromptContext) -> LayerResult:
@@ -125,15 +144,23 @@ class SynapseEngine:
125
144
  session_id = (ctx.extra or {}).get("session_id", "default")
126
145
  cache_key += f":{session_id}"
127
146
 
128
- # Check cache
147
+ # Check cache. The stored value is `tag` alone, or `tag\x00content`
148
+ # whenever content differs from tag: replaying the tag as the content
149
+ # would feed a marker to the model in place of what it advertises.
150
+ # This condition is deliberately WIDER than `emits_block` — the cache
151
+ # round-trips content faithfully for every layer (AgentLayer stores
152
+ # `[agent:x disc:y]\x00x` and is no block at all); only the block
153
+ # channel in inject() is opt-in.
129
154
  if layer.cache_ttl > 0:
130
155
  cached = self._cache.get(cache_key)
131
156
  if cached is not None:
157
+ tag, _, content = cached.partition("\x00")
158
+ content = content or tag
132
159
  return LayerResult(
133
160
  layer_id=layer.id,
134
- tag=cached,
135
- content=cached,
136
- tokens_est=len(cached.split()),
161
+ tag=tag,
162
+ content=content,
163
+ tokens_est=len(content.split()),
137
164
  compute_ms=0,
138
165
  cached=True,
139
166
  )
@@ -143,7 +170,10 @@ class SynapseEngine:
143
170
 
144
171
  # Cache if TTL > 0 and result is non-empty
145
172
  if layer.cache_ttl > 0 and result.tag:
146
- self._cache.set(cache_key, result.tag, layer.cache_ttl)
173
+ stored = result.tag
174
+ if result.content and result.content != result.tag:
175
+ stored = f"{result.tag}\x00{result.content}"
176
+ self._cache.set(cache_key, stored, layer.cache_ttl)
147
177
 
148
178
  return result
149
179
 
@@ -188,21 +218,21 @@ def create_default_engine(
188
218
  Returns:
189
219
  Configured SynapseEngine ready to use.
190
220
  """
221
+ from core.synapse.agent_experiences_layer import AgentExperiencesLayer
222
+ from core.synapse.graph_context_layer import GraphContextLayer
191
223
  from core.synapse.layers import (
192
- ConstitutionLayer,
193
- DepartmentLayer,
194
224
  AgentLayer,
195
- ProjectLayer,
196
225
  BranchLayer,
197
226
  CommandHintsLayer,
198
- QualityGateLayer,
199
- KnowledgeRetrievalLayer,
200
- KBContextLayer,
227
+ ConstitutionLayer,
228
+ DepartmentLayer,
201
229
  ForgeContextLayer,
230
+ KBContextLayer,
231
+ KnowledgeRetrievalLayer,
232
+ ProjectLayer,
233
+ QualityGateLayer,
202
234
  SessionContextLayer,
203
235
  )
204
- from core.synapse.agent_experiences_layer import AgentExperiencesLayer
205
- from core.synapse.graph_context_layer import GraphContextLayer
206
236
  from core.synapse.pattern_library_layer import PatternLibraryLayer
207
237
  from core.synapse.recipe_layer import RecipeLayer
208
238
  from core.synapse.routing_feedback_layer import RoutingFeedbackLayer
@@ -26,7 +26,7 @@ import json
26
26
  import os
27
27
  import time
28
28
  from pathlib import Path
29
- from typing import Any, Optional
29
+ from typing import Any
30
30
 
31
31
  from core.synapse.layers import Layer, LayerResult, PromptContext
32
32
  from core.synapse.pattern_library_layer import _extract_keywords
@@ -58,7 +58,7 @@ def _graph_flag_on() -> bool:
58
58
  return bool(synapse_cfg.get("graphContext", True))
59
59
 
60
60
 
61
- def _locate_graph(cwd: str) -> Optional[Path]:
61
+ def _locate_graph(cwd: str) -> Path | None:
62
62
  """Find ``graphify-out/graph.json`` in cwd or up to 3 parent dirs."""
63
63
  if not cwd:
64
64
  return None
@@ -149,6 +149,11 @@ class GraphContextLayer(Layer):
149
149
  def cache_ttl(self) -> int:
150
150
  return 30
151
151
 
152
+ @property
153
+ def emits_block(self) -> bool:
154
+ """Content is the self-naming [arka:graph-context:N] block."""
155
+ return True
156
+
152
157
  @property
153
158
  def priority(self) -> int:
154
159
  return 26 # right after L2.5 KBContext (25), before ProjectLayer (30)