arkaos 4.22.0 → 4.23.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 (252) hide show
  1. package/THE-ARKAOS-GUIDE.md +72 -0
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +28 -6
  4. package/arka/skills/bootstrap-agent/SKILL.md +3 -2
  5. package/arka/skills/flow/SKILL.md +3 -2
  6. package/arka/skills/forge/SKILL.md +3 -2
  7. package/arka/skills/recipes/SKILL.md +3 -2
  8. package/arka/skills/refine/SKILL.md +3 -2
  9. package/arka/skills/research/SKILL.md +3 -2
  10. package/bin/arka-doctor +8 -0
  11. package/core/cognition/insights/store.py +15 -4
  12. package/core/forge/persistence.py +27 -9
  13. package/core/forge/schema.py +45 -22
  14. package/core/governance/evidence_checks.py +101 -9
  15. package/core/governance/kb_cite_check.py +16 -6
  16. package/core/governance/stop_lint.py +38 -11
  17. package/core/governance/stop_lint_telemetry.py +11 -2
  18. package/core/hooks/user_prompt_submit.py +21 -23
  19. package/core/synapse/engine.py +46 -16
  20. package/core/synapse/graph_context_layer.py +7 -2
  21. package/core/synapse/kb_cache.py +63 -16
  22. package/core/synapse/layers.py +93 -30
  23. package/core/synapse/layers_base.py +16 -0
  24. package/core/synapse/layers_kb.py +325 -19
  25. package/dashboard/app/layouts/default.vue +7 -0
  26. package/dashboard/app/pages/plan-canvas.vue +630 -0
  27. package/departments/brand/SKILL.md +3 -2
  28. package/departments/brand/skills/archetype-finder/SKILL.md +3 -2
  29. package/departments/brand/skills/colors/SKILL.md +3 -2
  30. package/departments/brand/skills/design-system/SKILL.md +3 -2
  31. package/departments/brand/skills/identity-system/SKILL.md +3 -2
  32. package/departments/brand/skills/logo-brief/SKILL.md +3 -2
  33. package/departments/brand/skills/mockup-generate/SKILL.md +3 -2
  34. package/departments/brand/skills/naming-evaluate/SKILL.md +3 -2
  35. package/departments/brand/skills/positioning-statement/SKILL.md +3 -2
  36. package/departments/brand/skills/primal-audit/SKILL.md +3 -2
  37. package/departments/brand/skills/ux-audit/SKILL.md +3 -2
  38. package/departments/brand/skills/voice-guide/SKILL.md +3 -2
  39. package/departments/brand/skills/wireframe/SKILL.md +3 -2
  40. package/departments/community/SKILL.md +3 -2
  41. package/departments/community/skills/ai-community/SKILL.md +3 -2
  42. package/departments/community/skills/betting-setup/SKILL.md +3 -2
  43. package/departments/community/skills/business-model/SKILL.md +3 -2
  44. package/departments/community/skills/content-calendar/SKILL.md +3 -2
  45. package/departments/community/skills/events-plan/SKILL.md +3 -2
  46. package/departments/community/skills/gamification-design/SKILL.md +3 -2
  47. package/departments/community/skills/growth-plan/SKILL.md +3 -2
  48. package/departments/community/skills/metrics-track/SKILL.md +3 -2
  49. package/departments/community/skills/moderation/SKILL.md +3 -2
  50. package/departments/community/skills/monetize-plan/SKILL.md +3 -2
  51. package/departments/community/skills/niche-setup/SKILL.md +3 -2
  52. package/departments/community/skills/onboarding-flow/SKILL.md +3 -2
  53. package/departments/community/skills/platform-select/SKILL.md +3 -2
  54. package/departments/content/SKILL.md +3 -2
  55. package/departments/content/skills/ai-workflow/SKILL.md +3 -2
  56. package/departments/content/skills/analytics/SKILL.md +3 -2
  57. package/departments/content/skills/calendar/SKILL.md +3 -2
  58. package/departments/content/skills/content-system/SKILL.md +3 -2
  59. package/departments/content/skills/monetization-plan/SKILL.md +3 -2
  60. package/departments/content/skills/newsletter-write/SKILL.md +3 -2
  61. package/departments/content/skills/platform-optimize/SKILL.md +3 -2
  62. package/departments/content/skills/repurpose-plan/SKILL.md +3 -2
  63. package/departments/content/skills/research-compile/SKILL.md +3 -2
  64. package/departments/content/skills/script-structure/SKILL.md +3 -2
  65. package/departments/content/skills/short-form/SKILL.md +3 -2
  66. package/departments/content/skills/thumbnail-package/SKILL.md +3 -2
  67. package/departments/content/skills/trend-hunt/SKILL.md +3 -2
  68. package/departments/content/skills/video-produce/SKILL.md +3 -2
  69. package/departments/content/skills/video-setup/SKILL.md +3 -2
  70. package/departments/content/skills/viral-design/SKILL.md +3 -2
  71. package/departments/content/skills/youtube-strategy/SKILL.md +3 -2
  72. package/departments/dev/SKILL.md +3 -2
  73. package/departments/dev/skills/ai-assisted-dev/SKILL.md +3 -2
  74. package/departments/dev/skills/animated-website/SKILL.md +7 -0
  75. package/departments/dev/skills/architecture-design/SKILL.md +3 -2
  76. package/departments/dev/skills/code-review/SKILL.md +3 -2
  77. package/departments/dev/skills/db-design/SKILL.md +3 -2
  78. package/departments/dev/skills/ddd-model/SKILL.md +3 -2
  79. package/departments/dev/skills/demo-gif/SKILL.md +3 -2
  80. package/departments/dev/skills/deploy/SKILL.md +3 -2
  81. package/departments/dev/skills/devops-pipeline/SKILL.md +3 -2
  82. package/departments/dev/skills/docs/SKILL.md +3 -2
  83. package/departments/dev/skills/mcp/SKILL.md +3 -2
  84. package/departments/dev/skills/performance-audit/SKILL.md +3 -2
  85. package/departments/dev/skills/refactor-plan/SKILL.md +3 -2
  86. package/departments/dev/skills/research/SKILL.md +3 -2
  87. package/departments/dev/skills/security-compliance/SKILL.md +3 -2
  88. package/departments/dev/skills/stack-check/SKILL.md +3 -2
  89. package/departments/ecom/SKILL.md +3 -2
  90. package/departments/ecom/skills/analytics/SKILL.md +3 -2
  91. package/departments/ecom/skills/browse-competitor/SKILL.md +3 -2
  92. package/departments/ecom/skills/cart-recovery/SKILL.md +3 -2
  93. package/departments/ecom/skills/cro-optimize/SKILL.md +3 -2
  94. package/departments/ecom/skills/customer-journey/SKILL.md +3 -2
  95. package/departments/ecom/skills/fulfillment-plan/SKILL.md +3 -2
  96. package/departments/ecom/skills/marketplace-manage/SKILL.md +3 -2
  97. package/departments/ecom/skills/pricing-strategy/SKILL.md +3 -2
  98. package/departments/ecom/skills/product-launch/SKILL.md +3 -2
  99. package/departments/ecom/skills/rfm-segment/SKILL.md +3 -2
  100. package/departments/ecom/skills/social-commerce/SKILL.md +3 -2
  101. package/departments/ecom/skills/store-audit/SKILL.md +3 -2
  102. package/departments/ecom/skills/subscription-model/SKILL.md +3 -2
  103. package/departments/finance/SKILL.md +3 -2
  104. package/departments/finance/skills/budget-plan/SKILL.md +3 -2
  105. package/departments/finance/skills/cashflow-forecast/SKILL.md +3 -2
  106. package/departments/finance/skills/ciso-advisor/SKILL.md +3 -2
  107. package/departments/finance/skills/financial-model/SKILL.md +3 -2
  108. package/departments/finance/skills/pitch-deck/SKILL.md +3 -2
  109. package/departments/finance/skills/scenario-analysis/SKILL.md +3 -2
  110. package/departments/finance/skills/unit-economics/SKILL.md +3 -2
  111. package/departments/finance/skills/valuation-model/SKILL.md +3 -2
  112. package/departments/kb/SKILL.md +3 -2
  113. package/departments/kb/skills/ai-research/SKILL.md +3 -2
  114. package/departments/kb/skills/competitive-intel/SKILL.md +3 -2
  115. package/departments/kb/skills/doc-extraction/SKILL.md +3 -2
  116. package/departments/kb/skills/doc-redaction/SKILL.md +3 -2
  117. package/departments/kb/skills/knowledge-ops/SKILL.md +3 -2
  118. package/departments/kb/skills/knowledge-review/SKILL.md +3 -2
  119. package/departments/kb/skills/learn-content/SKILL.md +3 -2
  120. package/departments/kb/skills/moc-create/SKILL.md +3 -2
  121. package/departments/kb/skills/persona-build/SKILL.md +3 -2
  122. package/departments/kb/skills/research-plan/SKILL.md +3 -2
  123. package/departments/kb/skills/search-kb/SKILL.md +3 -2
  124. package/departments/kb/skills/source-evaluate/SKILL.md +3 -2
  125. package/departments/kb/skills/taxonomy-manage/SKILL.md +3 -2
  126. package/departments/kb/skills/write-as-persona/SKILL.md +3 -2
  127. package/departments/landing/SKILL.md +3 -2
  128. package/departments/landing/skills/ab-test/SKILL.md +3 -2
  129. package/departments/landing/skills/affiliate-bridge/SKILL.md +3 -2
  130. package/departments/landing/skills/awareness-diagnose/SKILL.md +3 -2
  131. package/departments/landing/skills/email-sequence/SKILL.md +3 -2
  132. package/departments/landing/skills/funnel-metrics/SKILL.md +3 -2
  133. package/departments/landing/skills/headline-write/SKILL.md +3 -2
  134. package/departments/landing/skills/launch-sequence/SKILL.md +3 -2
  135. package/departments/landing/skills/offer-create/SKILL.md +3 -2
  136. package/departments/landing/skills/optimize-page/SKILL.md +3 -2
  137. package/departments/landing/skills/page-architect/SKILL.md +3 -2
  138. package/departments/landing/skills/persuasion-apply/SKILL.md +3 -2
  139. package/departments/landing/skills/webinar-funnel/SKILL.md +3 -2
  140. package/departments/leadership/SKILL.md +3 -2
  141. package/departments/leadership/skills/change-manage/SKILL.md +3 -2
  142. package/departments/leadership/skills/conflict-resolve/SKILL.md +3 -2
  143. package/departments/leadership/skills/culture-audit/SKILL.md +3 -2
  144. package/departments/leadership/skills/delegation-matrix/SKILL.md +3 -2
  145. package/departments/leadership/skills/disc-assess/SKILL.md +3 -2
  146. package/departments/leadership/skills/feedback-give/SKILL.md +3 -2
  147. package/departments/leadership/skills/hiring-plan/SKILL.md +3 -2
  148. package/departments/leadership/skills/performance-review/SKILL.md +3 -2
  149. package/departments/marketing/SKILL.md +3 -2
  150. package/departments/marketing/skills/ab-test/SKILL.md +3 -2
  151. package/departments/marketing/skills/analytics-report/SKILL.md +3 -2
  152. package/departments/marketing/skills/audience-segment/SKILL.md +3 -2
  153. package/departments/marketing/skills/calendar-plan/SKILL.md +3 -2
  154. package/departments/marketing/skills/competitor-analysis/SKILL.md +3 -2
  155. package/departments/marketing/skills/content-audit/SKILL.md +3 -2
  156. package/departments/marketing/skills/email-sequence/SKILL.md +3 -2
  157. package/departments/marketing/skills/growth-loop/SKILL.md +3 -2
  158. package/departments/marketing/skills/marketing-automation/SKILL.md +3 -2
  159. package/departments/marketing/skills/paid-campaign/SKILL.md +3 -2
  160. package/departments/marketing/skills/programmatic-seo/SKILL.md +3 -2
  161. package/departments/marketing/skills/seo-audit/SKILL.md +3 -2
  162. package/departments/marketing/skills/social-strategy/SKILL.md +3 -2
  163. package/departments/ops/SKILL.md +3 -2
  164. package/departments/ops/skills/bottleneck-find/SKILL.md +3 -2
  165. package/departments/ops/skills/dashboard-build/SKILL.md +3 -2
  166. package/departments/ops/skills/gdpr-compliance/SKILL.md +3 -2
  167. package/departments/ops/skills/github-ops/SKILL.md +3 -2
  168. package/departments/ops/skills/gtd-setup/SKILL.md +3 -2
  169. package/departments/ops/skills/integration-design/SKILL.md +3 -2
  170. package/departments/ops/skills/iso27001/SKILL.md +3 -2
  171. package/departments/ops/skills/lean-audit/SKILL.md +3 -2
  172. package/departments/ops/skills/metrics-dashboard/SKILL.md +3 -2
  173. package/departments/ops/skills/n8n-flow/SKILL.md +3 -2
  174. package/departments/ops/skills/quality-management/SKILL.md +3 -2
  175. package/departments/ops/skills/risk-management/SKILL.md +3 -2
  176. package/departments/ops/skills/soc2-compliance/SKILL.md +3 -2
  177. package/departments/ops/skills/sop-create/SKILL.md +3 -2
  178. package/departments/ops/skills/terminal-ops/SKILL.md +3 -2
  179. package/departments/ops/skills/workflow-automate/SKILL.md +3 -2
  180. package/departments/ops/skills/zapier-flow/SKILL.md +3 -2
  181. package/departments/org/SKILL.md +3 -2
  182. package/departments/org/skills/compensation-plan/SKILL.md +3 -2
  183. package/departments/org/skills/culture-define/SKILL.md +3 -2
  184. package/departments/org/skills/decision-framework/SKILL.md +3 -2
  185. package/departments/org/skills/hiring-plan/SKILL.md +3 -2
  186. package/departments/org/skills/meeting-optimize/SKILL.md +3 -2
  187. package/departments/org/skills/onboarding-design/SKILL.md +3 -2
  188. package/departments/org/skills/org-design/SKILL.md +3 -2
  189. package/departments/org/skills/remote-setup/SKILL.md +3 -2
  190. package/departments/org/skills/sop-process/SKILL.md +3 -2
  191. package/departments/org/skills/team-assess/SKILL.md +3 -2
  192. package/departments/pm/SKILL.md +3 -2
  193. package/departments/pm/skills/backlog-groom/SKILL.md +3 -2
  194. package/departments/pm/skills/discovery-plan/SKILL.md +3 -2
  195. package/departments/pm/skills/estimate-forecast/SKILL.md +3 -2
  196. package/departments/pm/skills/impact-map/SKILL.md +3 -2
  197. package/departments/pm/skills/kanban-setup/SKILL.md +3 -2
  198. package/departments/pm/skills/risk-register/SKILL.md +3 -2
  199. package/departments/pm/skills/roadmap-build/SKILL.md +3 -2
  200. package/departments/pm/skills/sprint-plan/SKILL.md +3 -2
  201. package/departments/pm/skills/stakeholder-map/SKILL.md +3 -2
  202. package/departments/pm/skills/standup-run/SKILL.md +3 -2
  203. package/departments/pm/skills/story-write/SKILL.md +3 -2
  204. package/departments/saas/SKILL.md +3 -2
  205. package/departments/saas/skills/benchmark-compare/SKILL.md +3 -2
  206. package/departments/saas/skills/churn-analysis/SKILL.md +3 -2
  207. package/departments/saas/skills/customer-success/SKILL.md +3 -2
  208. package/departments/saas/skills/growth-plan/SKILL.md +3 -2
  209. package/departments/saas/skills/gtm-strategy/SKILL.md +3 -2
  210. package/departments/saas/skills/launch-execute/SKILL.md +3 -2
  211. package/departments/saas/skills/metrics-dashboard/SKILL.md +3 -2
  212. package/departments/saas/skills/micro-saas-stack/SKILL.md +3 -2
  213. package/departments/saas/skills/mvp-build/SKILL.md +3 -2
  214. package/departments/saas/skills/niche-evaluate/SKILL.md +3 -2
  215. package/departments/saas/skills/onboarding-optimize/SKILL.md +3 -2
  216. package/departments/saas/skills/plg-setup/SKILL.md +3 -2
  217. package/departments/saas/skills/pricing-strategy/SKILL.md +3 -2
  218. package/departments/saas/skills/validate-idea/SKILL.md +3 -2
  219. package/departments/sales/SKILL.md +3 -2
  220. package/departments/sales/skills/challenger-sell/SKILL.md +3 -2
  221. package/departments/sales/skills/deal-qualify/SKILL.md +3 -2
  222. package/departments/sales/skills/discovery-call/SKILL.md +3 -2
  223. package/departments/sales/skills/forecast-revenue/SKILL.md +3 -2
  224. package/departments/sales/skills/negotiate-plan/SKILL.md +3 -2
  225. package/departments/sales/skills/objection-handle/SKILL.md +3 -2
  226. package/departments/sales/skills/pipeline-manage/SKILL.md +3 -2
  227. package/departments/sales/skills/pricing-negotiate/SKILL.md +3 -2
  228. package/departments/strategy/SKILL.md +3 -2
  229. package/departments/strategy/skills/blue-ocean/SKILL.md +3 -2
  230. package/departments/strategy/skills/bmc/SKILL.md +3 -2
  231. package/departments/strategy/skills/board-advisor/SKILL.md +3 -2
  232. package/departments/strategy/skills/cto-advisor/SKILL.md +3 -2
  233. package/departments/strategy/skills/extract-data/SKILL.md +3 -2
  234. package/departments/strategy/skills/five-forces/SKILL.md +3 -2
  235. package/departments/strategy/skills/growth-strategy/SKILL.md +3 -2
  236. package/departments/strategy/skills/moat-analysis/SKILL.md +3 -2
  237. package/departments/strategy/skills/position/SKILL.md +3 -2
  238. package/departments/strategy/skills/scenario-plan/SKILL.md +3 -2
  239. package/installer/config-seed.js +81 -25
  240. package/installer/graphify.js +340 -0
  241. package/installer/index.js +8 -1
  242. package/installer/keys.js +18 -3
  243. package/installer/update.js +8 -1
  244. package/knowledge/skills-manifest.json +1 -1
  245. package/mcps/registry.json +9 -9
  246. package/mcps/scripts/apply-mcps.sh +6 -2
  247. package/package.json +2 -1
  248. package/pyproject.toml +1 -1
  249. package/scripts/dashboard-api.py +336 -124
  250. package/scripts/guide_gen.py +139 -0
  251. package/scripts/migrate_skills_kb_first.py +3 -2
  252. package/scripts/synapse-bridge.py +9 -1
