@jaguilar87/gaia 5.0.0-rc.2

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 (621) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +26 -0
  3. package/ARCHITECTURE.md +335 -0
  4. package/CHANGELOG.md +1298 -0
  5. package/CODE_OF_CONDUCT.md +11 -0
  6. package/CONTRIBUTING.md +146 -0
  7. package/INSTALL.md +436 -0
  8. package/LICENSE +21 -0
  9. package/README.md +222 -0
  10. package/SECURITY.md +47 -0
  11. package/agents/README.md +78 -0
  12. package/agents/cloud-troubleshooter.md +73 -0
  13. package/agents/developer.md +65 -0
  14. package/agents/gaia-operator.md +64 -0
  15. package/agents/gaia-orchestrator.md +111 -0
  16. package/agents/gaia-planner.md +53 -0
  17. package/agents/gaia-system.md +71 -0
  18. package/agents/gitops-operator.md +61 -0
  19. package/agents/terraform-architect.md +63 -0
  20. package/bin/README.md +106 -0
  21. package/bin/cli/__init__.py +1 -0
  22. package/bin/cli/approvals.py +740 -0
  23. package/bin/cli/cleanup.py +562 -0
  24. package/bin/cli/context.py +283 -0
  25. package/bin/cli/doctor.py +651 -0
  26. package/bin/cli/history.py +305 -0
  27. package/bin/cli/memory.py +483 -0
  28. package/bin/cli/metrics.py +1068 -0
  29. package/bin/cli/plans.py +515 -0
  30. package/bin/cli/status.py +302 -0
  31. package/bin/cli/update.py +382 -0
  32. package/bin/gaia +112 -0
  33. package/bin/gaia-cleanup.js +531 -0
  34. package/bin/gaia-doctor.js +635 -0
  35. package/bin/gaia-evidence +126 -0
  36. package/bin/gaia-history.js +251 -0
  37. package/bin/gaia-metrics.js +1278 -0
  38. package/bin/gaia-review.js +269 -0
  39. package/bin/gaia-scan +44 -0
  40. package/bin/gaia-scan.py +589 -0
  41. package/bin/gaia-skills-diagnose.js +929 -0
  42. package/bin/gaia-status.js +278 -0
  43. package/bin/gaia-uninstall.js +111 -0
  44. package/bin/gaia-update.js +919 -0
  45. package/bin/pre-publish-validate.js +610 -0
  46. package/bin/python-detect.js +60 -0
  47. package/bin/validate-sandbox.sh +601 -0
  48. package/commands/README.md +64 -0
  49. package/commands/gaia.md +37 -0
  50. package/commands/scan-project.md +67 -0
  51. package/config/README.md +71 -0
  52. package/config/cloud/aws.json +134 -0
  53. package/config/cloud/gcp.json +139 -0
  54. package/config/context-contracts.json +158 -0
  55. package/config/crons-schema.md +81 -0
  56. package/config/git_standards.json +72 -0
  57. package/config/surface-routing.json +417 -0
  58. package/config/universal-rules.json +102 -0
  59. package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
  60. package/dist/gaia-ops/README.md +80 -0
  61. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  62. package/dist/gaia-ops/agents/developer.md +65 -0
  63. package/dist/gaia-ops/agents/gaia-operator.md +64 -0
  64. package/dist/gaia-ops/agents/gaia-orchestrator.md +111 -0
  65. package/dist/gaia-ops/agents/gaia-planner.md +53 -0
  66. package/dist/gaia-ops/agents/gaia-system.md +71 -0
  67. package/dist/gaia-ops/agents/gitops-operator.md +61 -0
  68. package/dist/gaia-ops/agents/terraform-architect.md +63 -0
  69. package/dist/gaia-ops/commands/gaia.md +37 -0
  70. package/dist/gaia-ops/config/README.md +71 -0
  71. package/dist/gaia-ops/config/cloud/aws.json +134 -0
  72. package/dist/gaia-ops/config/cloud/gcp.json +139 -0
  73. package/dist/gaia-ops/config/context-contracts.json +158 -0
  74. package/dist/gaia-ops/config/crons-schema.md +81 -0
  75. package/dist/gaia-ops/config/git_standards.json +72 -0
  76. package/dist/gaia-ops/config/surface-routing.json +417 -0
  77. package/dist/gaia-ops/config/universal-rules.json +102 -0
  78. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  79. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  80. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  81. package/dist/gaia-ops/hooks/adapters/claude_code.py +1890 -0
  82. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  83. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  84. package/dist/gaia-ops/hooks/hooks.json +192 -0
  85. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  86. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  87. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  88. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  89. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +120 -0
  90. package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -0
  91. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  92. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  93. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  94. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  95. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  96. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  97. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  98. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +611 -0
  99. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  100. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  101. package/dist/gaia-ops/hooks/modules/context/agentic_loop_detector.py +165 -0
  102. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  103. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
  104. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  105. package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
  106. package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -0
  107. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  108. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  109. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  110. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  111. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  112. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +577 -0
  113. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  114. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  115. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  116. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  117. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  118. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
  119. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  120. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -0
  121. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  122. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  123. package/dist/gaia-ops/hooks/modules/security/__init__.py +120 -0
  124. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  125. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  126. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +1638 -0
  127. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  128. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
  129. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
  130. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
  131. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
  132. package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
  133. package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
  134. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  135. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
  136. package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
  137. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  138. package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -0
  139. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  140. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  141. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +174 -0
  142. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  143. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +160 -0
  144. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  145. package/dist/gaia-ops/hooks/modules/session/session_registry.py +333 -0
  146. package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
  147. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
  148. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  149. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  150. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  151. package/dist/gaia-ops/hooks/modules/tools/stage_decomposer.py +315 -0
  152. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -0
  153. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  154. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  155. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  156. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  157. package/dist/gaia-ops/hooks/pre_compact.py +60 -0
  158. package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
  159. package/dist/gaia-ops/hooks/session_end_hook.py +77 -0
  160. package/dist/gaia-ops/hooks/session_start.py +81 -0
  161. package/dist/gaia-ops/hooks/stop_hook.py +70 -0
  162. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  163. package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
  164. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  165. package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
  166. package/dist/gaia-ops/settings.json +72 -0
  167. package/dist/gaia-ops/skills/README.md +158 -0
  168. package/dist/gaia-ops/skills/agent-creation/SKILL.md +87 -0
  169. package/dist/gaia-ops/skills/agent-creation/examples.md +170 -0
  170. package/dist/gaia-ops/skills/agent-creation/reference.md +191 -0
  171. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
  172. package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
  173. package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
  174. package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
  175. package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
  176. package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
  177. package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
  178. package/dist/gaia-ops/skills/brief-spec/SKILL.md +185 -0
  179. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  180. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  181. package/dist/gaia-ops/skills/context-updater/SKILL.md +87 -0
  182. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  183. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
  184. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  185. package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
  186. package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
  187. package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
  188. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
  189. package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
  190. package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
  191. package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
  192. package/dist/gaia-ops/skills/gaia-release/SKILL.md +85 -0
  193. package/dist/gaia-ops/skills/gaia-release/reference.md +92 -0
  194. package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
  195. package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
  196. package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
  197. package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
  198. package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
  199. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
  200. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  201. package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
  202. package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
  203. package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
  204. package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
  205. package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
  206. package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
  207. package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
  208. package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
  209. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
  210. package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
  211. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
  212. package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
  213. package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
  214. package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
  215. package/dist/gaia-ops/skills/reference.md +135 -0
  216. package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
  217. package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
  218. package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
  219. package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
  220. package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
  221. package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -0
  222. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  223. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  224. package/dist/gaia-ops/skills/session-reflection/SKILL.md +69 -0
  225. package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
  226. package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
  227. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
  228. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  229. package/dist/gaia-ops/tools/__init__.py +9 -0
  230. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
  231. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
  232. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -0
  233. package/dist/gaia-ops/tools/context/README.md +132 -0
  234. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  235. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  236. package/dist/gaia-ops/tools/context/context_provider.py +721 -0
  237. package/dist/gaia-ops/tools/context/context_section_reader.py +342 -0
  238. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  239. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  240. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  241. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  242. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  243. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  244. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  245. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  246. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  247. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  248. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  249. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  250. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  251. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  252. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  253. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  254. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  255. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +264 -0
  256. package/dist/gaia-ops/tools/memory/README.md +0 -0
  257. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  258. package/dist/gaia-ops/tools/memory/backfill_fts5.py +107 -0
  259. package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
  260. package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
  261. package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
  262. package/dist/gaia-ops/tools/memory/paths.py +102 -0
  263. package/dist/gaia-ops/tools/memory/scoring.py +193 -0
  264. package/dist/gaia-ops/tools/memory/search_store.py +375 -0
  265. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  266. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  267. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  268. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  269. package/dist/gaia-ops/tools/scan/config.py +247 -0
  270. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  271. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  272. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  273. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  274. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  275. package/dist/gaia-ops/tools/scan/scanners/environment.py +349 -0
  276. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  277. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  278. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  279. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  280. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  281. package/dist/gaia-ops/tools/scan/setup.py +686 -0
  282. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  283. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  284. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  285. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  286. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  287. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  288. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  289. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  290. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  291. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  292. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  293. package/dist/gaia-ops/tools/scan/verify.py +270 -0
  294. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  295. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  296. package/dist/gaia-ops/tools/validation/README.md +244 -0
  297. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  298. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  299. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  300. package/dist/gaia-security/.claude-plugin/plugin.json +24 -0
  301. package/dist/gaia-security/README.md +90 -0
  302. package/dist/gaia-security/config/universal-rules.json +102 -0
  303. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  304. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  305. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  306. package/dist/gaia-security/hooks/adapters/claude_code.py +1890 -0
  307. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  308. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  309. package/dist/gaia-security/hooks/hooks.json +113 -0
  310. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  311. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  312. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  313. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  314. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +120 -0
  315. package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -0
  316. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  317. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  318. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  319. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  320. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  321. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  322. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  323. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +611 -0
  324. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  325. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  326. package/dist/gaia-security/hooks/modules/context/agentic_loop_detector.py +165 -0
  327. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  328. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
  329. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  330. package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
  331. package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -0
  332. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  333. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  334. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  335. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  336. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  337. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +577 -0
  338. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  339. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  340. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  341. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  342. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  343. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
  344. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  345. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -0
  346. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  347. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  348. package/dist/gaia-security/hooks/modules/security/__init__.py +120 -0
  349. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  350. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  351. package/dist/gaia-security/hooks/modules/security/approval_grants.py +1638 -0
  352. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  353. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
  354. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
  355. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
  356. package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
  357. package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
  358. package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
  359. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  360. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
  361. package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
  362. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  363. package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -0
  364. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  365. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  366. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +174 -0
  367. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  368. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +160 -0
  369. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  370. package/dist/gaia-security/hooks/modules/session/session_registry.py +333 -0
  371. package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
  372. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
  373. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  374. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  375. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  376. package/dist/gaia-security/hooks/modules/tools/stage_decomposer.py +315 -0
  377. package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -0
  378. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  379. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  380. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  381. package/dist/gaia-security/hooks/pre_tool_use.py +413 -0
  382. package/dist/gaia-security/hooks/session_end_hook.py +77 -0
  383. package/dist/gaia-security/hooks/session_start.py +81 -0
  384. package/dist/gaia-security/hooks/stop_hook.py +70 -0
  385. package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
  386. package/dist/gaia-security/settings.json +58 -0
  387. package/git-hooks/commit-msg +41 -0
  388. package/hooks/README.md +100 -0
  389. package/hooks/adapters/__init__.py +52 -0
  390. package/hooks/adapters/base.py +219 -0
  391. package/hooks/adapters/channel.py +17 -0
  392. package/hooks/adapters/claude_code.py +1890 -0
  393. package/hooks/adapters/types.py +194 -0
  394. package/hooks/adapters/utils.py +25 -0
  395. package/hooks/elicitation_result.py +179 -0
  396. package/hooks/hooks.json +84 -0
  397. package/hooks/modules/README.md +189 -0
  398. package/hooks/modules/__init__.py +15 -0
  399. package/hooks/modules/agents/__init__.py +29 -0
  400. package/hooks/modules/agents/contract_validator.py +647 -0
  401. package/hooks/modules/agents/response_contract.py +496 -0
  402. package/hooks/modules/agents/skill_injection_verifier.py +120 -0
  403. package/hooks/modules/agents/state_tracker.py +267 -0
  404. package/hooks/modules/agents/task_info_builder.py +74 -0
  405. package/hooks/modules/agents/transcript_analyzer.py +458 -0
  406. package/hooks/modules/agents/transcript_reader.py +152 -0
  407. package/hooks/modules/audit/__init__.py +28 -0
  408. package/hooks/modules/audit/event_detector.py +168 -0
  409. package/hooks/modules/audit/logger.py +131 -0
  410. package/hooks/modules/audit/metrics.py +134 -0
  411. package/hooks/modules/audit/workflow_auditor.py +611 -0
  412. package/hooks/modules/audit/workflow_recorder.py +296 -0
  413. package/hooks/modules/context/__init__.py +11 -0
  414. package/hooks/modules/context/agentic_loop_detector.py +165 -0
  415. package/hooks/modules/context/anchor_tracker.py +317 -0
  416. package/hooks/modules/context/compact_context_builder.py +218 -0
  417. package/hooks/modules/context/context_freshness.py +145 -0
  418. package/hooks/modules/context/context_injector.py +558 -0
  419. package/hooks/modules/context/context_writer.py +530 -0
  420. package/hooks/modules/context/contracts_loader.py +161 -0
  421. package/hooks/modules/core/__init__.py +40 -0
  422. package/hooks/modules/core/hook_entry.py +78 -0
  423. package/hooks/modules/core/paths.py +160 -0
  424. package/hooks/modules/core/plugin_mode.py +149 -0
  425. package/hooks/modules/core/plugin_setup.py +577 -0
  426. package/hooks/modules/core/state.py +179 -0
  427. package/hooks/modules/core/stdin.py +24 -0
  428. package/hooks/modules/events/__init__.py +1 -0
  429. package/hooks/modules/events/event_writer.py +210 -0
  430. package/hooks/modules/evidence/__init__.py +34 -0
  431. package/hooks/modules/evidence/assertions.py +137 -0
  432. package/hooks/modules/evidence/index_writer.py +57 -0
  433. package/hooks/modules/evidence/loader.py +126 -0
  434. package/hooks/modules/evidence/runner.py +241 -0
  435. package/hooks/modules/memory/__init__.py +8 -0
  436. package/hooks/modules/memory/episode_writer.py +216 -0
  437. package/hooks/modules/orchestrator/__init__.py +1 -0
  438. package/hooks/modules/orchestrator/delegate_mode.py +122 -0
  439. package/hooks/modules/scanning/__init__.py +8 -0
  440. package/hooks/modules/scanning/scan_trigger.py +84 -0
  441. package/hooks/modules/security/__init__.py +120 -0
  442. package/hooks/modules/security/approval_cleanup.py +87 -0
  443. package/hooks/modules/security/approval_constants.py +23 -0
  444. package/hooks/modules/security/approval_grants.py +1638 -0
  445. package/hooks/modules/security/approval_messages.py +71 -0
  446. package/hooks/modules/security/approval_scopes.py +222 -0
  447. package/hooks/modules/security/blocked_commands.py +595 -0
  448. package/hooks/modules/security/blocked_message_formatter.py +87 -0
  449. package/hooks/modules/security/command_semantics.py +181 -0
  450. package/hooks/modules/security/composition_rules.py +547 -0
  451. package/hooks/modules/security/flag_classifiers.py +873 -0
  452. package/hooks/modules/security/gitops_validator.py +179 -0
  453. package/hooks/modules/security/mutative_verbs.py +1131 -0
  454. package/hooks/modules/security/network_hosts.py +481 -0
  455. package/hooks/modules/security/prompt_validator.py +40 -0
  456. package/hooks/modules/security/shell_unwrapper.py +165 -0
  457. package/hooks/modules/security/tiers.py +196 -0
  458. package/hooks/modules/session/__init__.py +10 -0
  459. package/hooks/modules/session/pending_scanner.py +174 -0
  460. package/hooks/modules/session/session_context_writer.py +100 -0
  461. package/hooks/modules/session/session_event_injector.py +160 -0
  462. package/hooks/modules/session/session_manager.py +31 -0
  463. package/hooks/modules/session/session_registry.py +333 -0
  464. package/hooks/modules/tools/__init__.py +29 -0
  465. package/hooks/modules/tools/bash_validator.py +1008 -0
  466. package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  467. package/hooks/modules/tools/hook_response.py +55 -0
  468. package/hooks/modules/tools/shell_parser.py +227 -0
  469. package/hooks/modules/tools/stage_decomposer.py +315 -0
  470. package/hooks/modules/tools/task_validator.py +294 -0
  471. package/hooks/modules/validation/__init__.py +23 -0
  472. package/hooks/modules/validation/commit_validator.py +380 -0
  473. package/hooks/post_compact.py +43 -0
  474. package/hooks/post_tool_use.py +54 -0
  475. package/hooks/pre_compact.py +60 -0
  476. package/hooks/pre_tool_use.py +413 -0
  477. package/hooks/session_end_hook.py +77 -0
  478. package/hooks/session_start.py +81 -0
  479. package/hooks/stop_hook.py +70 -0
  480. package/hooks/subagent_start.py +71 -0
  481. package/hooks/subagent_stop.py +295 -0
  482. package/hooks/task_completed.py +70 -0
  483. package/hooks/user_prompt_submit.py +246 -0
  484. package/index.js +83 -0
  485. package/package.json +103 -0
  486. package/pyproject.toml +32 -0
  487. package/skills/README.md +158 -0
  488. package/skills/agent-creation/SKILL.md +87 -0
  489. package/skills/agent-creation/examples.md +170 -0
  490. package/skills/agent-creation/reference.md +191 -0
  491. package/skills/agent-protocol/SKILL.md +93 -0
  492. package/skills/agent-protocol/examples.md +223 -0
  493. package/skills/agent-response/SKILL.md +69 -0
  494. package/skills/agentic-loop/SKILL.md +80 -0
  495. package/skills/agentic-loop/reference.md +378 -0
  496. package/skills/blog-writing/SKILL.md +98 -0
  497. package/skills/blog-writing/reference.md +130 -0
  498. package/skills/brief-spec/SKILL.md +185 -0
  499. package/skills/command-execution/SKILL.md +64 -0
  500. package/skills/command-execution/reference.md +83 -0
  501. package/skills/context-updater/SKILL.md +87 -0
  502. package/skills/context-updater/examples.md +71 -0
  503. package/skills/developer-patterns/SKILL.md +50 -0
  504. package/skills/developer-patterns/reference.md +112 -0
  505. package/skills/execution/SKILL.md +99 -0
  506. package/skills/fast-queries/SKILL.md +43 -0
  507. package/skills/gaia-compact/SKILL.md +74 -0
  508. package/skills/gaia-patterns/SKILL.md +108 -0
  509. package/skills/gaia-patterns/reference.md +395 -0
  510. package/skills/gaia-planner/SKILL.md +37 -0
  511. package/skills/gaia-planner/reference.md +107 -0
  512. package/skills/gaia-release/SKILL.md +85 -0
  513. package/skills/gaia-release/reference.md +92 -0
  514. package/skills/gaia-self-check/SKILL.md +114 -0
  515. package/skills/gaia-self-check/reference.md +453 -0
  516. package/skills/gaia-verify/SKILL.md +77 -0
  517. package/skills/gaia-verify/reference.md +80 -0
  518. package/skills/git-conventions/SKILL.md +47 -0
  519. package/skills/gitops-patterns/SKILL.md +60 -0
  520. package/skills/gitops-patterns/reference.md +183 -0
  521. package/skills/gmail-policy/SKILL.md +200 -0
  522. package/skills/gmail-policy/reference.md +150 -0
  523. package/skills/gmail-triage/SKILL.md +100 -0
  524. package/skills/gws-setup/SKILL.md +99 -0
  525. package/skills/gws-setup/reference.md +73 -0
  526. package/skills/investigation/SKILL.md +100 -0
  527. package/skills/memory-curation/SKILL.md +83 -0
  528. package/skills/memory-search/SKILL.md +88 -0
  529. package/skills/orchestrator-approval/SKILL.md +160 -0
  530. package/skills/orchestrator-approval/reference.md +174 -0
  531. package/skills/pending-approvals/SKILL.md +72 -0
  532. package/skills/pending-approvals/reference.md +214 -0
  533. package/skills/readme-writing/SKILL.md +71 -0
  534. package/skills/readme-writing/reference.md +188 -0
  535. package/skills/reference.md +135 -0
  536. package/skills/request-approval/SKILL.md +140 -0
  537. package/skills/request-approval/examples.md +140 -0
  538. package/skills/request-approval/reference.md +57 -0
  539. package/skills/schedule-task/SKILL.md +64 -0
  540. package/skills/schedule-task/reference.md +233 -0
  541. package/skills/security-tiers/SKILL.md +141 -0
  542. package/skills/security-tiers/destructive-commands-reference.md +623 -0
  543. package/skills/security-tiers/reference.md +39 -0
  544. package/skills/session-reflection/SKILL.md +69 -0
  545. package/skills/skill-creation/SKILL.md +92 -0
  546. package/skills/skill-creation/reference.md +29 -0
  547. package/skills/terraform-patterns/SKILL.md +89 -0
  548. package/skills/terraform-patterns/reference.md +93 -0
  549. package/templates/README.md +69 -0
  550. package/templates/managed-settings.template.json +43 -0
  551. package/tools/__init__.py +9 -0
  552. package/tools/agentic-loop/decide-status.py +210 -0
  553. package/tools/agentic-loop/parse-metric.py +106 -0
  554. package/tools/agentic-loop/record-iteration.py +221 -0
  555. package/tools/context/README.md +132 -0
  556. package/tools/context/__init__.py +42 -0
  557. package/tools/context/_paths.py +20 -0
  558. package/tools/context/context_provider.py +721 -0
  559. package/tools/context/context_section_reader.py +342 -0
  560. package/tools/context/deep_merge.py +159 -0
  561. package/tools/context/pending_updates.py +760 -0
  562. package/tools/context/surface_router.py +278 -0
  563. package/tools/fast-queries/README.md +65 -0
  564. package/tools/fast-queries/__init__.py +30 -0
  565. package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  566. package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  567. package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  568. package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  569. package/tools/fast-queries/run_triage.sh +59 -0
  570. package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  571. package/tools/gaia_simulator/__init__.py +33 -0
  572. package/tools/gaia_simulator/cli.py +354 -0
  573. package/tools/gaia_simulator/extractor.py +457 -0
  574. package/tools/gaia_simulator/reporter.py +258 -0
  575. package/tools/gaia_simulator/routing_simulator.py +334 -0
  576. package/tools/gaia_simulator/runner.py +539 -0
  577. package/tools/gaia_simulator/skills_mapper.py +264 -0
  578. package/tools/memory/README.md +0 -0
  579. package/tools/memory/__init__.py +20 -0
  580. package/tools/memory/backfill_fts5.py +107 -0
  581. package/tools/memory/conflict_detector.py +295 -0
  582. package/tools/memory/episodic.py +1210 -0
  583. package/tools/memory/git_invalidator.py +262 -0
  584. package/tools/memory/paths.py +102 -0
  585. package/tools/memory/scoring.py +193 -0
  586. package/tools/memory/search_store.py +375 -0
  587. package/tools/persist_transcript_analysis.py +85 -0
  588. package/tools/review/__init__.py +1 -0
  589. package/tools/review/review_engine.py +157 -0
  590. package/tools/scan/__init__.py +35 -0
  591. package/tools/scan/config.py +247 -0
  592. package/tools/scan/merge.py +212 -0
  593. package/tools/scan/orchestrator.py +549 -0
  594. package/tools/scan/registry.py +127 -0
  595. package/tools/scan/scanners/__init__.py +18 -0
  596. package/tools/scan/scanners/base.py +137 -0
  597. package/tools/scan/scanners/environment.py +349 -0
  598. package/tools/scan/scanners/git.py +570 -0
  599. package/tools/scan/scanners/infrastructure.py +875 -0
  600. package/tools/scan/scanners/orchestration.py +600 -0
  601. package/tools/scan/scanners/stack.py +1085 -0
  602. package/tools/scan/scanners/tools.py +260 -0
  603. package/tools/scan/setup.py +686 -0
  604. package/tools/scan/tests/__init__.py +1 -0
  605. package/tools/scan/tests/conftest.py +796 -0
  606. package/tools/scan/tests/test_environment.py +323 -0
  607. package/tools/scan/tests/test_git.py +419 -0
  608. package/tools/scan/tests/test_infrastructure.py +382 -0
  609. package/tools/scan/tests/test_integration.py +920 -0
  610. package/tools/scan/tests/test_merge.py +269 -0
  611. package/tools/scan/tests/test_orchestration.py +304 -0
  612. package/tools/scan/tests/test_stack.py +604 -0
  613. package/tools/scan/tests/test_tools.py +349 -0
  614. package/tools/scan/ui.py +624 -0
  615. package/tools/scan/verify.py +270 -0
  616. package/tools/scan/walk.py +118 -0
  617. package/tools/scan/workspace.py +85 -0
  618. package/tools/validation/README.md +244 -0
  619. package/tools/validation/__init__.py +17 -0
  620. package/tools/validation/approval_gate.py +321 -0
  621. package/tools/validation/validate_skills.py +189 -0
