@jaguilar87/gaia 5.0.0-rc1

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