@hybridlabor-api/aos 4.4.0 → 4.4.2-beta.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 (306) hide show
  1. package/.agents/skills/firecrawl/SKILL.md +149 -0
  2. package/.agents/skills/firecrawl/rules/install.md +82 -0
  3. package/.agents/skills/firecrawl/rules/security.md +26 -0
  4. package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
  5. package/.agents/skills/firecrawl-build/SKILL.md +39 -0
  6. package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
  7. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
  8. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
  9. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
  10. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
  11. package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
  12. package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
  13. package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
  14. package/.agents/skills/firecrawl-download/SKILL.md +70 -0
  15. package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
  16. package/.agents/skills/firecrawl-map/SKILL.md +51 -0
  17. package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
  18. package/.agents/skills/firecrawl-search/SKILL.md +60 -0
  19. package/.claude/hooks/memb-inject.mjs +6 -0
  20. package/CLAUDE.md +573 -0
  21. package/README.de.md +0 -8
  22. package/README.md +0 -8
  23. package/README.pt.md +0 -8
  24. package/assets/brand/ao-ant-mark.svg +91 -0
  25. package/assets/brand/bdb-core-mark.svg +34 -0
  26. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  27. package/assets/brand/memb-crystal-mark.svg +18 -0
  28. package/bin/aos-config.mjs +89 -0
  29. package/bin/aos-dashboard.mjs +608 -0
  30. package/bin/aos-uninstall.mjs +227 -0
  31. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  32. package/docs/skills_table.md +0 -3
  33. package/installer.js +428 -237
  34. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  35. package/mcps/after-effects-mcp/build/index.js +840 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  37. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  38. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  39. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  40. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  41. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  42. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  43. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  44. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  45. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  46. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  47. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  48. package/mcps/computer-use-mcp/dist/client.js +136 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  50. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  51. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  52. package/mcps/computer-use-mcp/dist/native.js +50 -0
  53. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  54. package/mcps/computer-use-mcp/dist/server.js +342 -0
  55. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  56. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  57. package/package.json +13 -4
  58. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  59. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  60. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  61. package/skills/global_config/aos-setup/SKILL.md +110 -11
  62. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +48 -4
  63. package/skills/global_config/ask-tim/SKILL.md +2 -7
  64. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  65. package/bin/setup-saas.mjs +0 -792
  66. package/mcps/blender-mcp-server/.dockerignore +0 -19
  67. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  68. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  69. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  70. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  71. package/mcps/blender-mcp-server/Dockerfile +0 -19
  72. package/mcps/blender-mcp-server/LICENSE +0 -21
  73. package/mcps/blender-mcp-server/README.md +0 -409
  74. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  75. package/mcps/blender-mcp-server/addon/models.py +0 -223
  76. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  77. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  78. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  79. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  80. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  81. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  82. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  83. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  85. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  86. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  87. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  88. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  89. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  90. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  91. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  92. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  93. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  94. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  95. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  96. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  97. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  98. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  99. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  100. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  101. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  104. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  105. package/mcps/davinci-mcp-professional/.bandit +0 -35
  106. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  107. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  108. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  109. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  110. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  112. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  113. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  114. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  115. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  116. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  117. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  118. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  119. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  120. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  121. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  122. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  123. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  124. package/mcps/davinci-mcp-professional/README.md +0 -143
  125. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  126. package/mcps/davinci-mcp-professional/USING.md +0 -301
  127. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  128. package/mcps/davinci-mcp-professional/build.py +0 -169
  129. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  130. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  131. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  132. package/mcps/davinci-mcp-professional/main.py +0 -40
  133. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  134. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  135. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  144. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  145. package/mcps/davinci-mcp-professional/test.py +0 -186
  146. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  147. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  148. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  149. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  151. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  152. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  153. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  154. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  155. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  156. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  157. package/mcps/davinci-resolve-mcp/README.md +0 -173
  158. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  159. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  160. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  161. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  162. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  163. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  164. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  165. package/mcps/davinci-resolve-mcp/package.json +0 -52
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  169. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  170. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  171. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  172. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  173. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  174. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  176. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  177. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  178. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  179. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  180. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  181. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  182. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  183. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  184. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  185. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  187. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  188. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  189. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  190. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  192. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  193. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  194. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  195. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  196. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  200. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  201. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  202. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  203. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  204. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  205. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  206. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  207. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  208. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  209. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  210. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  211. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  212. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  213. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  214. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  215. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  216. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  217. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  218. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  219. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  220. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  222. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  223. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  225. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  227. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  229. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  230. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  231. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  232. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  233. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  234. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  235. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  237. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  238. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  239. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  240. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  241. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  245. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  246. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  247. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  248. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  249. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  250. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  256. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  257. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  258. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  259. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  260. package/mcps/vectorworks-mcp/README.md +0 -156
  261. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  262. package/mcps/vectorworks-mcp/app/api.py +0 -52
  263. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  264. package/mcps/vectorworks-mcp/app/config.py +0 -29
  265. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  266. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  267. package/mcps/vectorworks-mcp/app/main.py +0 -33
  268. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  269. package/mcps/vectorworks-mcp/app/search.py +0 -54
  270. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  271. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  276. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  277. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  279. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  280. package/mcps/vectorworks-mcp/design.md +0 -57
  281. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  282. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  283. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  284. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  285. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  286. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  287. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  288. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  289. package/scripts/ecosystem-health-audit.js +0 -257
  290. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  291. package/skills/bdb-dev-os-skill/package.json +0 -11
  292. package/skills/bdbsaastraining/SKILL.md +0 -316
  293. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  294. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  295. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  296. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  297. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  298. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  299. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  300. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  301. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  302. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  303. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  304. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  305. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  306. package/skills/global_config/bdbsaashost/SKILL.md +0 -125