@@ -0,0 +1,919 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @jaguilar87/gaia - Update script
5
+ *
6
+ * Runs automatically on npm install/update (postinstall hook).
7
+ * Also available as: npx gaia-update
8
+ *
9
+ * Behavior:
10
+ * - First-time install (.claude/ doesn't exist):
11
+ * 1. Check Python 3 is available
12
+ * 2. Run gaia-scan --npm-postinstall to create .claude/, symlinks, settings, project-context
13
+ * 3. Create plugin-registry.json
14
+ * 4. Merge permissions into settings.local.json
15
+ * 5. Merge hooks into settings.local.json
16
+ * 6. Fall through to verification
17
+ * - Update (.claude/ exists):
18
+ * 1. Show version transition (previous → current)
19
+ * 2. settings.json: create only if missing (non-invasive, never overwrites)
20
+ * 3. Merge permissions, env vars, and agent key into settings.local.json (union, preserves user config)
21
+ * 4. Merge hooks from hooks.json into settings.local.json (npm mode requires this)
22
+ * 5. Symlinks: recreate if missing, fix broken ones
23
+ * 5. Verify: hooks, python, project-context, config files
24
+ * 6. Report: summary with any issues found
25
+ *
26
+ * Usage:
27
+ * npm update @jaguilar87/gaia # Automatic via postinstall
28
+ * npx gaia-update # Manual trigger
29
+ * npx gaia-update --verbose # Show all checks
30
+ */
31
+
32
+ import { fileURLToPath } from 'url';
33
+ import { dirname, join, relative, isAbsolute, resolve as resolvePath } from 'path';
34
+ import fs from 'fs/promises';
35
+ import { existsSync, realpathSync, readdirSync, readlinkSync } from 'fs';
36
+ import { exec } from 'child_process';
37
+ import { promisify } from 'util';
38
+ import chalk from 'chalk';
39
+ import ora from 'ora';
40
+ import { findPython } from './python-detect.js';
41
+
42
+ const execAsync = promisify(exec);
43
+ const __filename = fileURLToPath(import.meta.url);
44
+ const __dirname = dirname(__filename);
45
+ const CWD = process.env.INIT_CWD || process.cwd();
46
+ const VERBOSE = process.argv.includes('--verbose') || process.argv.includes('-v');
47
+
48
+ // Use junctions on Windows (no admin required), regular symlinks elsewhere
49
+ const LINK_TYPE = process.platform === 'win32' ? 'junction' : 'dir';
50
+
51
+ // ============================================================================
52
+ // Dynamic package resolution
53
+ // ============================================================================
54
+ //
55
+ // The gaia package was renamed from `@jaguilar87/gaia-ops` to `@jaguilar87/gaia`
56
+ // in v5. Hardcoding either name breaks the postinstall on the other variant.
57
+ // Resolve dynamically by scanning `node_modules/@jaguilar87/` for the first
58
+ // `gaia*` package installed in the consumer project.
59
+ //
60
+ // Returns the package name (e.g. `gaia` or `gaia-ops`) or `null` when the
61
+ // scope directory is missing / has no gaia package. Callers should fall back
62
+ // or report not-found rather than assume a default.
63
+
64
+ function resolveGaiaPackageName(cwd = CWD) {
65
+ const scopeDir = join(cwd, 'node_modules', '@jaguilar87');
66
+ if (!existsSync(scopeDir)) return null;
67
+ let entries;
68
+ try {
69
+ entries = readdirSync(scopeDir);
70
+ } catch {
71
+ return null;
72
+ }
73
+ // Prefer exact canonical name when present, else first gaia* match.
74
+ if (entries.includes('gaia')) return 'gaia';
75
+ const legacy = entries.find((name) => name.startsWith('gaia'));
76
+ return legacy || null;
77
+ }
78
+
79
+ // ============================================================================
80
+ // Version Detection
81
+ // ============================================================================
82
+
83
+ async function detectVersions() {
84
+ const current = await readPackageVersion(join(__dirname, '..', 'package.json'));
85
+
86
+ // Try to find previous version from the installed package.json backup or lock.
87
+ // Use the dynamically-resolved package name so both `@jaguilar87/gaia` (v5+)
88
+ // and the legacy `@jaguilar87/gaia-ops` are supported on upgrade paths.
89
+ let previous = null;
90
+ try {
91
+ const lockPath = join(CWD, 'package-lock.json');
92
+ if (existsSync(lockPath)) {
93
+ const lock = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
94
+ const pkgName = resolveGaiaPackageName() || 'gaia';
95
+ const dep = lock.packages?.[`node_modules/@jaguilar87/${pkgName}`]
96
+ || lock.dependencies?.[`@jaguilar87/${pkgName}`];
97
+ if (dep) previous = dep.version;
98
+ }
99
+ } catch { /* ignore */ }
100
+
101
+ return { previous, current };
102
+ }
103
+
104
+ async function readPackageVersion(path) {
105
+ try {
106
+ const pkg = JSON.parse(await fs.readFile(path, 'utf-8'));
107
+ return pkg.version;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ // ============================================================================
114
+ // Update Steps
115
+ // ============================================================================
116
+
117
+ async function updateSettingsJson() {
118
+ const spinner = ora('Checking settings.json...').start();
119
+ try {
120
+ const settingsPath = join(CWD, '.claude', 'settings.json');
121
+
122
+ if (!existsSync(join(CWD, '.claude'))) {
123
+ spinner.info('Skipped (.claude/ not found)');
124
+ return false;
125
+ }
126
+
127
+ // Non-invasive: only create if missing. Never overwrite.
128
+ // Hooks come from hooks.json (auto-discovered via symlink).
129
+ // Env vars and permissions live in settings.local.json.
130
+ if (existsSync(settingsPath)) {
131
+ spinner.succeed('settings.json already exists (not overwriting)');
132
+ return false;
133
+ }
134
+
135
+ await fs.writeFile(settingsPath, '{}\n');
136
+ spinner.succeed('settings.json created (minimal — hooks from hooks.json)');
137
+ return true;
138
+ } catch (error) {
139
+ spinner.fail(`settings.json: ${error.message}`);
140
+ return false;
141
+ }
142
+ }
143
+
144
+ async function updateLocalPermissions() {
145
+ const spinner = ora('Merging permissions into settings.local.json...').start();
146
+ try {
147
+ const claudeDir = join(CWD, '.claude');
148
+ const localPath = join(claudeDir, 'settings.local.json');
149
+
150
+ if (!existsSync(claudeDir)) {
151
+ spinner.info('Skipped (.claude/ not found)');
152
+ return false;
153
+ }
154
+
155
+ // Load existing settings.local.json — preserve everything (enabledPlugins, MCP servers, etc.)
156
+ let existing = {};
157
+ if (existsSync(localPath)) {
158
+ try {
159
+ existing = JSON.parse(await fs.readFile(localPath, 'utf-8'));
160
+ } catch {
161
+ existing = {};
162
+ }
163
+ }
164
+
165
+ // Track what changed
166
+ let changed = false;
167
+
168
+ // Set the orchestrator agent identity (always, even if Python extraction fails)
169
+ if (existing.agent !== 'gaia-orchestrator') {
170
+ existing.agent = 'gaia-orchestrator';
171
+ changed = true;
172
+ }
173
+
174
+ // Add env vars (smart merge: add if not present, don't overwrite)
175
+ existing.env = existing.env || {};
176
+ if (!('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' in existing.env)) {
177
+ existing.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
178
+ changed = true;
179
+ }
180
+
181
+ // Load permissions from plugin_setup.py — the single source of truth.
182
+ // We use ast.literal_eval to extract the constants without importing
183
+ // the module (which has relative imports that fail standalone).
184
+ let gaiaPerms;
185
+ try {
186
+ const setupPath = join(__dirname, '..', 'hooks', 'modules', 'core', 'plugin_setup.py');
187
+ const pythonCmd = findPython() || 'python3';
188
+
189
+ // Write the extraction script to a temp file instead of using -c with
190
+ // inline code. This avoids shell quoting issues on Windows where
191
+ // backslash paths and nested quotes break the inline Python string.
192
+ const tempScript = join(claudeDir, '.gaia-extract-perms.py');
193
+ const scriptContent = `
194
+ import ast, json, re, sys
195
+
196
+ setup_path = sys.argv[1]
197
+ source = open(setup_path, encoding="utf-8").read()
198
+
199
+ # Extract _DENY_RULES list
200
+ deny_match = re.search(r'^_DENY_RULES\\s*=\\s*\\[', source, re.MULTILINE)
201
+ if deny_match:
202
+ bracket_start = deny_match.start() + source[deny_match.start():].index('[')
203
+ depth, i = 0, bracket_start
204
+ for i, ch in enumerate(source[bracket_start:], bracket_start):
205
+ if ch == '[': depth += 1
206
+ elif ch == ']': depth -= 1
207
+ if depth == 0: break
208
+ deny_rules = ast.literal_eval(source[bracket_start:i+1])
209
+ else:
210
+ deny_rules = []
211
+
212
+ # Extract OPS_PERMISSIONS allow list
213
+ ops_match = re.search(r'^OPS_PERMISSIONS\\s*=', source, re.MULTILINE)
214
+ if ops_match:
215
+ bracket_start = source.index('{', ops_match.start())
216
+ depth, i = 0, bracket_start
217
+ for i, ch in enumerate(source[bracket_start:], bracket_start):
218
+ if ch == '{': depth += 1
219
+ elif ch == '}': depth -= 1
220
+ if depth == 0: break
221
+ # Replace _DENY_RULES reference with actual list for eval
222
+ ops_str = source[bracket_start:i+1].replace('_DENY_RULES', json.dumps(deny_rules))
223
+ ops_perms = ast.literal_eval(ops_str)
224
+ else:
225
+ ops_perms = {'permissions': {'allow': [], 'deny': deny_rules, 'ask': []}}
226
+
227
+ print(json.dumps(ops_perms))
228
+ `;
229
+ await fs.writeFile(tempScript, scriptContent);
230
+ try {
231
+ const { stdout } = await execAsync(
232
+ `${pythonCmd} "${tempScript}" "${setupPath}"`,
233
+ { timeout: 10000 }
234
+ );
235
+ gaiaPerms = JSON.parse(stdout.trim());
236
+ } finally {
237
+ // Clean up temp script
238
+ try { await fs.unlink(tempScript); } catch { /* ignore */ }
239
+ }
240
+ } catch (pyError) {
241
+ spinner.warn(`Could not load permissions from Python — ${pyError.message || 'unknown error'}`);
242
+ // Still write agent and env changes even if permissions extraction fails
243
+ if (changed) {
244
+ await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
245
+ spinner.succeed('settings.local.json agent and env merged (permissions skipped)');
246
+ return true;
247
+ }
248
+ return false;
249
+ }
250
+
251
+ const ourAllow = new Set(gaiaPerms.permissions.allow || []);
252
+ const ourDeny = new Set(gaiaPerms.permissions.deny || []);
253
+
254
+ const perms = existing.permissions || {};
255
+ const currentAllow = new Set(perms.allow || []);
256
+ const currentDeny = new Set(perms.deny || []);
257
+
258
+ // Union merge — add ours without removing user's
259
+ const mergedAllow = [...new Set([...currentAllow, ...ourAllow])].sort();
260
+ const mergedDeny = [...new Set([...currentDeny, ...ourDeny])].sort();
261
+
262
+ // Check if permissions changed
263
+ const allowChanged = mergedAllow.length !== currentAllow.size
264
+ || mergedAllow.some(r => !currentAllow.has(r));
265
+ const denyChanged = mergedDeny.length !== currentDeny.size
266
+ || mergedDeny.some(r => !currentDeny.has(r));
267
+
268
+ if (allowChanged || denyChanged) {
269
+ existing.permissions = existing.permissions || {};
270
+ existing.permissions.allow = mergedAllow;
271
+ existing.permissions.deny = mergedDeny;
272
+ existing.permissions.ask = existing.permissions.ask || [];
273
+ changed = true;
274
+ }
275
+
276
+ if (!changed) {
277
+ spinner.succeed('settings.local.json permissions already up to date');
278
+ return false;
279
+ }
280
+
281
+ await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
282
+ spinner.succeed('settings.local.json permissions, env, and agent merged');
283
+ return true;
284
+ } catch (error) {
285
+ spinner.fail(`settings.local.json: ${error.message}`);
286
+ return false;
287
+ }
288
+ }
289
+
290
+ async function updateLocalHooks() {
291
+ const spinner = ora('Merging hooks into settings.local.json...').start();
292
+ try {
293
+ const claudeDir = join(CWD, '.claude');
294
+ const localPath = join(claudeDir, 'settings.local.json');
295
+
296
+ if (!existsSync(claudeDir)) {
297
+ spinner.info('Skipped (.claude/ not found)');
298
+ return false;
299
+ }
300
+
301
+ // Read hooks.json from the installed package
302
+ const hooksJsonPath = join(__dirname, '..', 'hooks', 'hooks.json');
303
+ if (!existsSync(hooksJsonPath)) {
304
+ spinner.warn('hooks.json not found in package');
305
+ return false;
306
+ }
307
+
308
+ let hooksData;
309
+ try {
310
+ hooksData = JSON.parse(await fs.readFile(hooksJsonPath, 'utf-8'));
311
+ } catch {
312
+ spinner.warn('hooks.json is invalid JSON');
313
+ return false;
314
+ }
315
+
316
+ // Unwrap outer "hooks" key if present
317
+ const sourceHooks = hooksData.hooks || hooksData;
318
+
319
+ // Resolve absolute path to hooks directory so hooks work regardless of
320
+ // CWD at execution time (Stop/PostCompact hooks may run from unknown CWD)
321
+ const hooksSymlink = join(claudeDir, 'hooks');
322
+ let hooksAbs;
323
+ try {
324
+ hooksAbs = realpathSync(hooksSymlink);
325
+ } catch {
326
+ hooksAbs = hooksSymlink; // Fallback if symlink not yet created
327
+ }
328
+ const convertCommand = (cmd) => {
329
+ return cmd.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\//g, `${hooksAbs}/`);
330
+ };
331
+
332
+ const convertedHooks = {};
333
+ for (const [event, entries] of Object.entries(sourceHooks)) {
334
+ convertedHooks[event] = entries.map(entry => {
335
+ const converted = { ...entry };
336
+ if (converted.hooks) {
337
+ converted.hooks = converted.hooks.map(h => ({
338
+ ...h,
339
+ command: h.command ? convertCommand(h.command) : h.command,
340
+ }));
341
+ }
342
+ return converted;
343
+ });
344
+ }
345
+
346
+ // Load existing settings.local.json
347
+ let existing = {};
348
+ if (existsSync(localPath)) {
349
+ try {
350
+ existing = JSON.parse(await fs.readFile(localPath, 'utf-8'));
351
+ } catch {
352
+ existing = {};
353
+ }
354
+ }
355
+
356
+ // Migrate existing relative .claude/hooks/ paths to absolute
357
+ const existingHooks = existing.hooks || {};
358
+ let changed = false;
359
+
360
+ for (const [event, entries] of Object.entries(existingHooks)) {
361
+ for (const entry of entries) {
362
+ for (const h of (entry.hooks || [])) {
363
+ if (h.command && h.command.startsWith('.claude/hooks/')) {
364
+ h.command = h.command.replace('.claude/hooks/', `${hooksAbs}/`);
365
+ changed = true;
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ // Smart merge: for each hook event, deduplicate by command string
372
+ for (const [event, newEntries] of Object.entries(convertedHooks)) {
373
+ if (!existingHooks[event]) {
374
+ existingHooks[event] = newEntries;
375
+ changed = true;
376
+ continue;
377
+ }
378
+
379
+ // Collect existing command strings for deduplication
380
+ const existingCommands = new Set();
381
+ for (const entry of existingHooks[event]) {
382
+ for (const h of (entry.hooks || [])) {
383
+ if (h.command) existingCommands.add(h.command);
384
+ }
385
+ }
386
+
387
+ // Add new entries whose commands are not already present
388
+ for (const newEntry of newEntries) {
389
+ const newCommands = (newEntry.hooks || []).map(h => h.command).filter(Boolean);
390
+ const allPresent = newCommands.length > 0 && newCommands.every(c => existingCommands.has(c));
391
+ if (!allPresent) {
392
+ existingHooks[event].push(newEntry);
393
+ changed = true;
394
+ }
395
+ }
396
+ }
397
+
398
+ if (!changed) {
399
+ spinner.succeed('settings.local.json hooks already up to date');
400
+ return false;
401
+ }
402
+
403
+ existing.hooks = existingHooks;
404
+ await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
405
+ spinner.succeed('settings.local.json hooks merged');
406
+ return true;
407
+ } catch (error) {
408
+ spinner.fail(`hooks merge: ${error.message}`);
409
+ return false;
410
+ }
411
+ }
412
+
413
+ async function updateSymlinks() {
414
+ const spinner = ora('Checking symlinks...').start();
415
+ try {
416
+ const claudeDir = join(CWD, '.claude');
417
+ if (!existsSync(claudeDir)) {
418
+ spinner.info('Skipped (.claude/ not found)');
419
+ return { updated: false, fixed: 0, total: 0 };
420
+ }
421
+
422
+ // Resolve the installed package name dynamically so this works for both
423
+ // `@jaguilar87/gaia` (v5+) and the legacy `@jaguilar87/gaia-ops` when the
424
+ // consumer happens to have the old name on disk.
425
+ const pkgName = resolveGaiaPackageName();
426
+ if (!pkgName) {
427
+ spinner.fail('Package not found in node_modules/@jaguilar87/');
428
+ return { updated: false, fixed: 0, total: 0 };
429
+ }
430
+ const packagePath = join(CWD, 'node_modules', '@jaguilar87', pkgName);
431
+ if (!existsSync(packagePath)) {
432
+ spinner.fail(`Package not found at ${packagePath}`);
433
+ return { updated: false, fixed: 0, total: 0 };
434
+ }
435
+
436
+ const relativePath = relative(claudeDir, packagePath);
437
+ const symlinks = ['agents', 'tools', 'hooks', 'commands', 'templates', 'config', 'skills'];
438
+ let fixed = 0;
439
+
440
+ // Helpers: detect a stale symlink by reading the raw link target and
441
+ // checking whether the (absolute-resolved) target exists. Also detect
442
+ // symlinks that still point at the legacy `gaia-ops` path after the
443
+ // rename and repair them to the current package path.
444
+ const isStaleOrLegacy = (link) => {
445
+ let raw;
446
+ try {
447
+ raw = readlinkSync(link);
448
+ } catch {
449
+ return { stale: false, reason: null };
450
+ }
451
+ const absTarget = isAbsolute(raw) ? raw : resolvePath(dirname(link), raw);
452
+ if (!existsSync(absTarget)) {
453
+ return { stale: true, reason: `target missing: ${raw}` };
454
+ }
455
+ // Legacy name detection: if the installed package is `gaia` but the
456
+ // symlink still references `@jaguilar87/gaia-ops`, repair it.
457
+ if (pkgName === 'gaia' && raw.includes('@jaguilar87/gaia-ops')) {
458
+ return { stale: true, reason: `legacy target: ${raw}` };
459
+ }
460
+ return { stale: false, reason: null };
461
+ };
462
+
463
+ // Detect whether a path exists as a symlink (broken or not). Plain
464
+ // existsSync returns false for a broken symlink, so we need lstat to
465
+ // distinguish "no entry" from "entry whose target is missing".
466
+ const symlinkEntryExists = (p) => {
467
+ try {
468
+ return readlinkSync(p) !== undefined;
469
+ } catch {
470
+ return false;
471
+ }
472
+ };
473
+
474
+ for (const name of symlinks) {
475
+ const link = join(claudeDir, name);
476
+ // Junctions on Windows require absolute targets; symlinks on Unix use relative
477
+ const target = process.platform === 'win32'
478
+ ? join(packagePath, name)
479
+ : join(relativePath, name);
480
+
481
+ if (!existsSync(link) && !symlinkEntryExists(link)) {
482
+ try {
483
+ await fs.symlink(target, link, LINK_TYPE);
484
+ console.log(chalk.gray(`[gaia-update] Created symlink: ${link}`));
485
+ fixed++;
486
+ } catch { /* skip */ }
487
+ } else {
488
+ const { stale, reason } = isStaleOrLegacy(link);
489
+ if (stale) {
490
+ try {
491
+ await fs.unlink(link);
492
+ await fs.symlink(target, link, LINK_TYPE);
493
+ console.log(chalk.gray(`[gaia-update] Repaired stale symlink: ${link} (${reason})`));
494
+ fixed++;
495
+ } catch { /* skip */ }
496
+ }
497
+ }
498
+ }
499
+
500
+ // CHANGELOG.md — same stale-detection contract, but file-copy on Windows
501
+ const changelogLink = join(claudeDir, 'CHANGELOG.md');
502
+ const changelogSrc = join(packagePath, 'CHANGELOG.md');
503
+ if (!existsSync(changelogLink)) {
504
+ try {
505
+ if (process.platform === 'win32') {
506
+ // Junctions only work for directories; copy the file on Windows
507
+ await fs.copyFile(changelogSrc, changelogLink);
508
+ } else {
509
+ await fs.symlink(join(relativePath, 'CHANGELOG.md'), changelogLink);
510
+ }
511
+ console.log(chalk.gray(`[gaia-update] Created CHANGELOG link: ${changelogLink}`));
512
+ fixed++;
513
+ } catch { /* skip */ }
514
+ } else if (process.platform !== 'win32') {
515
+ const { stale, reason } = isStaleOrLegacy(changelogLink);
516
+ if (stale) {
517
+ try {
518
+ await fs.unlink(changelogLink);
519
+ await fs.symlink(join(relativePath, 'CHANGELOG.md'), changelogLink);
520
+ console.log(chalk.gray(`[gaia-update] Repaired stale CHANGELOG link (${reason})`));
521
+ fixed++;
522
+ } catch { /* skip */ }
523
+ }
524
+ }
525
+
526
+ const total = symlinks.length + 1;
527
+ if (fixed > 0) {
528
+ spinner.succeed(`Symlinks: fixed ${fixed}/${total}`);
529
+ } else {
530
+ spinner.succeed(`Symlinks: ${total}/${total} valid`);
531
+ }
532
+
533
+ return { updated: fixed > 0, fixed, total };
534
+ } catch (error) {
535
+ spinner.fail(`Symlinks: ${error.message}`);
536
+ return { updated: false, fixed: 0, total: 0 };
537
+ }
538
+ }
539
+
540
+ // ============================================================================
541
+ // FTS5 Backfill Safety-Net
542
+ // ============================================================================
543
+ //
544
+ // On upgrade paths, a project may already have episodes in
545
+ // `.claude/project-context/episodic-memory/index.json` but an empty FTS5
546
+ // `search.db`. This happens when episodes were produced before FTS5 was wired
547
+ // in, or when `search.db` was deleted for any reason. Fresh installs have
548
+ // zero episodes and will no-op through this check.
549
+ //
550
+ // Opt-out: pass `--no-fts5-backfill` (or set GAIA_SKIP_FTS5_BACKFILL=1).
551
+
552
+ async function maybeBackfillFts5() {
553
+ if (process.argv.includes('--no-fts5-backfill')
554
+ || process.env.GAIA_SKIP_FTS5_BACKFILL === '1') {
555
+ return;
556
+ }
557
+
558
+ const claudeDir = join(CWD, '.claude');
559
+ if (!existsSync(claudeDir)) return;
560
+
561
+ const memoryDir = join(claudeDir, 'project-context', 'episodic-memory');
562
+ const indexPath = join(memoryDir, 'index.json');
563
+ const dbPath = join(memoryDir, 'search.db');
564
+
565
+ if (!existsSync(indexPath)) return; // Fresh install — nothing to backfill
566
+
567
+ let total = 0;
568
+ try {
569
+ const idx = JSON.parse(await fs.readFile(indexPath, 'utf-8'));
570
+ total = Array.isArray(idx.episodes) ? idx.episodes.length : 0;
571
+ } catch {
572
+ return; // Unreadable index — let doctor handle it
573
+ }
574
+ if (total === 0) return; // No episodes to index
575
+
576
+ // If search.db doesn't exist yet but episodes do, fall through to the
577
+ // backfill step below. doctor --fix creates the db AND populates it.
578
+ // (Previously this returned early "doctor --fix will create it on first
579
+ // use", but nothing in the install flow runs doctor --fix automatically,
580
+ // so the index would stay empty until the user manually invoked it.)
581
+
582
+ // Query FTS5 count via python3 subprocess (sqlite3 binary may not be on PATH
583
+ // on Windows; python3 is already a hard requirement for gaia-ops hooks).
584
+ const pyCmd = findPython();
585
+ if (!pyCmd) return; // Python missing — the health check will report it
586
+
587
+ const spinner = ora('Checking FTS5 backfill status...').start();
588
+ let indexed = 0;
589
+ if (existsSync(dbPath)) {
590
+ try {
591
+ const probeScript = join(memoryDir, '.gaia-fts5-probe.py');
592
+ const probeContent = `
593
+ import sqlite3, sys
594
+ try:
595
+ con = sqlite3.connect(sys.argv[1])
596
+ cur = con.execute("SELECT COUNT(*) FROM episodes_fts")
597
+ print(cur.fetchone()[0])
598
+ except Exception:
599
+ print(-1)
600
+ `;
601
+ await fs.writeFile(probeScript, probeContent);
602
+ try {
603
+ const { stdout } = await execAsync(
604
+ `${pyCmd} "${probeScript}" "${dbPath}"`,
605
+ { timeout: 10000 }
606
+ );
607
+ indexed = parseInt(stdout.trim(), 10);
608
+ } finally {
609
+ try { await fs.unlink(probeScript); } catch { /* ignore */ }
610
+ }
611
+ } catch {
612
+ spinner.info('FTS5 probe skipped (sqlite3/python issue)');
613
+ return;
614
+ }
615
+
616
+ if (!Number.isFinite(indexed) || indexed < 0) {
617
+ // Table doesn't exist yet — treat as zero indexed, fall through to backfill.
618
+ indexed = 0;
619
+ }
620
+
621
+ // If indexed is already >=90% of total, no backfill needed — matches doctor
622
+ // threshold exactly so we don't loop users through unnecessary work.
623
+ if (indexed > 0 && indexed / total >= 0.9) {
624
+ spinner.succeed(`FTS5 backfill: ${indexed}/${total} episodes indexed (ok)`);
625
+ return;
626
+ }
627
+ }
628
+ // else: search.db missing entirely — doctor --fix will create + backfill.
629
+
630
+ spinner.text = existsSync(dbPath)
631
+ ? `FTS5 backfill: rebuilding ${total} episodes (had ${indexed})...`
632
+ : `FTS5 backfill: creating index for ${total} episodes...`;
633
+
634
+ // Invoke backfill via the gaia CLI doctor --fix. We call `python3 bin/gaia
635
+ // doctor --fix` from the installed package directory, with CWD set to the
636
+ // consumer project so doctor locates the right .claude/ tree.
637
+ const packageDir = join(__dirname, '..');
638
+ const gaiaEntry = join(packageDir, 'bin', 'gaia');
639
+
640
+ if (!existsSync(gaiaEntry)) {
641
+ spinner.warn('FTS5 backfill skipped (bin/gaia not found)');
642
+ return;
643
+ }
644
+
645
+ try {
646
+ const { stdout, stderr } = await execAsync(
647
+ `${pyCmd} "${gaiaEntry}" doctor --fix`,
648
+ { timeout: 120000, cwd: CWD }
649
+ );
650
+ if (VERBOSE) {
651
+ if (stdout) console.log(chalk.gray(stdout));
652
+ if (stderr) console.log(chalk.yellow(stderr));
653
+ }
654
+
655
+ // Re-probe to report outcome.
656
+ const probeScript = join(memoryDir, '.gaia-fts5-probe.py');
657
+ const probeContent = `
658
+ import sqlite3, sys
659
+ try:
660
+ con = sqlite3.connect(sys.argv[1])
661
+ cur = con.execute("SELECT COUNT(*) FROM episodes_fts")
662
+ print(cur.fetchone()[0])
663
+ except Exception:
664
+ print(-1)
665
+ `;
666
+ await fs.writeFile(probeScript, probeContent);
667
+ let newIndexed = -1;
668
+ try {
669
+ const { stdout: newOut } = await execAsync(
670
+ `${pyCmd} "${probeScript}" "${dbPath}"`,
671
+ { timeout: 10000 }
672
+ );
673
+ newIndexed = parseInt(newOut.trim(), 10);
674
+ } finally {
675
+ try { await fs.unlink(probeScript); } catch { /* ignore */ }
676
+ }
677
+
678
+ if (Number.isFinite(newIndexed) && newIndexed > indexed) {
679
+ spinner.succeed(`FTS5 backfill: rebuilt ${newIndexed}/${total} episodes`);
680
+ } else {
681
+ spinner.warn(`FTS5 backfill completed but index still at ${newIndexed}/${total}`);
682
+ }
683
+ } catch (err) {
684
+ spinner.warn(`FTS5 backfill skipped: ${err.message || 'unknown error'}`);
685
+ }
686
+ }
687
+
688
+ // ============================================================================
689
+ // Post-Update Verification
690
+ // ============================================================================
691
+
692
+ async function runVerification() {
693
+ const spinner = ora('Verifying installation health...').start();
694
+ const checks = [];
695
+ const issues = [];
696
+
697
+ // 1. Hooks exist and are reachable
698
+ const hookFiles = ['pre_tool_use.py', 'post_tool_use.py', 'subagent_stop.py'];
699
+ for (const hook of hookFiles) {
700
+ const path = join(CWD, '.claude', 'hooks', hook);
701
+ if (existsSync(path)) {
702
+ checks.push({ name: hook, ok: true });
703
+ } else {
704
+ checks.push({ name: hook, ok: false });
705
+ issues.push(`Hook missing: .claude/hooks/${hook}`);
706
+ }
707
+ }
708
+
709
+ // 2. Python available (try python3 first, fall back to python on Windows)
710
+ {
711
+ const pyCmd = findPython();
712
+ if (pyCmd) {
713
+ const { stdout } = await execAsync(`${pyCmd} --version`, { timeout: 5000 });
714
+ checks.push({ name: 'python3', ok: true, detail: stdout.trim() });
715
+ } else {
716
+ checks.push({ name: 'python3', ok: false });
717
+ issues.push('Python 3 not found (required for hooks)');
718
+ }
719
+ }
720
+
721
+ // 3. project-context.json exists and is valid
722
+ const ctxPath = join(CWD, '.claude', 'project-context', 'project-context.json');
723
+ if (existsSync(ctxPath)) {
724
+ try {
725
+ const ctx = JSON.parse(await fs.readFile(ctxPath, 'utf-8'));
726
+ const sections = Object.keys(ctx.sections || {}).length;
727
+ checks.push({ name: 'project-context.json', ok: sections >= 3, detail: `${sections} sections` });
728
+ if (sections < 3) issues.push('project-context.json has fewer than 3 sections');
729
+ } catch {
730
+ checks.push({ name: 'project-context.json', ok: false });
731
+ issues.push('project-context.json is invalid JSON');
732
+ }
733
+ } else {
734
+ checks.push({ name: 'project-context.json', ok: false });
735
+ issues.push('project-context.json not found (run gaia-scan)');
736
+ }
737
+
738
+ // 4. Config files accessible
739
+ const configFiles = ['git_standards.json', 'universal-rules.json', 'surface-routing.json'];
740
+ for (const cfg of configFiles) {
741
+ const path = join(CWD, '.claude', 'config', cfg);
742
+ if (existsSync(path)) {
743
+ checks.push({ name: cfg, ok: true });
744
+ } else {
745
+ checks.push({ name: cfg, ok: false });
746
+ if (VERBOSE) issues.push(`Config missing: .claude/config/${cfg}`);
747
+ }
748
+ }
749
+
750
+ // 5. Agent definitions accessible
751
+ const agentFiles = ['gaia-orchestrator.md', 'gaia-operator.md', 'terraform-architect.md', 'gitops-operator.md', 'cloud-troubleshooter.md', 'developer.md', 'gaia-system.md', 'gaia-planner.md'];
752
+ let agentsOk = 0;
753
+ for (const agent of agentFiles) {
754
+ if (existsSync(join(CWD, '.claude', 'agents', agent))) agentsOk++;
755
+ }
756
+ checks.push({ name: 'agent definitions', ok: agentsOk === agentFiles.length, detail: `${agentsOk}/${agentFiles.length}` });
757
+ if (agentsOk < agentFiles.length) issues.push(`${agentFiles.length - agentsOk} agent definition(s) missing`);
758
+
759
+ // 6. hooks.json exists (hooks are auto-discovered from hooks directory)
760
+ const hooksJsonPath = join(CWD, '.claude', 'hooks', 'hooks.json');
761
+ if (existsSync(hooksJsonPath)) {
762
+ try {
763
+ const hooksData = JSON.parse(await fs.readFile(hooksJsonPath, 'utf-8'));
764
+ const hasHooks = hooksData.hooks && Object.keys(hooksData.hooks).length > 0;
765
+ checks.push({ name: 'hooks.json', ok: hasHooks });
766
+ if (!hasHooks) issues.push('hooks.json has no hooks configured');
767
+ } catch {
768
+ checks.push({ name: 'hooks.json', ok: false });
769
+ issues.push('hooks.json is invalid');
770
+ }
771
+ } else {
772
+ checks.push({ name: 'hooks.json', ok: false });
773
+ issues.push('hooks.json not found (hooks symlink may be broken)');
774
+ }
775
+
776
+ const passed = checks.filter(c => c.ok).length;
777
+ const total = checks.length;
778
+
779
+ if (issues.length === 0) {
780
+ spinner.succeed(`Health check: ${passed}/${total} passed`);
781
+ } else {
782
+ spinner.warn(`Health check: ${passed}/${total} passed, ${issues.length} issue(s)`);
783
+ }
784
+
785
+ return { checks, issues, passed, total };
786
+ }
787
+
788
+ // ============================================================================
789
+ // Main
790
+ // ============================================================================
791
+
792
+ async function runFreshInstall() {
793
+ const packageDir = join(__dirname, '..');
794
+ const scanScript = join(packageDir, 'bin', 'gaia-scan.py');
795
+ const { current } = await detectVersions();
796
+
797
+ console.log(chalk.cyan(`\n gaia-ops ${chalk.green(current)} — fresh install\n`));
798
+
799
+ // 1. Check Python 3 is available (try python3, then python)
800
+ const spinner = ora('Checking Python 3...').start();
801
+ const pyCmd = findPython();
802
+ if (pyCmd) {
803
+ spinner.succeed(`Python 3 found (${pyCmd})`);
804
+ } else {
805
+ spinner.warn('Python 3 not found — skipping project setup');
806
+ console.log(chalk.gray(' Install Python 3.9+ and run: npx gaia-scan\n'));
807
+ return;
808
+ }
809
+
810
+ // 2. Run gaia-scan --npm-postinstall
811
+ const scanSpinner = ora('Running gaia-scan...').start();
812
+ try {
813
+ const { stdout, stderr } = await execAsync(
814
+ `${pyCmd} "${scanScript}" --npm-postinstall --root "${CWD}"`,
815
+ { timeout: 60000 }
816
+ );
817
+ scanSpinner.succeed('Project scanned and configured');
818
+ if (VERBOSE && stdout) console.log(chalk.gray(stdout));
819
+ if (VERBOSE && stderr) console.log(chalk.yellow(stderr));
820
+ } catch (error) {
821
+ scanSpinner.warn('gaia-scan encountered issues (non-fatal)');
822
+ if (VERBOSE && error.stderr) console.log(chalk.gray(error.stderr));
823
+ }
824
+
825
+ // 3. Create plugin-registry.json (in .claude/, same path Python hooks expect)
826
+ try {
827
+ const claudeDirPath = join(CWD, '.claude');
828
+ if (!existsSync(claudeDirPath)) {
829
+ await fs.mkdir(claudeDirPath, { recursive: true });
830
+ }
831
+ const registryPath = join(claudeDirPath, 'plugin-registry.json');
832
+ const registry = {
833
+ installed: [{ name: 'gaia-ops', version: current || 'unknown' }],
834
+ source: 'npm-postinstall',
835
+ };
836
+ await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
837
+ } catch {
838
+ // Non-fatal — plugin-registry is a convenience, not critical
839
+ }
840
+
841
+ // 4. Merge permissions into settings.local.json (same approach as plugin mode)
842
+ await updateLocalPermissions();
843
+
844
+ // 5. Merge hooks into settings.local.json (npm mode — Claude Code reads hooks from settings, not hooks.json)
845
+ await updateLocalHooks();
846
+ }
847
+
848
+ async function main() {
849
+ process.stderr.write('[DEPRECATED] gaia-update.js is deprecated. Use: python3 bin/gaia update\n[DEPRECATED] Migration guide: see CHANGELOG.md\n');
850
+
851
+ const claudeDir = join(CWD, '.claude');
852
+ const isUpdate = existsSync(claudeDir);
853
+
854
+ if (!isUpdate) {
855
+ // First-time install — run gaia-scan to bootstrap everything
856
+ await runFreshInstall();
857
+ } else {
858
+ // Version info
859
+ const { previous, current } = await detectVersions();
860
+ const versionLine = previous && previous !== current
861
+ ? `${chalk.gray(previous)} → ${chalk.green(current)}`
862
+ : chalk.green(current);
863
+
864
+ console.log(chalk.cyan(`\n gaia-ops update ${versionLine}\n`));
865
+
866
+ // Step 1-4: Update files
867
+ await updateSettingsJson();
868
+ await updateLocalPermissions();
869
+ await updateLocalHooks();
870
+ await updateSymlinks();
871
+ }
872
+
873
+ // Ensure plugin-registry.json exists in .claude/ (both fresh and update)
874
+ try {
875
+ const registryPath = join(CWD, '.claude', 'plugin-registry.json');
876
+ if (!existsSync(registryPath)) {
877
+ const { current } = await detectVersions();
878
+ const registry = {
879
+ installed: [{ name: 'gaia-ops', version: current || 'unknown' }],
880
+ source: 'npm-postinstall',
881
+ };
882
+ await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
883
+ }
884
+ } catch { /* non-fatal */ }
885
+
886
+ // FTS5 backfill safety-net (no-op on fresh install; only fires when
887
+ // episodes exist in index.json but search.db is under-indexed)
888
+ await maybeBackfillFts5();
889
+
890
+ // Verify (runs for both fresh install and update)
891
+ const { issues, passed, total } = await runVerification();
892
+
893
+ console.log('');
894
+ if (issues.length > 0) {
895
+ console.log(chalk.yellow(` ${issues.length} issue(s) found:`));
896
+ for (const issue of issues) {
897
+ console.log(chalk.yellow(` - ${issue}`));
898
+ }
899
+ } else {
900
+ console.log(chalk.green(' Everything up to date'));
901
+ }
902
+
903
+ console.log(chalk.gray(`\n Health: ${passed}/${total} checks passed\n`));
904
+ }
905
+
906
+ // Only execute main() when this file is invoked directly (not when imported
907
+ // for testing). This lets unit tests import internal helpers without
908
+ // triggering the postinstall side effects.
909
+ const _invokedDirectly = process.argv[1]
910
+ && fileURLToPath(import.meta.url) === resolvePath(process.argv[1]);
911
+
912
+ if (_invokedDirectly) {
913
+ main().catch(error => {
914
+ console.error(chalk.red(`\n Update failed: ${error.message}\n`));
915
+ process.exit(0); // Never fail npm install
916
+ });
917
+ }
918
+
919
+ export { resolveGaiaPackageName };