@@ -28,20 +28,20 @@ from typing import Any
28
28
  from core.synapse.layers_base import Layer, LayerResult, PromptContext
29
29
 
30
30
  __all__ = [
31
- "Layer",
32
- "LayerResult",
33
- "PromptContext",
34
- "ConstitutionLayer",
35
- "DepartmentLayer",
36
31
  "AgentLayer",
37
- "ProjectLayer",
38
32
  "BranchLayer",
39
33
  "CommandHintsLayer",
40
- "QualityGateLayer",
41
- "KnowledgeRetrievalLayer",
34
+ "ConstitutionLayer",
35
+ "DepartmentLayer",
42
36
  "ForgeContextLayer",
43
- "SessionContextLayer",
44
37
  "KBContextLayer",
38
+ "KnowledgeRetrievalLayer",
39
+ "Layer",
40
+ "LayerResult",
41
+ "ProjectLayer",
42
+ "PromptContext",
43
+ "QualityGateLayer",
44
+ "SessionContextLayer",
45
45
  ]
46
46
 
47
47
 
@@ -90,22 +90,75 @@ class ConstitutionLayer(Layer):
90
90
  # --- L1: Department Detection ---
91
91
 
92
92
  DEPARTMENT_PATTERNS: dict[str, str] = {
93
- "dev": r"\b(build|code|feature|deploy|test|review|scaffold|debug|refactor|api|migration|stack|implement|fix|bug)\b",
94
- "marketing": r"\b(social|content|campaign|post|instagram|linkedin|twitter|tiktok|seo|marketing|ads|email.?campaign|growth|viral)\b",
95
- "finance": r"\b(budget|invoice|revenue|forecast|profit|loss|roi|margin|cash.?flow|financial|invest|valuation|pricing)\b",
96
- "ecom": r"\b(store|product|shop|shopify|ecommerce|catalog|inventory|cart|checkout|pricing|marketplace)\b",
97
- "strategy": r"\b(strategy|brainstorm|market|swot|competitors?|roadmap|pivot|growth|porter|blue.?ocean|positioning)\b",
93
+ "dev": (
94
+ r"\b("
95
+ r"build|code|feature|deploy|test|review|scaffold|"
96
+ r"debug|refactor|api|migration|stack|implement|fix|"
97
+ r"bug)\b"
98
+ ),
99
+ "marketing": (
100
+ r"\b("
101
+ r"social|content|campaign|post|instagram|linkedin|"
102
+ r"twitter|tiktok|seo|marketing|ads|email.?campaign|"
103
+ r"growth|viral)\b"
104
+ ),
105
+ "finance": (
106
+ r"\b("
107
+ r"budget|invoice|revenue|forecast|profit|loss|roi|"
108
+ r"margin|cash.?flow|financial|invest|valuation|"
109
+ r"pricing)\b"
110
+ ),
111
+ "ecom": (
112
+ r"\b("
113
+ r"store|product|shop|shopify|ecommerce|catalog|"
114
+ r"inventory|cart|checkout|pricing|marketplace)\b"
115
+ ),
116
+ "strategy": (
117
+ r"\b("
118
+ r"strategy|brainstorm|market|swot|competitors?|"
119
+ r"roadmap|pivot|growth|porter|blue.?ocean|"
120
+ r"positioning)\b"
121
+ ),
98
122
  "ops": r"\b(task|automate|meeting|workflow|process|schedule|sop|integration|zapier|n8n)\b",
99
123
  "kb": r"\b(learn|persona|knowledge|youtube|transcribe|article|research|zettelkasten|note)\b",
100
- "brand": r"\b(brand|logo|colors|palette|mockup|photoshoot|brand.?identity|brand.?guide|mood.?board|naming|visual.?design|motion|ux|ui|wireframe)\b",
124
+ "brand": (
125
+ r"\b("
126
+ r"brand|logo|colors|palette|mockup|photoshoot|brand.?identity|"
127
+ r"brand.?guide|mood.?board|naming|visual.?design|motion|ux|ui|"
128
+ r"wireframe)\b"
129
+ ),
101
130
  "saas": r"\b(saas|micro.?saas|plg|freemium|churn|mrr|arr|subscription|onboarding|metrics)\b",
102
- "landing": r"\b(landing|funnel|copy|headline|offer|launch|affiliate|webinar|conversion|sales.?page)\b",
103
- "community": r"\b(community|group|membership|discord|telegram|skool|circle|gamification|engagement)\b",
131
+ "landing": (
132
+ r"\b("
133
+ r"landing|funnel|copy|headline|offer|launch|"
134
+ r"affiliate|webinar|conversion|sales.?page)\b"
135
+ ),
136
+ "community": (
137
+ r"\b("
138
+ r"community|group|membership|discord|"
139
+ r"telegram|skool|circle|gamification|"
140
+ r"engagement)\b"
141
+ ),
104
142
  "content": r"\b(viral|hook|script|repurpose|youtube|tiktok|reels|shorts|newsletter|creator)\b",
105
143
  "pm": r"\b(sprint|backlog|standup|retro|scrum|kanban|story|estimate|roadmap|agile)\b",
106
- "lead": r"\b(leadership|delegation|1on1|feedback|culture|hiring|performance.?review|team.?build)\b",
107
- "sales": r"\b(pipeline|proposal|discovery.?call|objection|negotiate|deal|close|prospect|spin|challenger|cold.?email|outreach)\b",
108
- "org": r"\b(org.?design|hiring.?plan|onboarding|remote|meeting.?optimize|compensation|decision.?framework)\b",
144
+ "lead": (
145
+ r"\b("
146
+ r"leadership|delegation|1on1|feedback|"
147
+ r"culture|hiring|performance.?review|"
148
+ r"team.?build)\b"
149
+ ),
150
+ "sales": (
151
+ r"\b("
152
+ r"pipeline|proposal|discovery.?call|objection|negotiate|"
153
+ r"deal|close|prospect|spin|challenger|cold.?email|"
154
+ r"outreach)\b"
155
+ ),
156
+ "org": (
157
+ r"\b("
158
+ r"org.?design|hiring.?plan|onboarding|remote|"
159
+ r"meeting.?optimize|compensation|"
160
+ r"decision.?framework)\b"
161
+ ),
109
162
  }