@@ -1,755 +0,0 @@
1
- """Memory and heartbeat layer for V2 analysis architecture.
2
-
3
- Per the V2 shot schema spec §9 and the project_v2_architecture memory,
4
- each project's analysis root carries narrative memory alongside the structured DB:
5
-
6
- {analysis_root}/memory/
7
- bin_summary.md - Regenerated post-analysis: machine's first impression of the bin.
8
- session_notes/ - One file per chat session, dated.
9
- corrections.md - Running log of what humans fixed and why.
10
- decisions.md - Load-bearing conclusions reached together.
11
-
12
- {analysis_root}/heartbeat.json
13
- Current-state snapshot updated by the analysis pipeline:
14
- last run timestamp, clip counts, what's pending, recent failures,
15
- what's new since session N. Read at session start so the LLM walks in
16
- knowing what changed overnight.
17
-
18
- The user-scoped Soul layer lives separately at ~/davinci-resolve-mcp/soul/
19
- (see soul scaffolding in P5).
20
- """
21
-
22
- from __future__ import annotations
23
-
24
- import json
25
- import os
26
- import time
27
- from pathlib import Path
28
- from typing import Any, Dict, Iterable, List, Optional, Tuple
29
-
30
-
31
- MEMORY_DIR_NAME = "memory"
32
- HEARTBEAT_FILENAME = "heartbeat.json"
33
- BIN_SUMMARY_FILENAME = "bin_summary.md"
34
- SESSION_NOTES_DIR_NAME = "session_notes"
35
- CORRECTIONS_FILENAME = "corrections.md"
36
- DECISIONS_FILENAME = "decisions.md"
37
-
38
- # V2 B6: panel_state.json — shared state between chat and the control panel.
39
- # Lives next to heartbeat.json at the project's analysis root. Either side can
40
- # write; both read at session start. Single-user model, last-write-wins.
41
- PANEL_STATE_FILENAME = "panel_state.json"
42
- PANEL_STATE_SCHEMA_VERSION = "2.0"
43
-
44
- HEARTBEAT_SCHEMA_VERSION = "2.0"
45
-
46
- # ---- User-scoped soul layer (cross-project) ----
47
- # Soul lives outside any specific project's analysis root, under a `_soul`
48
- # directory sibling to project directories. The underscore prefix marks it
49
- # as user-scoped, not a project. Cross-project continuity: when Sam starts a
50
- # new project the soul still applies; per-project memory + DB are fresh.
51
- SOUL_DIR_NAME = "_soul"
52
- SOUL_PERSPECTIVE_FILENAME = "perspective.md"
53
- SOUL_WORKING_STYLE_FILENAME = "working_style.md"
54
- SOUL_LEARNED_FILENAME = "learned_from_corrections.md"
55
-
56
-
57
- def _now_iso() -> str:
58
- return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
59
-
60
-
61
- def _today_date() -> str:
62
- return time.strftime("%Y-%m-%d", time.gmtime())
63
-
64
-
65
- def memory_dir(project_root: str) -> str:
66
- return os.path.join(project_root, MEMORY_DIR_NAME)
67
-
68
-
69
- def session_notes_dir(project_root: str) -> str:
70
- return os.path.join(memory_dir(project_root), SESSION_NOTES_DIR_NAME)
71
-
72
-
73
- def heartbeat_path(project_root: str) -> str:
74
- return os.path.join(project_root, HEARTBEAT_FILENAME)
75
-
76
-
77
- def bin_summary_path(project_root: str) -> str:
78
- return os.path.join(memory_dir(project_root), BIN_SUMMARY_FILENAME)
79
-
80
-
81
- def corrections_path(project_root: str) -> str:
82
- return os.path.join(memory_dir(project_root), CORRECTIONS_FILENAME)
83
-
84
-
85
- def decisions_path(project_root: str) -> str:
86
- return os.path.join(memory_dir(project_root), DECISIONS_FILENAME)
87
-
88
-
89
- def ensure_memory_structure(project_root: str) -> Dict[str, Any]:
90
- """Create the memory directory structure if it doesn't exist. Idempotent."""
91
- created = []
92
- for d in (memory_dir(project_root), session_notes_dir(project_root)):
93
- if not os.path.isdir(d):
94
- os.makedirs(d, exist_ok=True)
95
- created.append(d)
96
- # Seed the long-running narrative files if absent
97
- for path, header in (
98
- (corrections_path(project_root), "# Corrections log\n\nRunning record of what was corrected, by whom, when, and why.\n\n"),
99
- (decisions_path(project_root), "# Decisions log\n\nLoad-bearing conclusions reached during work on this project.\n\n"),
100
- ):
101
- if not os.path.isfile(path):
102
- with open(path, "w", encoding="utf-8") as handle:
103
- handle.write(header)
104
- created.append(path)
105
- return {
106
- "success": True,
107
- "project_root": project_root,
108
- "memory_dir": memory_dir(project_root),
109
- "session_notes_dir": session_notes_dir(project_root),
110
- "created": created,
111
- }
112
-
113
-
114
- # ============ Heartbeat ============
115
-
116
- def read_heartbeat(project_root: str) -> Optional[Dict[str, Any]]:
117
- """Read heartbeat.json. Returns None if it doesn't exist or is malformed."""
118
- path = heartbeat_path(project_root)
119
- if not os.path.isfile(path):
120
- return None
121
- try:
122
- with open(path, "r", encoding="utf-8") as handle:
123
- return json.load(handle)
124
- except (OSError, json.JSONDecodeError):
125
- return None
126
-
127
-
128
- def write_heartbeat(project_root: str, payload: Dict[str, Any]) -> Dict[str, Any]:
129
- """Write heartbeat.json atomically."""
130
- ensure_memory_structure(project_root)
131
- payload = dict(payload)
132
- payload.setdefault("schema_version", HEARTBEAT_SCHEMA_VERSION)
133
- payload["updated_at"] = _now_iso()
134
- path = heartbeat_path(project_root)
135
- tmp_path = path + ".tmp"
136
- try:
137
- with open(tmp_path, "w", encoding="utf-8") as handle:
138
- json.dump(payload, handle, indent=2, sort_keys=True)
139
- os.replace(tmp_path, path)
140
- return {"success": True, "path": path}
141
- except OSError as exc:
142
- if os.path.isfile(tmp_path):
143
- try:
144
- os.remove(tmp_path)
145
- except OSError:
146
- pass
147
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
148
-
149
-
150
- def update_heartbeat(
151
- project_root: str,
152
- *,
153
- last_run: Optional[Dict[str, Any]] = None,
154
- clip_counts: Optional[Dict[str, int]] = None,
155
- pending: Optional[List[Dict[str, Any]]] = None,
156
- recent_failures: Optional[List[Dict[str, Any]]] = None,
157
- new_since_last_session: Optional[List[Dict[str, Any]]] = None,
158
- notes: Optional[List[str]] = None,
159
- ) -> Dict[str, Any]:
160
- """Update heartbeat fields, preserving keys not explicitly overridden."""
161
- existing = read_heartbeat(project_root) or {}
162
- payload = dict(existing)
163
- if last_run is not None:
164
- payload["last_run"] = last_run
165
- if clip_counts is not None:
166
- payload["clip_counts"] = clip_counts
167
- if pending is not None:
168
- payload["pending"] = pending
169
- if recent_failures is not None:
170
- payload["recent_failures"] = recent_failures
171
- if new_since_last_session is not None:
172
- payload["new_since_last_session"] = new_since_last_session
173
- if notes is not None:
174
- payload["notes"] = notes
175
- return write_heartbeat(project_root, payload)
176
-
177
-
178
- # ============ Bin summary ============
179
-
180
- def regenerate_bin_summary_from_manifest(
181
- project_root: str,
182
- manifest: Dict[str, Any],
183
- *,
184
- project_name: Optional[str] = None,
185
- ) -> Dict[str, Any]:
186
- """Write bin_summary.md from a manifest's per-clip rows.
187
-
188
- V2.0 (this function): a smart aggregator that synthesizes a bin-level
189
- overview from per-clip summaries — clip counts, dominant editorial roles,
190
- top search_tags, recurring locations, select-potential distribution,
191
- energy arc, and a per-clip listing. It's NOT a vision-synthesized
192
- briefing yet.
193
-
194
- V2.1 (deferred): replace with a separate vision pass that reads all
195
- per-clip clip_summary fields + a representative frame per clip, and
196
- produces a true colleague-style briefing paragraph. See C2 in the
197
- gameplan; implementation pattern follows the per-clip vision flow
198
- (deferred payload + commit_bin_summary action).
199
- """
200
- ensure_memory_structure(project_root)
201
- clips = manifest.get("clips") or []
202
- project_name = project_name or manifest.get("project_name") or "(unknown project)"
203
-
204
- # Read per-clip analyses from disk so we can aggregate
205
- enriched: List[Dict[str, Any]] = []
206
- for clip in clips:
207
- info = {
208
- "record": clip.get("record") or {},
209
- "clip": clip,
210
- "report": None,
211
- }
212
- analysis_path = clip.get("analysis_json")
213
- if analysis_path and os.path.isfile(analysis_path):
214
- try:
215
- with open(analysis_path, "r", encoding="utf-8") as handle:
216
- info["report"] = json.load(handle)
217
- except (OSError, json.JSONDecodeError):
218
- pass
219
- enriched.append(info)
220
-
221
- # Aggregate fields from the per-clip visual analyses
222
- primary_use_counts: Dict[str, int] = {}
223
- select_potential_counts: Dict[str, int] = {}
224
- style_counts: Dict[str, int] = {}
225
- energy_counts: Dict[str, int] = {}
226
- tag_counts: Dict[str, int] = {}
227
- location_counts: Dict[str, int] = {}
228
- total_duration = 0.0
229
- longest_clips: List[Tuple[float, str, Optional[str]]] = []
230
- high_select_clips: List[Tuple[str, str, Optional[str]]] = []
231
- pending_vision_count = 0
232
-
233
- def _bump(d: Dict[str, int], key: Optional[str]) -> None:
234
- if key:
235
- d[str(key)] = d.get(str(key), 0) + 1
236
-
237
- for info in enriched:
238
- record = info["record"]
239
- report = info["report"] or {}
240
- visual = report.get("visual") if isinstance(report.get("visual"), dict) else None
241
- duration = _extract_duration_seconds(record)
242
- total_duration += float(duration or 0.0)
243
- clip_name = record.get("clip_name") or "Untitled"
244
- clip_id = record.get("clip_id")
245
- if duration:
246
- longest_clips.append((float(duration), clip_name, clip_id))
247
-
248
- if visual is None:
249
- pending_vision_count += 1
250
- continue
251
- classification = visual.get("editorial_classification") or {}
252
- _bump(primary_use_counts, classification.get("primary_use"))
253
- _bump(select_potential_counts, classification.get("select_potential"))
254
- _bump(style_counts, classification.get("style"))
255
- _bump(energy_counts, classification.get("energy_arc"))
256
- if classification.get("select_potential") == "high":
257
- high_select_clips.append((clip_name, classification.get("primary_use") or "", clip_id))
258
- # Aggregate search tags
259
- editing_notes = visual.get("editing_notes") or {}
260
- for tag in (editing_notes.get("search_tags") or []):
261
- if isinstance(tag, str) and tag.strip():
262
- _bump(tag_counts, tag.strip().lower())
263
- # Aggregate locations from content if structured
264
- content = visual.get("content") or {}
265
- for loc in (content.get("locations") or []):
266
- if isinstance(loc, str) and loc.strip():
267
- _bump(location_counts, loc.strip())
268
-
269
- def _top(d: Dict[str, int], n: int = 5) -> List[Tuple[str, int]]:
270
- return sorted(d.items(), key=lambda kv: (-kv[1], kv[0]))[:n]
271
-
272
- longest_clips.sort(reverse=True)
273
- top_5_longest = longest_clips[:5]
274
-
275
- # Build the markdown
276
- lines: List[str] = []
277
- lines.append(f"# Bin summary — {project_name}\n")
278
- lines.append(f"_Regenerated: {_now_iso()}_\n")
279
- lines.append("_(V2.0 aggregated overview; V2.1 will replace with a vision-synthesized colleague briefing — see C2 in the gameplan.)_\n\n")
280
-
281
- lines.append("## Overview\n\n")
282
- lines.append(f"- **Clip count**: {len(clips)}")
283
- if pending_vision_count:
284
- lines.append(f" ({pending_vision_count} pending vision)")
285
- lines.append("\n")
286
- lines.append(f"- **Total duration**: ~{_fmt_duration(total_duration)}\n")
287
- lines.append(f"- **Analysis schema version**: {manifest.get('analysis_version', 'unknown')}\n\n")
288
-
289
- if primary_use_counts:
290
- lines.append("## Editorial composition\n\n")
291
- lines.append("**Primary use:**\n\n")
292
- for use, n in _top(primary_use_counts, 10):
293
- lines.append(f"- {use}: {n}\n")
294
- lines.append("\n")
295
-
296
- if select_potential_counts:
297
- lines.append("**Select potential distribution:**\n\n")
298
- for level in ("high", "medium", "low"):
299
- if level in select_potential_counts:
300
- lines.append(f"- {level}: {select_potential_counts[level]}\n")
301
- lines.append("\n")
302
-
303
- if style_counts:
304
- lines.append("**Dominant style:**\n\n")
305
- for s, n in _top(style_counts, 5):
306
- lines.append(f"- {s}: {n}\n")
307
- lines.append("\n")
308
-
309
- if energy_counts:
310
- lines.append("**Energy arcs:**\n\n")
311
- for e, n in _top(energy_counts, 5):
312
- lines.append(f"- {e}: {n}\n")
313
- lines.append("\n")
314
-
315
- if tag_counts:
316
- lines.append("## Top tags\n\n")
317
- top_tags = _top(tag_counts, 20)
318
- lines.append("`" + "` `".join(f"{t} ({n})" for t, n in top_tags) + "`\n\n")
319
-
320
- if location_counts:
321
- lines.append("## Recurring locations\n\n")
322
- for loc, n in _top(location_counts, 10):
323
- lines.append(f"- {loc}: {n}\n")
324
- lines.append("\n")
325
-
326
- if high_select_clips:
327
- lines.append("## High select-potential clips\n\n")
328
- for name, use, cid in high_select_clips[:10]:
329
- cid_text = f" `{cid}`" if cid else ""
330
- lines.append(f"- **{name}**{cid_text} — {use}\n")
331
- lines.append("\n")
332
-
333
- if top_5_longest:
334
- lines.append("## Longest clips\n\n")
335
- for dur, name, cid in top_5_longest:
336
- cid_text = f" `{cid}`" if cid else ""
337
- lines.append(f"- **{name}**{cid_text} — {_fmt_duration(dur)}\n")
338
- lines.append("\n")
339
-
340
- lines.append("## Clips\n\n")
341
- for index, info in enumerate(enriched, 1):
342
- record = info["record"]
343
- name = record.get("clip_name") or record.get("file_path") or f"Clip {index}"
344
- clip_id = record.get("clip_id") or "(no id)"
345
- duration = _fmt_duration(_extract_duration_seconds(record))
346
- bin_path = record.get("bin_path") or ""
347
- lines.append(f"### {index}. {name}\n\n")
348
- lines.append(f"- **clip_id**: `{clip_id}`\n")
349
- lines.append(f"- **duration**: {duration}\n")
350
- if bin_path:
351
- lines.append(f"- **bin**: {bin_path}\n")
352
-
353
- summary = _read_clip_summary(info["clip"])
354
- if summary:
355
- lines.append(f"\n{summary}\n")
356
- else:
357
- lines.append("\n_Summary not yet available (vision pending or analysis incomplete)._\n")
358
- lines.append("\n")
359
-
360
- path = bin_summary_path(project_root)
361
- # Atomic write (temp + os.replace): a crash mid-write must not truncate the
362
- # bin summary that session_start_context reads at startup (PS4).
363
- tmp_path = path + ".tmp"
364
- try:
365
- with open(tmp_path, "w", encoding="utf-8") as handle:
366
- handle.writelines(lines)
367
- os.replace(tmp_path, path)
368
- return {
369
- "success": True,
370
- "path": path,
371
- "clip_count": len(clips),
372
- "pending_vision_count": pending_vision_count,
373
- "aggregates": {
374
- "primary_use": primary_use_counts,
375
- "select_potential": select_potential_counts,
376
- "style": style_counts,
377
- "energy_arc": energy_counts,
378
- "top_tags": dict(_top(tag_counts, 20)),
379
- "top_locations": dict(_top(location_counts, 10)),
380
- },
381
- }
382
- except OSError as exc:
383
- if os.path.isfile(tmp_path):
384
- try:
385
- os.remove(tmp_path)
386
- except OSError:
387
- pass
388
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
389
-
390
-
391
- def _extract_duration_seconds(record: Dict[str, Any]) -> float:
392
- # Try a few common shapes the record may carry duration in
393
- for key in ("duration_seconds", "duration", "Duration"):
394
- value = record.get(key)
395
- if isinstance(value, (int, float)):
396
- return float(value)
397
- return 0.0
398
-
399
-
400
- def _fmt_duration(seconds: float) -> str:
401
- if seconds <= 0:
402
- return "0s"
403
- if seconds < 60:
404
- return f"{seconds:.1f}s"
405
- minutes, sec = divmod(int(seconds), 60)
406
- if minutes < 60:
407
- return f"{minutes}m{sec:02d}s"
408
- hours, minutes = divmod(minutes, 60)
409
- return f"{hours}h{minutes:02d}m"
410
-
411
-
412
- def _read_clip_summary(clip: Dict[str, Any]) -> Optional[str]:
413
- """Try to read the clip_summary from the clip's analysis.json if present."""
414
- analysis_json_path = clip.get("analysis_json")
415
- if not analysis_json_path or not os.path.isfile(analysis_json_path):
416
- return None
417
- try:
418
- with open(analysis_json_path, "r", encoding="utf-8") as handle:
419
- report = json.load(handle)
420
- except (OSError, json.JSONDecodeError):
421
- return None
422
- visual = report.get("visual") or {}
423
- summary = visual.get("clip_summary")
424
- if isinstance(summary, str) and summary.strip():
425
- return summary.strip()
426
- if isinstance(summary, dict):
427
- para = summary.get("paragraph") or summary.get("one_line")
428
- if isinstance(para, str) and para.strip():
429
- return para.strip()
430
- return None
431
-
432
-
433
- # ============ Session notes ============
434
-
435
- def record_session_note(project_root: str, note: str, *, session_id: Optional[str] = None) -> Dict[str, Any]:
436
- """Append a session note to today's session_notes file."""
437
- ensure_memory_structure(project_root)
438
- date = _today_date()
439
- path = os.path.join(session_notes_dir(project_root), f"{date}.md")
440
- header = f"# Session notes — {date}\n\n" if not os.path.isfile(path) else ""
441
- entry = f"## {_now_iso()}" + (f" ({session_id})" if session_id else "") + "\n\n" + note.strip() + "\n\n"
442
- try:
443
- with open(path, "a", encoding="utf-8") as handle:
444
- if header:
445
- handle.write(header)
446
- handle.write(entry)
447
- return {"success": True, "path": path}
448
- except OSError as exc:
449
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
450
-
451
-
452
- def record_correction(
453
- project_root: str,
454
- *,
455
- entity_type: str,
456
- entity_uuid: str,
457
- field_path: str,
458
- previous_value: Any,
459
- new_value: Any,
460
- author: str,
461
- reason: Optional[str] = None,
462
- ) -> Dict[str, Any]:
463
- """Append a correction entry to corrections.md."""
464
- ensure_memory_structure(project_root)
465
- path = corrections_path(project_root)
466
- entry_lines = [
467
- f"## {_now_iso()} — {author}\n",
468
- f"- **target**: {entity_type} `{entity_uuid}` field `{field_path}`\n",
469
- f"- **previous**: `{_short_repr(previous_value)}`\n",
470
- f"- **new**: `{_short_repr(new_value)}`\n",
471
- ]
472
- if reason:
473
- entry_lines.append(f"- **why**: {reason}\n")
474
- entry_lines.append("\n")
475
- try:
476
- with open(path, "a", encoding="utf-8") as handle:
477
- handle.write("".join(entry_lines))
478
- return {"success": True, "path": path}
479
- except OSError as exc:
480
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
481
-
482
-
483
- def record_decision(
484
- project_root: str,
485
- *,
486
- title: str,
487
- description: str,
488
- author: str,
489
- tags: Optional[Iterable[str]] = None,
490
- ) -> Dict[str, Any]:
491
- """Append a load-bearing decision to decisions.md."""
492
- ensure_memory_structure(project_root)
493
- path = decisions_path(project_root)
494
- tag_line = ""
495
- if tags:
496
- tag_str = ", ".join(f"`{t}`" for t in tags)
497
- tag_line = f"_Tags: {tag_str}_\n\n"
498
- entry = f"## {_now_iso()} — {title}\n\n_By: {author}_\n\n{tag_line}{description.strip()}\n\n"
499
- try:
500
- with open(path, "a", encoding="utf-8") as handle:
501
- handle.write(entry)
502
- return {"success": True, "path": path}
503
- except OSError as exc:
504
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
505
-
506
-
507
- def _short_repr(value: Any, limit: int = 200) -> str:
508
- if value is None:
509
- return "null"
510
- if isinstance(value, (str, int, float, bool)):
511
- s = str(value)
512
- else:
513
- try:
514
- s = json.dumps(value, sort_keys=True)
515
- except (TypeError, ValueError):
516
- s = repr(value)
517
- return s if len(s) <= limit else s[:limit] + "…"
518
-
519
-
520
- # ============ Panel state (V2 B6 — chat ↔ control panel sync) ============
521
-
522
- def panel_state_path(project_root: str) -> str:
523
- return os.path.join(project_root, PANEL_STATE_FILENAME)
524
-
525
-
526
- def read_panel_state(project_root: str) -> Optional[Dict[str, Any]]:
527
- """Read panel_state.json. Returns None if absent or unreadable.
528
-
529
- The state file holds: current_clip_id, current_shot_index, current_view
530
- ('bin' | 'clip' | 'shot'), focus_history, plus whatever either chat or
531
- the panel writes. Either side can read; either side can write.
532
- """
533
- path = panel_state_path(project_root)
534
- if not os.path.isfile(path):
535
- return None
536
- try:
537
- with open(path, "r", encoding="utf-8") as handle:
538
- return json.load(handle)
539
- except (OSError, json.JSONDecodeError):
540
- return None
541
-
542
-
543
- def write_panel_state(
544
- project_root: str,
545
- updates: Dict[str, Any],
546
- *,
547
- written_by: Optional[str] = None,
548
- merge: bool = True,
549
- ) -> Dict[str, Any]:
550
- """Write panel_state.json atomically.
551
-
552
- `updates` is merged into the existing state by default (merge=True) so
553
- callers can partial-update fields. Pass merge=False to replace.
554
- """
555
- ensure_memory_structure(project_root)
556
- path = panel_state_path(project_root)
557
- if merge:
558
- existing = read_panel_state(project_root) or {}
559
- payload = dict(existing)
560
- payload.update(updates)
561
- else:
562
- payload = dict(updates)
563
- payload["schema_version"] = PANEL_STATE_SCHEMA_VERSION
564
- payload["updated_at"] = _now_iso()
565
- if written_by:
566
- payload["last_written_by"] = written_by
567
- tmp_path = path + ".tmp"
568
- try:
569
- with open(tmp_path, "w", encoding="utf-8") as handle:
570
- json.dump(payload, handle, indent=2, sort_keys=True)
571
- os.replace(tmp_path, path)
572
- return {"success": True, "path": path, "state": payload}
573
- except OSError as exc:
574
- if os.path.isfile(tmp_path):
575
- try:
576
- os.remove(tmp_path)
577
- except OSError:
578
- pass
579
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
580
-
581
-
582
- # ============ Soul (user-scoped, cross-project) ============
583
-
584
- def soul_dir(analysis_base_root: str) -> str:
585
- """Return the user-scoped soul directory path.
586
-
587
- `analysis_base_root` is the parent of all project analysis roots
588
- (typically `~/Documents/davinci-resolve-mcp-analysis`). The soul lives
589
- at `{base}/_soul/` so it's discoverable alongside project data but
590
- clearly marked user-scoped via the underscore prefix.
591
- """
592
- return os.path.join(analysis_base_root, SOUL_DIR_NAME)
593
-
594
-
595
- def ensure_soul_structure(analysis_base_root: str) -> Dict[str, Any]:
596
- """Create the soul directory + initial files if absent. Idempotent."""
597
- d = soul_dir(analysis_base_root)
598
- if not os.path.isdir(d):
599
- os.makedirs(d, exist_ok=True)
600
-
601
- created = []
602
- seeds = {
603
- SOUL_PERSPECTIVE_FILENAME: _SOUL_PERSPECTIVE_TEMPLATE,
604
- SOUL_WORKING_STYLE_FILENAME: _SOUL_WORKING_STYLE_TEMPLATE,
605
- SOUL_LEARNED_FILENAME: _SOUL_LEARNED_TEMPLATE,
606
- }
607
- for filename, template in seeds.items():
608
- path = os.path.join(d, filename)
609
- if not os.path.isfile(path):
610
- with open(path, "w", encoding="utf-8") as handle:
611
- handle.write(template)
612
- created.append(path)
613
-
614
- return {
615
- "success": True,
616
- "soul_dir": d,
617
- "created": created,
618
- }
619
-
620
-
621
- def read_soul(analysis_base_root: str) -> Dict[str, Optional[str]]:
622
- """Read all soul files. Returns dict mapping filename → contents (or None)."""
623
- d = soul_dir(analysis_base_root)
624
- out: Dict[str, Optional[str]] = {}
625
- for name in (SOUL_PERSPECTIVE_FILENAME, SOUL_WORKING_STYLE_FILENAME, SOUL_LEARNED_FILENAME):
626
- path = os.path.join(d, name)
627
- if os.path.isfile(path):
628
- try:
629
- with open(path, "r", encoding="utf-8") as handle:
630
- out[name] = handle.read()
631
- except OSError:
632
- out[name] = None
633
- else:
634
- out[name] = None
635
- return out
636
-
637
-
638
- def append_to_soul(
639
- analysis_base_root: str,
640
- filename: str,
641
- entry: str,
642
- *,
643
- author: str,
644
- why: Optional[str] = None,
645
- ) -> Dict[str, Any]:
646
- """Append an entry to a soul file, with timestamp and author header.
647
-
648
- The soul evolves slowly; entries are added when an observation generalizes
649
- beyond a single project. Pruning / consolidation is V2.1+ work.
650
- """
651
- if filename not in (SOUL_PERSPECTIVE_FILENAME, SOUL_WORKING_STYLE_FILENAME, SOUL_LEARNED_FILENAME):
652
- return {"success": False, "error": f"Unknown soul file: {filename}"}
653
- ensure_soul_structure(analysis_base_root)
654
- path = os.path.join(soul_dir(analysis_base_root), filename)
655
- block = [f"\n## {_now_iso()} — {author}\n"]
656
- if why:
657
- block.append(f"_Why: {why}_\n\n")
658
- block.append(entry.strip() + "\n")
659
- try:
660
- with open(path, "a", encoding="utf-8") as handle:
661
- handle.write("".join(block))
662
- return {"success": True, "path": path}
663
- except OSError as exc:
664
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
665
-
666
-
667
- _SOUL_PERSPECTIVE_TEMPLATE = """# Soul — Perspective
668
-
669
- User-scoped, cross-project. The accumulated aesthetic and editorial perspective
670
- that carries between projects. Slow-evolving. Updated when something
671
- generalizes beyond a single project's specifics.
672
-
673
- This file is one of three soul layers. The others:
674
- - `working_style.md` — how the user and I collaborate
675
- - `learned_from_corrections.md` — patterns from corrections across projects
676
-
677
- ---
678
- """
679
-
680
- _SOUL_WORKING_STYLE_TEMPLATE = """# Soul — Working style
681
-
682
- User-scoped, cross-project. How the user and I work together — pace,
683
- preferences, the rhythm of our collaboration.
684
-
685
- Entries added when a working-style observation feels durable across projects,
686
- not specific to this one.
687
-
688
- ---
689
- """
690
-
691
- _SOUL_LEARNED_TEMPLATE = """# Soul — Learned from corrections
692
-
693
- User-scoped, cross-project. Patterns from corrections the user has made across
694
- projects. Generalized lessons, not specific fixes.
695
-
696
- Example pattern: "User consistently prefers concrete physical description over
697
- interpretation when subject identity is uncertain. Hedge identity claims;
698
- describe what's visible."
699
-
700
- Specific per-project corrections live in `{project}/memory/corrections.md`.
701
- This file is for what generalizes across projects.
702
-
703
- ---
704
- """
705
-
706
-
707
- # ============ Session start protocol ============
708
-
709
- def session_start_context(
710
- project_root: str,
711
- *,
712
- analysis_base_root: Optional[str] = None,
713
- ) -> Dict[str, Any]:
714
- """Assemble the context an LLM should load at session start.
715
-
716
- Read order: soul (cross-project) → project memory → heartbeat → DB on demand.
717
-
718
- Args:
719
- project_root: This project's analysis root.
720
- analysis_base_root: Parent of all project analysis roots, for the soul.
721
- If None, derived as the parent of project_root.
722
- """
723
- ensure_memory_structure(project_root)
724
- if analysis_base_root is None:
725
- analysis_base_root = os.path.dirname(project_root)
726
- ensure_soul_structure(analysis_base_root)
727
-
728
- heartbeat = read_heartbeat(project_root)
729
- summary_path = bin_summary_path(project_root)
730
- notes_dir = session_notes_dir(project_root)
731
- recent_notes: List[str] = []
732
- if os.path.isdir(notes_dir):
733
- try:
734
- files = sorted(os.listdir(notes_dir), reverse=True)
735
- recent_notes = [os.path.join(notes_dir, f) for f in files[:7] if f.endswith(".md")]
736
- except OSError:
737
- pass
738
-
739
- return {
740
- "project_root": project_root,
741
- "analysis_base_root": analysis_base_root,
742
- # Soul (cross-project, user-scoped) — read first; informs perspective.
743
- "soul_dir": soul_dir(analysis_base_root),
744
- "soul_files": read_soul(analysis_base_root),
745
- # Project-scoped narrative
746
- "heartbeat": heartbeat,
747
- "heartbeat_path": heartbeat_path(project_root),
748
- "bin_summary_path": summary_path if os.path.isfile(summary_path) else None,
749
- "recent_session_notes": recent_notes,
750
- "corrections_path": corrections_path(project_root) if os.path.isfile(corrections_path(project_root)) else None,
751
- "decisions_path": decisions_path(project_root) if os.path.isfile(decisions_path(project_root)) else None,
752
- # Shared state with control panel (V2 B6)
753
- "panel_state": read_panel_state(project_root),
754
- "panel_state_path": panel_state_path(project_root),
755
- }