110
163
 
111
164
 
@@ -317,10 +370,7 @@ class BranchLayer(Layer):
317
370
  start = time.time()
318
371
  branch = ctx.git_branch
319
372
  # Hide main/master/dev branches
320
- if branch in ("main", "master", "dev", ""):
321
- tag = ""
322
- else:
323
- tag = f"[branch:{branch}]"
373
+ tag = "" if branch in ("main", "master", "dev", "") else f"[branch:{branch}]"
324
374
 
325
375
  ms = int((time.time() - start) * 1000)
326
376
  return LayerResult(
@@ -427,7 +477,7 @@ class QualityGateLayer(Layer):
427
477
  def compute(self, ctx: PromptContext) -> LayerResult:
428
478
  start = time.time()
429
479
  try:
430
- from core.governance.quality_api import list_pending, list_approved
480
+ from core.governance.quality_api import list_approved, list_pending
431
481
 
432
482
  pending = list_pending()
433
483
  approved = list_approved(limit=3)
@@ -496,6 +546,17 @@ class KnowledgeRetrievalLayer(Layer):
496
546
  def cache_ttl(self) -> int:
497
547
  return 30
498
548
 
549
+ @property
550
+ def emits_block(self) -> bool:
551
+ """The retrieved chunks are a real block, not the tag's value.
552
+
553
+ `[knowledge:N chunks]` makes a QUANTIFIED claim about content the
554
+ model never received before the content channel existed — the
555
+ loudest unbacked claim in the tag line (QG review). Opting in
556
+ delivers what the tag advertises.
557
+ """
558
+ return True
559
+
499
560
  @property
500
561
  def priority(self) -> int:
501
562
  return 35
@@ -580,7 +641,10 @@ class KnowledgeRetrievalLayer(Layer):
580
641
  parts.append(f"[cached] {src}: {text}" if src else f"[cached] {text}")
581
642
  parts.extend(snippets)
582
643
 
583
- content = " | ".join(parts)
644
+ # Self-naming, because the content channel delivers this verbatim to
645
+ # the model: an unlabelled `[cached] file.md: …` fragment would read
646
+ # as stray text (the exact defect the opt-in channel exists to avoid).
647
+ content = f"[arka:knowledge] {' | '.join(parts)}" if parts else ""
584
648
  chunk_count = len(snippets) + (len(overlapping) if overlapping else 0)
585
649
  # RAG honesty (PR-3 v4.1): keyword-degraded results must never be
586
650
  # presented as semantic similarity — label the tag explicitly.
@@ -639,7 +703,7 @@ class ForgeContextLayer(Layer):
639
703
  parts = [f"Forge plan: {plan.id} ({plan.status.value})"]
640
704
  if plan.critic.confidence > 0:
641
705
  decisions = []
642
- for source, elements in plan.critic.synthesis.items():
706
+ for _source, elements in plan.critic.synthesis.items():
643
707
  decisions.extend(elements)
644
708
  if decisions:
645
709
  parts.append(f"Decisions: {'; '.join(decisions[:5])}")
@@ -701,7 +765,6 @@ class SessionContextLayer(Layer):
701
765
  layer_id=self.id, tag="", content="", tokens_est=0, compute_ms=0, cached=False
702
766
  )
703
767
 
704
- lines = content.split("\n")
705
768
  tag = "[session:resume]"
706
769
  tokens = len(content.split())
707
770
 
@@ -720,6 +783,8 @@ class SessionContextLayer(Layer):
720
783
  # `core.synapse.layers` import paths and helper references keep working.
721
784
 
722
785
  from core.synapse.layers_kb import ( # noqa: E402,F401
786
+ _KB_CONFIG_PATH,
787
+ _MAX_FALLBACK_NOTES,
723
788
  KBContextLayer,
724
789
  _apply_grounding_policy,
725
790
  _build_note_entry,
@@ -732,10 +797,8 @@ from core.synapse.layers_kb import ( # noqa: E402,F401
732
797
  _hit_is_inferred,
733
798
  _jaccard,
734
799
  _jaccard_fallback,
735
- _KB_CONFIG_PATH,
736
800
  _l25_feature_flag_on,
737
801
  _load_fallback_notes,
738
- _MAX_FALLBACK_NOTES,
739
802
  _note_from_vector_hit,
740
803
  _tokenize_for_jaccard,
741
804
  _vector_search,
@@ -69,6 +69,22 @@ class Layer(ABC):
69
69
  """
70
70
  return False
71
71
 
72
+ @property
73
+ def emits_block(self) -> bool:
74
+ """True when ``content`` is a self-describing full-text block.
75
+
76
+ OPT-IN, and deliberately so. The engine forwards ``content`` to the
77
+ model only for layers that declare this. Inferring it from
78
+ ``content != tag`` looked equivalent and was not: most layers set
79
+ ``content`` to their tag's VALUE (``dev``, ``active``,
80
+ ``feat/plan-canvas``), so every prompt gained stray unlabeled lines
81
+ carrying nothing the tag had not already said (QG review).
82
+
83
+ A block must stand alone: it names itself (``[arka:kb-context]``,
84
+ ``[arka:graph-remote unverified]``) and is meaningful without the tag.
85
+ """
86
+ return False
87
+
72
88
  @property
73
89
  def session_sensitive(self) -> bool:
74
90
  """True when compute() has per-session side effects or output.
@@ -6,12 +6,14 @@ All names are re-exported by core.synapse.layers for backward
6
6
  compatibility — import from there unless you need the module itself.
7
7
  """
8
8
 
9
+ import contextlib
9
10
  import json
10
11
  import os
11
12
  import re
12
13
  import time
14
+ import urllib.request
13
15
  from pathlib import Path
14
- from typing import Any, Optional
16
+ from typing import Any
15
17
 
16
18
  from core.synapse.layers_base import Layer, LayerResult, PromptContext
17
19
 
@@ -23,6 +25,9 @@ _KB_CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
23
25
  # (Jaccard ranks the top few notes; scanning 2000 sorted-by-name first
24
26
  # is plenty — see `_load_fallback_notes`) while still bounding worst-case latency.
25
27
  _MAX_FALLBACK_NOTES = 2000
28
+ # A config.json past this is not a config — refuse it rather than parse it
29
+ # inside the Synapse budget. 4 MiB is ~1000x the real file.
30
+ _MAX_CONFIG_BYTES = 4 * 1024 * 1024
26
31
  _KB_STOPWORDS: frozenset[str] = frozenset({
27
32
  "a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for", "of",
28
33
  "with", "by", "from", "as", "is", "was", "are", "were", "be", "been", "being",
@@ -34,19 +39,288 @@ _KB_STOPWORDS: frozenset[str] = frozenset({
34
39
  })
35
40
 
36
41
 
42
+ def _read_json_dict(path: Path) -> dict:
43
+ """Parse ``path`` into a dict, or ``{}``. The single never-raises reader.
44
+
45
+ Synapse does not wrap ``layer.compute``, so ANY exception escaping a
46
+ config read breaks all 12 layers for the turn. Three failure modes are
47
+ covered together, which is why this is one helper and not three
48
+ try/excepts: OSError (unreadable), ValueError (bad JSON *and* the
49
+ UnicodeDecodeError a non-UTF-8 file raises), and RecursionError (deeply
50
+ nested JSON — neither of the other two, and it escaped in QG review).
51
+
52
+ A fourth case needs no ``except`` at all: well-formed JSON whose root is
53
+ not an object — ``[1,2,3]`` or ``"yes"`` — would raise AttributeError on
54
+ the first ``.get``, so the return is isinstance-guarded instead.
55
+ """
56
+ try:
57
+ # is_file() excludes a FIFO, whose read_text() blocks FOREVER with no
58
+ # exception for the net below to catch (QG review). The size cap keeps
59
+ # a huge file from stalling the ~87ms budget. Both are stat-only.
60
+ if not path.is_file() or path.stat().st_size > _MAX_CONFIG_BYTES:
61
+ return {}
62
+ data = json.loads(path.read_text(encoding="utf-8"))
63
+ except (ValueError, OSError, RecursionError):
64
+ return {}
65
+ return data if isinstance(data, dict) else {}
66
+
67
+
68
+ def _section(data: dict, key: str) -> dict:
69
+ """A dict-valued section of a config, or ``{}`` — never raises."""
70
+ value = data.get(key)
71
+ return value if isinstance(value, dict) else {}
72
+
73
+
37
74
  def _l25_feature_flag_on() -> bool:
38
75
  if os.environ.get("ARKA_BYPASS_L25", "").strip() == "1":
39
76
  return False
40
77
  if not _KB_CONFIG_PATH.exists():
41
78
  return True
42
- try:
43
- data = json.loads(_KB_CONFIG_PATH.read_text(encoding="utf-8"))
44
- except (json.JSONDecodeError, OSError):
45
- return True
46
- synapse_cfg = data.get("synapse") or {}
79
+ synapse_cfg = _section(_read_json_dict(_KB_CONFIG_PATH), "synapse")
47
80
  return bool(synapse_cfg.get("l25KbContext", True))
48
81
 
49
82
 
83
+ # --- Graphify graph-context pre-injection (opt-in, default OFF) ------------
84
+ # The knowledge graph is a per-user HTTP endpoint. Pre-injecting it costs up
85
+ # to THREE bounded round-trips (initialize → initialized → tools/call), i.e.
86
+ # ~3x _GRAPHIFY_TIMEOUT_S worst case against a ~87ms Synapse budget. That is
87
+ # why it is OFF by default (synapse.l25Graphify) and always fail-open: any
88
+ # missing config, corrupt config, unreachable host, or slow response yields
89
+ # no block and never raises.
90
+ _KEYS_PATH = Path.home() / ".arkaos" / "keys.json"
91
+ _GRAPHIFY_TIMEOUT_S = 0.3
92
+
93
+
94
+ def _l25_graphify_flag_on() -> bool:
95
+ if os.environ.get("ARKA_BYPASS_L25_GRAPHIFY", "").strip() == "1":
96
+ return False
97
+ if not _KB_CONFIG_PATH.exists():
98
+ return False
99
+ synapse_cfg = _section(_read_json_dict(_KB_CONFIG_PATH), "synapse")
100
+ return bool(synapse_cfg.get("l25Graphify", False))
101
+
102
+
103
+ # The exact ranges ``isPrivateHost`` in installer/graphify.js accepts. Kept
104
+ # as literals rather than ``ip.is_private`` (a strict superset: 0.0.0.0/8,
105
+ # TEST-NET-1/2/3, 240/4, 255.255.255.255 …) so the two implementations cannot
106
+ # drift — tests/python/test_graphify_url_parity.py drives one shared corpus
107
+ # through both and fails on any divergence.
108
+ _PRIVATE_V4 = ("127.0.0.0/8", "10.0.0.0/8", "192.168.0.0/16",
109
+ "172.16.0.0/12", "169.254.0.0/16")
110
+ _PRIVATE_V6 = ("::1/128", "fc00::/7", "fe80::/10")
111
+ # A whole-host label that is a bare number or 0x-hex, or any dotted form with
112
+ # a 0-prefixed (octal) octet: 134744072, 0x08080808, 0177.0.0.1.
113
+ _OBFUSCATED_IP_RE = re.compile(r"^(?:0[xX][0-9a-fA-F]+|\d+)$|(?:^|\.)0\d")
114
+
115
+
116
+ def _ip_is_private(ip: Any) -> bool:
117
+ """Membership in the explicit private ranges — no is_private shortcut."""
118
+ import ipaddress
119
+
120
+ nets = _PRIVATE_V6 if ip.version == 6 else _PRIVATE_V4
121
+ return any(ip in ipaddress.ip_network(n) for n in nets)
122
+
123
+
124
+ def _is_private_host(raw_host: str) -> bool:
125
+ """True only for hosts that cannot reach a public endpoint.
126
+
127
+ Python mirror of ``isPrivateHost`` in ``installer/graphify.js`` — and the
128
+ side that resolves the name before opening the socket, so it validates
129
+ independently of any registration-time check. It is NOT the only sender:
130
+ ``registerGraphifyHttpMcp`` hands the same token to Claude Code, which
131
+ then talks to the endpoint on every session (installer/graphify.js).
132
+
133
+ A NAME is resolved rather than pattern-matched. Judging the string alone
134
+ let ``http://134744072/`` through (8.8.8.8 in decimal is a single-label
135
+ "name" with no dot) and the token went out over plaintext HTTP — captured
136
+ in QG review. Resolving also closes hex literals and DNS rebinding, since
137
+ every returned address must be private.
138
+ """
139
+ import ipaddress
140
+ import socket
141
+
142
+ host = raw_host.strip()
143
+ if host.startswith("[") and host.endswith("]"):
144
+ host = host[1:-1]
145
+ host = host.rstrip(".") # trailing-dot FQDN is the same host
146
+ if not host:
147
+ return False
148
+ if "%" in host: # IPv6 zone-id: strip before parsing
149
+ host = host.split("%", 1)[0]
150
+ # Obfuscated IP encodings (decimal 134744072, hex 0x08080808, octal
151
+ # 0177.0.0.1) have no legitimate use in a configured endpoint and are the
152
+ # classic filter bypass. Rejected outright, matching the JS twin, rather
153
+ # than resolved — even when they decode to a private address.
154
+ if _OBFUSCATED_IP_RE.match(host):
155
+ return False
156
+
157
+ try:
158
+ return _ip_is_private(ipaddress.ip_address(host))
159
+ except ValueError:
160
+ pass
161
+
162
+ try:
163
+ infos = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
164
+ except (OSError, UnicodeError):
165
+ return False # unresolvable → never send a credential to it
166
+ if not infos:
167
+ return False
168
+ for info in infos:
169
+ try:
170
+ addr = ipaddress.ip_address(info[4][0].split("%", 1)[0])
171
+ except ValueError:
172
+ return False
173
+ if not _ip_is_private(addr):
174
+ return False # ANY public answer disqualifies the name
175
+ return True
176
+
177
+
178
+ def _valid_graphify_url(raw: str) -> str:
179
+ """``raw`` when safe to send a credential to, else ''."""
180
+ if not raw:
181
+ return ""
182
+ from urllib.parse import urlsplit
183
+
184
+ try:
185
+ parts = urlsplit(raw)
186
+ except ValueError:
187
+ return ""
188
+ if parts.scheme == "https":
189
+ return raw
190
+ if parts.scheme != "http" or not parts.hostname:
191
+ return ""
192
+ return raw if _is_private_host(parts.hostname) else ""
193
+
194
+
195
+ def _resolve_graphify() -> tuple[str, str]:
196
+ """(url, token) from config.json + keys.json + env. Empty when unset.
197
+
198
+ The URL is validated here and not only in the installer: an unvalidated
199
+ endpoint plus a token from keys.json is credential redirection, and this
200
+ function reads ``GRAPHIFY_URL`` straight from the environment.
201
+ """
202
+ url = os.environ.get("GRAPHIFY_URL", "").strip()
203
+ token = os.environ.get("GRAPHIFY_TOKEN", "").strip()
204
+ gcfg = _section(_section(_read_json_dict(_KB_CONFIG_PATH), "knowledge"), "graphify")
205
+ if gcfg.get("enabled") is False:
206
+ return "", ""
207
+ url = _valid_graphify_url(url or str(gcfg.get("url") or "").strip())
208
+ if not url:
209
+ return "", ""
210
+ if not token and _KEYS_PATH.exists():
211
+ token = str(_read_json_dict(_KEYS_PATH).get("GRAPHIFY_TOKEN") or "").strip()
212
+ return url, token
213
+
214
+
215
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
216
+ """Refuse every redirect.
217
+
218
+ urllib copies non-content headers across a redirect, so a 302 from the
219
+ graph server would forward ``Authorization: Bearer <token>`` to whatever
220
+ host it names — reproduced end-to-end in QG review. An MCP endpoint has
221
+ no legitimate reason to redirect, so the safe answer is to never follow.
222
+ """
223
+
224
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
225
+ return None
226
+
227
+
228
+ def _graphify_post(url: str, headers: dict, payload: dict, extra: dict | None = None):
229
+ """One bounded MCP POST. Raises on transport failure — callers guard."""
230
+ body = json.dumps(payload).encode("utf-8")
231
+ req = urllib.request.Request(url, data=body, headers={**headers, **(extra or {})})
232
+ opener = urllib.request.build_opener(_NoRedirect)
233
+ return opener.open(req, timeout=_GRAPHIFY_TIMEOUT_S)
234
+
235
+
236
+ def _graphify_open_session(url: str, headers: dict) -> dict:
237
+ """Initialize the MCP session; returns the session-id header dict."""
238
+ init = {
239
+ "jsonrpc": "2.0", "id": 1, "method": "initialize",
240
+ "params": {
241
+ "protocolVersion": "2024-11-05", "capabilities": {},
242
+ "clientInfo": {"name": "synapse-l25", "version": "1.0"},
243
+ },
244
+ }
245
+ with _graphify_post(url, headers, init) as resp:
246
+ session_id = resp.headers.get("Mcp-Session-Id", "")
247
+ sid_header = {"Mcp-Session-Id": session_id} if session_id else {}
248
+ # Some MCP servers require the initialized notification before a
249
+ # tools/call; servers that do not need it ignore the extra request.
250
+ with contextlib.suppress(Exception):
251
+ _graphify_post(
252
+ url, headers,
253
+ {"jsonrpc": "2.0", "method": "notifications/initialized"}, sid_header,
254
+ ).close()
255
+ return sid_header
256
+
257
+
258
+ def _graphify_parse_sse(raw: str) -> dict | None:
259
+ """First JSON object in an SSE or plain JSON-RPC response body."""
260
+ for line in raw.splitlines():
261
+ stripped = line.strip()
262
+ if stripped.startswith("data:"):
263
+ stripped = stripped[5:].strip()
264
+ if not stripped:
265
+ continue
266
+ try:
267
+ return json.loads(stripped)
268
+ except ValueError:
269
+ continue
270
+ return None
271
+
272
+
273
+ def _graphify_query(prompt: str, url: str, token: str) -> str | None:
274
+ """Best-effort MCP `query_graph` over streamable HTTP. Never raises.
275
+
276
+ Returns the graph text-context, or None on any failure/timeout. Each of
277
+ the up-to-three requests is bounded by ``_GRAPHIFY_TIMEOUT_S``, so a fully
278
+ unresponsive endpoint costs ~3x that before this gives up and falls open.
279
+ """
280
+ headers = {
281
+ "Authorization": f"Bearer {token}",
282
+ "Content-Type": "application/json",
283
+ "Accept": "application/json, text/event-stream",
284
+ }
285
+ try:
286
+ sid_header = _graphify_open_session(url, headers)
287
+ call = {
288
+ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
289
+ "params": {"name": "query_graph", "arguments": {"query": prompt[:200]}},
290
+ }
291
+ with _graphify_post(url, headers, call, sid_header) as resp:
292
+ raw = resp.read().decode("utf-8", errors="replace")
293
+ data = _graphify_parse_sse(raw)
294
+ if not data:
295
+ return None
296
+ content = (data.get("result") or {}).get("content") or []
297
+ texts = [c.get("text", "") for c in content if c.get("type") == "text"]
298
+ return "\n".join(t for t in texts if t).strip() or None
299
+ except Exception:
300
+ return None
301
+
302
+
303
+ def _graphify_context(prompt: str) -> str:
304
+ """Formatted ``[arka:graph-remote]`` block, or '' — fail-open.
305
+
306
+ Deliberately NOT ``[arka:graph-context]``: L2.7 GraphContextLayer already
307
+ owns that marker for the LOCAL graph, under a stricter contract (never
308
+ truncates a source location, never injects AMBIGUOUS nodes). This block
309
+ comes from a remote server, is not confidence-filtered, and is clipped to
310
+ a byte budget — same marker, contradictory guarantees, so it gets its own.
311
+ """
312
+ if not prompt or not _l25_graphify_flag_on():
313
+ return ""
314
+ url, token = _resolve_graphify()
315
+ if not url or not token:
316
+ return ""
317
+ text = _graphify_query(prompt, url, token)
318
+ if not text:
319
+ return ""
320
+ excerpt = text[:600].rstrip()
321
+ return f"[arka:graph-remote unverified]\n{excerpt}\n"
322
+
323
+
50
324
  def _tokenize_for_jaccard(text: str) -> set[str]:
51
325
  if not text:
52
326
  return set()
@@ -121,7 +395,13 @@ def _format_kb_block(notes: list[dict], degraded: bool = False) -> str:
121
395
  path = note.get("path", "")
122
396
  excerpt = note.get("excerpt", "")
123
397
  relates = note.get("relates", []) or []
124
- suffix = " (inferred not authoritative)" if note.get("inferred") else ""
398
+ # Deliberately pt-PT: this block is operator-facing and every other
399
+ # string in it is pt-PT (header, Excerto, Relacionada, the degraded
400
+ # warning). The label was the lone English fragment. The RAG-honesty
401
+ # contract is unchanged — only the wording — and the two assertions
402
+ # guarding it in test_rag_honesty.py were updated in the same change
403
+ # for that reason, not to make a failing test pass.
404
+ suffix = " (inferida — não autoritativa)" if note.get("inferred") else ""
125
405
  lines.append(f"- [[{title}]]{suffix} (path: `{path}`)")
126
406
  if excerpt:
127
407
  lines.append(f" Excerto: {excerpt}")
@@ -160,7 +440,7 @@ def _jaccard_fallback(
160
440
  return [n for _, n in scored[:top_k]]
161
441
 
162
442
 
163
- def _load_fallback_notes(vault_path: Optional[Path]) -> list[dict]:
443
+ def _load_fallback_notes(vault_path: Path | None) -> list[dict]:
164
444
  if vault_path is None or not vault_path.exists() or not vault_path.is_dir():
165
445
  return []
166
446
  notes: list[dict] = []
@@ -211,7 +491,7 @@ def _hit_is_inferred(hit: dict) -> bool:
211
491
  if not source:
212
492
  return False
213
493
  try:
214
- with open(source, "r", encoding="utf-8", errors="ignore") as fh:
494
+ with open(source, encoding="utf-8", errors="ignore") as fh:
215
495
  head = fh.read(2048)
216
496
  except OSError:
217
497
  return False
@@ -246,7 +526,7 @@ def _apply_grounding_policy(notes: list[dict], max_notes: int) -> list[dict]:
246
526
  """Quarantine inferred notes (Dreaming output) from grounded context.
247
527
 
248
528
  Policy (PR-3 v4.1): inferred notes are EXCLUDED by default; they are
249
- only included — explicitly suffixed `(inferrednot authoritative)`
529
+ only included — explicitly suffixed `(inferidanão autoritativa)`
250
530
  by the formatter — when fewer than 2 grounded notes matched.
251
531
  """
252
532
  grounded = [n for n in notes if not n.get("inferred")]
@@ -277,7 +557,7 @@ class KBContextLayer(Layer):
277
557
  def __init__(
278
558
  self,
279
559
  vector_store: Any = None,
280
- vault_path: Optional[str] = None,
560
+ vault_path: str | None = None,
281
561
  max_notes: int = 5,
282
562
  min_similarity: float = 0.5,
283
563
  ) -> None:
@@ -302,6 +582,11 @@ class KBContextLayer(Layer):
302
582
  def cache_ttl(self) -> int:
303
583
  return 0
304
584
 
585
+ @property
586
+ def emits_block(self) -> bool:
587
+ """Content is the self-naming [arka:kb-context] / graph block."""
588
+ return True
589
+
305
590
  @property
306
591
  def priority(self) -> int:
307
592
  return 25
@@ -360,7 +645,7 @@ class KBContextLayer(Layer):
360
645
  ]
361
646
  return _apply_grounding_policy(fallback_notes, self._max_notes), False
362
647
 
363
- def build(self, prompt: str) -> Optional[str]:
648
+ def build(self, prompt: str) -> str | None:
364
649
  """Public entrypoint — returns the formatted block or None."""
365
650
  if not prompt or not _l25_feature_flag_on():
366
651
  return None
@@ -369,6 +654,28 @@ class KBContextLayer(Layer):
369
654
  return None
370
655
  return _format_kb_block(notes[: self._max_notes], degraded=degraded)
371
656
 
657
+ def _safe_graph_block(self, prompt: str) -> str:
658
+ """Graphify block, or ''. Guarded: Synapse does not wrap compute."""
659
+ try:
660
+ return _graphify_context(prompt)
661
+ except Exception:
662
+ return ""
663
+
664
+ def _render(self, notes: list[dict], degraded: bool, graph_block: str) -> tuple[str, str]:
665
+ """(content, tag) for the assembled KB + graph context."""
666
+ parts = []
667
+ if notes:
668
+ parts.append(_format_kb_block(notes[: self._max_notes], degraded=degraded))
669
+ if graph_block:
670
+ parts.append(graph_block)
671
+ tag = (
672
+ f"[kb-context:{len(notes)} degraded=keyword]" if degraded
673
+ else f"[kb-context:{len(notes)}]"
674
+ )
675
+ if graph_block:
676
+ tag = f"{tag[:-1]} +graph]"
677
+ return "\n".join(parts), tag
678
+
372
679
  def compute(self, ctx: PromptContext) -> LayerResult:
373
680
  start = time.time()
374
681
  if not ctx.user_input or not _l25_feature_flag_on():
@@ -378,18 +685,17 @@ class KBContextLayer(Layer):
378
685
  except Exception:
379
686
  return self._empty(start)
380
687
  self._record(ctx, len(notes))
381
- if not notes:
688
+ # Graphify graph-context is opt-in (default OFF) — a cheap no-op when
689
+ # the flag is off, and always fail-open so it never blocks Obsidian.
690
+ graph_block = self._safe_graph_block(ctx.user_input[:2000])
691
+ if not notes and not graph_block:
382
692
  return self._empty(start)
383
- block = _format_kb_block(notes[: self._max_notes], degraded=degraded)
384
- ms = int((time.time() - start) * 1000)
385
- tag = f"[kb-context:{len(notes)}]"
386
- if degraded:
387
- tag = f"[kb-context:{len(notes)} degraded=keyword]"
693
+ block, tag = self._render(notes, degraded, graph_block)
388
694
  return LayerResult(
389
695
  layer_id=self.id,
390
696
  tag=tag,
391
697
  content=block,
392
698
  tokens_est=len(block.split()),
393
- compute_ms=ms,
699
+ compute_ms=int((time.time() - start) * 1000),
394
700
  cached=False,
395
701
  )
@@ -80,6 +80,13 @@ const links = [[{
80
80
  onSelect: () => {
81
81
  open.value = false
82
82
  }
83
+ }, {
84
+ label: 'Plan Canvas',
85
+ icon: 'i-lucide-drafting-compass',
86
+ to: '/plan-canvas',
87
+ onSelect: () => {
88
+ open.value = false
89
+ }
83
90
  }, {
84
91
  label: 'Knowledge',
85
92
  icon: 'i-lucide-brain',