flyee 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/install.js +357 -0
  4. package/bridge/bridge.py +1780 -0
  5. package/bridge/local_tracker.py +722 -0
  6. package/core/agents/backend-specialist.md +266 -0
  7. package/core/agents/code-archaeologist.md +106 -0
  8. package/core/agents/database-architect.md +226 -0
  9. package/core/agents/debugger.md +225 -0
  10. package/core/agents/devops-engineer.md +323 -0
  11. package/core/agents/documentation-writer.md +104 -0
  12. package/core/agents/explorer-agent.md +73 -0
  13. package/core/agents/frontend-specialist.md +743 -0
  14. package/core/agents/game-developer.md +162 -0
  15. package/core/agents/mobile-developer.md +377 -0
  16. package/core/agents/orchestrator.md +416 -0
  17. package/core/agents/penetration-tester.md +188 -0
  18. package/core/agents/performance-optimizer.md +187 -0
  19. package/core/agents/product-manager.md +112 -0
  20. package/core/agents/product-owner.md +95 -0
  21. package/core/agents/project-planner.md +470 -0
  22. package/core/agents/qa-automation-engineer.md +103 -0
  23. package/core/agents/security-auditor.md +170 -0
  24. package/core/agents/seo-specialist.md +111 -0
  25. package/core/agents/stitch-designer.md +190 -0
  26. package/core/agents/tdd-reviewer.md +282 -0
  27. package/core/agents/test-engineer.md +158 -0
  28. package/core/scripts/auto_preview.py +148 -0
  29. package/core/scripts/checklist.py +243 -0
  30. package/core/scripts/cost_report.py +149 -0
  31. package/core/scripts/doc-sync-check.py +461 -0
  32. package/core/scripts/parse_user_stories.py +79 -0
  33. package/core/scripts/prepare_notion_updates.py +172 -0
  34. package/core/scripts/print_create_payload.py +18 -0
  35. package/core/scripts/session_manager.py +120 -0
  36. package/core/scripts/task_complete.py +127 -0
  37. package/core/scripts/verify_all.py +327 -0
  38. package/core/skills/analytics-strategy/SKILL.md +128 -0
  39. package/core/skills/api-patterns/SKILL.md +81 -0
  40. package/core/skills/api-patterns/api-style.md +42 -0
  41. package/core/skills/api-patterns/auth.md +24 -0
  42. package/core/skills/api-patterns/documentation.md +26 -0
  43. package/core/skills/api-patterns/graphql.md +41 -0
  44. package/core/skills/api-patterns/rate-limiting.md +31 -0
  45. package/core/skills/api-patterns/response.md +37 -0
  46. package/core/skills/api-patterns/rest.md +40 -0
  47. package/core/skills/api-patterns/scripts/api_validator.py +211 -0
  48. package/core/skills/api-patterns/security-testing.md +122 -0
  49. package/core/skills/api-patterns/trpc.md +41 -0
  50. package/core/skills/api-patterns/versioning.md +22 -0
  51. package/core/skills/app-builder/SKILL.md +75 -0
  52. package/core/skills/app-builder/agent-coordination.md +71 -0
  53. package/core/skills/app-builder/feature-building.md +53 -0
  54. package/core/skills/app-builder/project-detection.md +34 -0
  55. package/core/skills/app-builder/scaffolding.md +118 -0
  56. package/core/skills/app-builder/tech-stack.md +40 -0
  57. package/core/skills/app-builder/templates/SKILL.md +39 -0
  58. package/core/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
  59. package/core/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
  60. package/core/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  61. package/core/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
  62. package/core/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
  63. package/core/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
  64. package/core/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
  65. package/core/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +82 -0
  66. package/core/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +100 -0
  67. package/core/skills/app-builder/templates/nextjs-static/TEMPLATE.md +106 -0
  68. package/core/skills/app-builder/templates/nuxt-app/TEMPLATE.md +101 -0
  69. package/core/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
  70. package/core/skills/app-builder/templates/react-native-app/TEMPLATE.md +93 -0
  71. package/core/skills/architecture/SKILL.md +55 -0
  72. package/core/skills/architecture/context-discovery.md +43 -0
  73. package/core/skills/architecture/examples.md +94 -0
  74. package/core/skills/architecture/pattern-selection.md +68 -0
  75. package/core/skills/architecture/patterns-reference.md +50 -0
  76. package/core/skills/architecture/trade-off-analysis.md +77 -0
  77. package/core/skills/atomic-design/SKILL.md +282 -0
  78. package/core/skills/atomic-design/references/classification-guide.md +132 -0
  79. package/core/skills/atomic-design/references/quality-checklist.md +60 -0
  80. package/core/skills/atomic-design/references/stacks/stack-blade.md +254 -0
  81. package/core/skills/atomic-design/references/stacks/stack-nextjs.md +272 -0
  82. package/core/skills/atomic-design/references/stacks/stack-react.md +239 -0
  83. package/core/skills/atomic-design/references/stacks/stack-vue.md +224 -0
  84. package/core/skills/bash-linux/SKILL.md +199 -0
  85. package/core/skills/behavioral-modes/SKILL.md +242 -0
  86. package/core/skills/brainstorming/SKILL.md +163 -0
  87. package/core/skills/brainstorming/dynamic-questioning.md +373 -0
  88. package/core/skills/checkpointing-patterns/SKILL.md +163 -0
  89. package/core/skills/clean-code/SKILL.md +201 -0
  90. package/core/skills/code-review-checklist/SKILL.md +109 -0
  91. package/core/skills/code-truth-validation/SKILL.md +149 -0
  92. package/core/skills/component-library-discovery/SKILL.md +154 -0
  93. package/core/skills/content-strategy/SKILL.md +222 -0
  94. package/core/skills/context-budget/SKILL.md +155 -0
  95. package/core/skills/context-gathering-patterns/SKILL.md +278 -0
  96. package/core/skills/cost-tracking/SKILL.md +206 -0
  97. package/core/skills/database-design/SKILL.md +52 -0
  98. package/core/skills/database-design/database-selection.md +43 -0
  99. package/core/skills/database-design/indexing.md +39 -0
  100. package/core/skills/database-design/migrations.md +48 -0
  101. package/core/skills/database-design/optimization.md +36 -0
  102. package/core/skills/database-design/orm-selection.md +30 -0
  103. package/core/skills/database-design/schema-design.md +56 -0
  104. package/core/skills/database-design/scripts/schema_validator.py +172 -0
  105. package/core/skills/deployment-procedures/SKILL.md +295 -0
  106. package/core/skills/design-md/README.md +34 -0
  107. package/core/skills/design-md/SKILL.md +172 -0
  108. package/core/skills/design-md/examples/DESIGN.md +154 -0
  109. package/core/skills/design-system-enforcement/SKILL.md +339 -0
  110. package/core/skills/doc.md +177 -0
  111. package/core/skills/document-registry/SKILL.md +130 -0
  112. package/core/skills/documentation-publishing/SKILL.md +174 -0
  113. package/core/skills/documentation-templates/SKILL.md +194 -0
  114. package/core/skills/enhance-prompt/README.md +34 -0
  115. package/core/skills/enhance-prompt/SKILL.md +204 -0
  116. package/core/skills/enhance-prompt/references/KEYWORDS.md +114 -0
  117. package/core/skills/frontend-design/SKILL.md +430 -0
  118. package/core/skills/frontend-design/animation-guide.md +331 -0
  119. package/core/skills/frontend-design/color-system.md +311 -0
  120. package/core/skills/frontend-design/decision-trees.md +418 -0
  121. package/core/skills/frontend-design/motion-graphics.md +306 -0
  122. package/core/skills/frontend-design/scripts/accessibility_checker.py +183 -0
  123. package/core/skills/frontend-design/scripts/ux_audit.py +722 -0
  124. package/core/skills/frontend-design/typography-system.md +345 -0
  125. package/core/skills/frontend-design/ux-psychology.md +541 -0
  126. package/core/skills/frontend-design/visual-effects.md +383 -0
  127. package/core/skills/game-development/2d-games/SKILL.md +119 -0
  128. package/core/skills/game-development/3d-games/SKILL.md +135 -0
  129. package/core/skills/game-development/SKILL.md +167 -0
  130. package/core/skills/game-development/game-art/SKILL.md +185 -0
  131. package/core/skills/game-development/game-audio/SKILL.md +190 -0
  132. package/core/skills/game-development/game-design/SKILL.md +129 -0
  133. package/core/skills/game-development/mobile-games/SKILL.md +108 -0
  134. package/core/skills/game-development/multiplayer/SKILL.md +132 -0
  135. package/core/skills/game-development/pc-games/SKILL.md +144 -0
  136. package/core/skills/game-development/vr-ar/SKILL.md +123 -0
  137. package/core/skills/game-development/web-games/SKILL.md +150 -0
  138. package/core/skills/geo-fundamentals/SKILL.md +156 -0
  139. package/core/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
  140. package/core/skills/git-workflow/SKILL.md +263 -0
  141. package/core/skills/history-check-patterns/SKILL.md +125 -0
  142. package/core/skills/i18n-localization/SKILL.md +154 -0
  143. package/core/skills/i18n-localization/scripts/i18n_checker.py +241 -0
  144. package/core/skills/integration-completeness/SKILL.md +219 -0
  145. package/core/skills/intelligent-routing/SKILL.md +370 -0
  146. package/core/skills/lint-and-validate/SKILL.md +45 -0
  147. package/core/skills/lint-and-validate/scripts/lint_runner.py +173 -0
  148. package/core/skills/lint-and-validate/scripts/type_coverage.py +173 -0
  149. package/core/skills/local-verification/SKILL.md +195 -0
  150. package/core/skills/mcp-builder/SKILL.md +176 -0
  151. package/core/skills/mobile-design/SKILL.md +394 -0
  152. package/core/skills/mobile-design/decision-trees.md +516 -0
  153. package/core/skills/mobile-design/mobile-backend.md +491 -0
  154. package/core/skills/mobile-design/mobile-color-system.md +420 -0
  155. package/core/skills/mobile-design/mobile-debugging.md +122 -0
  156. package/core/skills/mobile-design/mobile-design-thinking.md +357 -0
  157. package/core/skills/mobile-design/mobile-navigation.md +458 -0
  158. package/core/skills/mobile-design/mobile-performance.md +767 -0
  159. package/core/skills/mobile-design/mobile-testing.md +356 -0
  160. package/core/skills/mobile-design/mobile-typography.md +433 -0
  161. package/core/skills/mobile-design/platform-android.md +666 -0
  162. package/core/skills/mobile-design/platform-ios.md +561 -0
  163. package/core/skills/mobile-design/scripts/mobile_audit.py +670 -0
  164. package/core/skills/mobile-design/touch-psychology.md +537 -0
  165. package/core/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +312 -0
  166. package/core/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
  167. package/core/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
  168. package/core/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
  169. package/core/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
  170. package/core/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
  171. package/core/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
  172. package/core/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
  173. package/core/skills/nextjs-react-expert/SKILL.md +267 -0
  174. package/core/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
  175. package/core/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -0
  176. package/core/skills/nodejs-best-practices/SKILL.md +333 -0
  177. package/core/skills/notion-task-patterns/SKILL.md +2529 -0
  178. package/core/skills/page-specifications/SKILL.md +367 -0
  179. package/core/skills/parallel-agents/SKILL.md +175 -0
  180. package/core/skills/performance-profiling/SKILL.md +143 -0
  181. package/core/skills/performance-profiling/scripts/lighthouse_audit.py +76 -0
  182. package/core/skills/plan-writing/SKILL.md +190 -0
  183. package/core/skills/powershell-windows/SKILL.md +167 -0
  184. package/core/skills/project-foundation/SKILL.md +117 -0
  185. package/core/skills/project-setup/SKILL.md +141 -0
  186. package/core/skills/project-tracking-patterns/SKILL.md +357 -0
  187. package/core/skills/project-type-discovery/SKILL.md +239 -0
  188. package/core/skills/python-patterns/SKILL.md +441 -0
  189. package/core/skills/qa-test-generation/SKILL.md +156 -0
  190. package/core/skills/react-components/README.md +36 -0
  191. package/core/skills/react-components/SKILL.md +47 -0
  192. package/core/skills/react-components/examples/gold-standard-card.tsx +80 -0
  193. package/core/skills/react-components/package-lock.json +231 -0
  194. package/core/skills/react-components/package.json +16 -0
  195. package/core/skills/react-components/resources/architecture-checklist.md +15 -0
  196. package/core/skills/react-components/resources/component-template.tsx +37 -0
  197. package/core/skills/react-components/resources/stitch-api-reference.md +14 -0
  198. package/core/skills/react-components/resources/style-guide.json +27 -0
  199. package/core/skills/react-components/scripts/fetch-stitch.sh +30 -0
  200. package/core/skills/react-components/scripts/validate.js +68 -0
  201. package/core/skills/red-team-tactics/SKILL.md +199 -0
  202. package/core/skills/remotion/README.md +105 -0
  203. package/core/skills/remotion/SKILL.md +393 -0
  204. package/core/skills/remotion/examples/WalkthroughComposition.tsx +78 -0
  205. package/core/skills/remotion/examples/screens.json +56 -0
  206. package/core/skills/remotion/resources/composition-checklist.md +124 -0
  207. package/core/skills/remotion/resources/screen-slide-template.tsx +123 -0
  208. package/core/skills/remotion/scripts/download-stitch-asset.sh +38 -0
  209. package/core/skills/seo-fundamentals/SKILL.md +129 -0
  210. package/core/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
  211. package/core/skills/server-management/SKILL.md +161 -0
  212. package/core/skills/session-resilience/SKILL.md +199 -0
  213. package/core/skills/shadcn-ui/README.md +248 -0
  214. package/core/skills/shadcn-ui/SKILL.md +326 -0
  215. package/core/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  216. package/core/skills/shadcn-ui/examples/data-table.tsx +313 -0
  217. package/core/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  218. package/core/skills/shadcn-ui/resources/component-catalog.md +481 -0
  219. package/core/skills/shadcn-ui/resources/customization-guide.md +516 -0
  220. package/core/skills/shadcn-ui/resources/migration-guide.md +463 -0
  221. package/core/skills/shadcn-ui/resources/setup-guide.md +412 -0
  222. package/core/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  223. package/core/skills/state-machine/SKILL.md +264 -0
  224. package/core/skills/stitch-loop/README.md +54 -0
  225. package/core/skills/stitch-loop/SKILL.md +203 -0
  226. package/core/skills/stitch-loop/examples/SITE.md +73 -0
  227. package/core/skills/stitch-loop/examples/next-prompt.md +25 -0
  228. package/core/skills/stitch-loop/resources/baton-schema.md +61 -0
  229. package/core/skills/stitch-loop/resources/site-template.md +104 -0
  230. package/core/skills/systematic-debugging/SKILL.md +109 -0
  231. package/core/skills/tailwind-patterns/SKILL.md +284 -0
  232. package/core/skills/tdd-validation/SKILL.md +243 -0
  233. package/core/skills/tdd-workflow/SKILL.md +284 -0
  234. package/core/skills/testing-patterns/SKILL.md +196 -0
  235. package/core/skills/testing-patterns/scripts/test_runner.py +219 -0
  236. package/core/skills/ui-ux-discovery/SKILL.md +329 -0
  237. package/core/skills/ui-validation/SKILL.md +190 -0
  238. package/core/skills/ui-validation/scripts/ui_antipattern_check.py +317 -0
  239. package/core/skills/verification-gate/SKILL.md +205 -0
  240. package/core/skills/vulnerability-scanner/SKILL.md +276 -0
  241. package/core/skills/vulnerability-scanner/checklists.md +121 -0
  242. package/core/skills/vulnerability-scanner/scripts/security_scan.py +458 -0
  243. package/core/skills/web-design-guidelines/SKILL.md +57 -0
  244. package/core/skills/webapp-testing/SKILL.md +187 -0
  245. package/core/skills/webapp-testing/scripts/playwright_runner.py +173 -0
  246. package/core/templates/ARCHITECTURE.template.md +407 -0
  247. package/core/templates/project-resources.example.json +71 -0
  248. package/core/workflows/atomic.md +182 -0
  249. package/core/workflows/brainstorm.md +134 -0
  250. package/core/workflows/check-task.md +242 -0
  251. package/core/workflows/copy-collect.md +306 -0
  252. package/core/workflows/create-agent.md +33 -0
  253. package/core/workflows/create-skill.md +39 -0
  254. package/core/workflows/create-workflow.md +33 -0
  255. package/core/workflows/create.md +92 -0
  256. package/core/workflows/debug.md +186 -0
  257. package/core/workflows/demand.md +443 -0
  258. package/core/workflows/deploy.md +260 -0
  259. package/core/workflows/discovery.md +267 -0
  260. package/core/workflows/document.md +272 -0
  261. package/core/workflows/ds-components.md +296 -0
  262. package/core/workflows/ds-init.md +58 -0
  263. package/core/workflows/ds-refactor.md +245 -0
  264. package/core/workflows/ds-references.md +197 -0
  265. package/core/workflows/ds-styleguide.md +237 -0
  266. package/core/workflows/ds-token-diff.md +103 -0
  267. package/core/workflows/ds-tokens.md +317 -0
  268. package/core/workflows/ds-validate.md +309 -0
  269. package/core/workflows/execute.md +483 -0
  270. package/core/workflows/extract-template.md +278 -0
  271. package/core/workflows/fix-failed-tests.md +160 -0
  272. package/core/workflows/init-project.md +386 -0
  273. package/core/workflows/legacy-project.md +849 -0
  274. package/core/workflows/log.md +97 -0
  275. package/core/workflows/new-project.md +610 -0
  276. package/core/workflows/new-project.md.bak +3292 -0
  277. package/core/workflows/new-task.md +404 -0
  278. package/core/workflows/orchestrate.md +237 -0
  279. package/core/workflows/page-build.md +296 -0
  280. package/core/workflows/plan.md +89 -0
  281. package/core/workflows/prd.md +255 -0
  282. package/core/workflows/preview.md +81 -0
  283. package/core/workflows/review-page.md +304 -0
  284. package/core/workflows/status.md +86 -0
  285. package/core/workflows/stitch.md +226 -0
  286. package/core/workflows/task-complete.md +473 -0
  287. package/core/workflows/task-update.md +163 -0
  288. package/core/workflows/tdd.md +344 -0
  289. package/core/workflows/test.md +251 -0
  290. package/core/workflows/ui-ux-pro-max.md +437 -0
  291. package/core/workflows/ux-mobile-optimize.md +262 -0
  292. package/core/workflows/ux-mobile-validate.md +297 -0
  293. package/engine-files/GEMINI.md +69 -0
  294. package/package.json +47 -0
  295. package/runtime-adapters/antigravity.js +26 -0
  296. package/runtime-adapters/claude.js +57 -0
  297. package/runtime-adapters/codex.js +51 -0
  298. package/runtime-adapters/copilot.js +51 -0
  299. package/runtime-adapters/cursor.js +51 -0
  300. package/runtime-adapters/gemini-cli.js +30 -0
  301. package/runtime-adapters/opencode.js +51 -0
  302. package/runtime-adapters/windsurf.js +51 -0
@@ -0,0 +1,1780 @@
1
+ #!/usr/bin/env python3
2
+ """flyee-bridge β€” Connects the .agent runtime to the Flyee Platform via structured events.
3
+
4
+ Usage:
5
+ # Test connectivity
6
+ python bridge.py --test
7
+
8
+ # Emit an event
9
+ python bridge.py emit "dev.task_completed" '{"task": "T1.1", "files": ["sdk.ts"]}'
10
+
11
+ # Configure interactively (with project creation + doc registration)
12
+ python bridge.py --setup
13
+
14
+ # List projects on the platform
15
+ python bridge.py --list-projects
16
+
17
+ # Scan and register local docs
18
+ python bridge.py --register-docs
19
+
20
+ # Persist a plan/document (auto-creates or appends version)
21
+ python bridge.py --persist-plan ./docs/plan.md --title "Sprint 16" --task-id "uuid"
22
+ """
23
+
24
+ import glob
25
+ import hashlib
26
+ import json
27
+ import os
28
+ import re
29
+ import sys
30
+ import time
31
+ from datetime import datetime, timezone
32
+ from pathlib import Path
33
+ from typing import Any, Optional, Union
34
+
35
+ # Resolve config path relative to project root
36
+ BRIDGE_DIR = Path(__file__).parent
37
+ PROJECT_ROOT = BRIDGE_DIR.parent.parent
38
+ CONFIG_PATH = PROJECT_ROOT / "flyee.json"
39
+ FALLBACK_PATH = BRIDGE_DIR / "events.jsonl"
40
+
41
+ PROD_API_URL = "https://flyee-api.flyeelab.com"
42
+
43
+ DEFAULT_CONFIG = {
44
+ "api_url": PROD_API_URL,
45
+ "project_id": "",
46
+ "api_key": "",
47
+ "enabled": False,
48
+ "opted_out": False,
49
+ "fallback_file": str(FALLBACK_PATH),
50
+ }
51
+
52
+
53
+ def load_config() -> dict:
54
+ """Load bridge config, creating default if absent."""
55
+ if not CONFIG_PATH.exists():
56
+ save_config(DEFAULT_CONFIG)
57
+ return DEFAULT_CONFIG.copy()
58
+ with open(CONFIG_PATH) as f:
59
+ return json.load(f)
60
+
61
+
62
+ def save_config(config: dict) -> None:
63
+ """Persist config to disk."""
64
+ with open(CONFIG_PATH, "w") as f:
65
+ json.dump(config, f, indent=2)
66
+
67
+
68
+ def is_configured(config: dict) -> bool:
69
+ """Check if bridge is properly configured for event emission."""
70
+ if config.get("opted_out"):
71
+ return False
72
+ return bool(
73
+ config.get("enabled")
74
+ and config.get("api_key")
75
+ and config.get("project_id")
76
+ )
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # API Helpers β€” Project & Document management
81
+ # ---------------------------------------------------------------------------
82
+
83
+ def api_request(
84
+ method: str,
85
+ url: str,
86
+ api_key: str,
87
+ data: Optional[dict] = None,
88
+ timeout: int = 30,
89
+ ) -> Any:
90
+ """Make an authenticated HTTP request to the Flyee API. Returns parsed JSON or None."""
91
+ import urllib.request
92
+ import urllib.error
93
+
94
+ headers = {
95
+ "Content-Type": "application/json",
96
+ "X-API-Key": api_key,
97
+ "X-Bridge-API-Key": api_key,
98
+ }
99
+ body = json.dumps(data).encode("utf-8") if data else None
100
+ req = urllib.request.Request(url, data=body, headers=headers, method=method)
101
+ try:
102
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
103
+ return json.loads(resp.read().decode("utf-8"))
104
+ except urllib.error.HTTPError as e:
105
+ detail = ""
106
+ try:
107
+ detail = e.read().decode("utf-8", errors="replace")[:500]
108
+ except Exception:
109
+ pass
110
+ print(f"❌ API error {e.code}: {e.reason}")
111
+ if detail:
112
+ print(f" Detail: {detail}")
113
+ return None
114
+ except (ConnectionResetError, OSError) as e:
115
+ # Server may have processed the request but dropped the connection
116
+ # (common with Temporal workflow start). Treat as likely success for mutations.
117
+ if method in ("POST", "PUT", "PATCH"):
118
+ print(f"⚠️ Connection reset after {method} β€” server likely processed the request.")
119
+ print(f" Detail: {e}")
120
+ return {"_connection_reset": True, "status": "likely_created"}
121
+ print(f"❌ Connection error: {e}")
122
+ return None
123
+ except Exception as e:
124
+ print(f"❌ Connection error: {e}")
125
+ return None
126
+
127
+
128
+ def list_projects(api_url: str, api_key: str) -> Optional[list]:
129
+ """List all projects on the Flyee Platform."""
130
+ url = f"{api_url.rstrip('/')}/flyee/projects/"
131
+ return api_request("GET", url, api_key)
132
+
133
+
134
+ def create_project(
135
+ api_url: str, api_key: str, name: str, description: str = ""
136
+ ) -> Optional[dict]:
137
+ """Create a new project on the Flyee Platform. Returns project dict with 'id'."""
138
+ url = f"{api_url.rstrip('/')}/flyee/projects/"
139
+ return api_request("POST", url, api_key, {
140
+ "name": name,
141
+ "description": description or f"Project created via flyee-bridge on {datetime.now().strftime('%Y-%m-%d')}",
142
+ "status": "active",
143
+ })
144
+
145
+
146
+ def _detect_doc_type(filepath: str) -> tuple:
147
+ """Detect document type from filepath. Returns (doc_type, title)."""
148
+ name = os.path.basename(filepath)
149
+ name_no_ext = os.path.splitext(name)[0]
150
+
151
+ if re.match(r"PRD-", name, re.IGNORECASE):
152
+ return "prd", name_no_ext.replace("PRD-", "PRD β€” ")
153
+ if re.match(r"SDD-", name, re.IGNORECASE):
154
+ return "sdd", name_no_ext.replace("SDD-", "SDD β€” ")
155
+ if "BREAKDOWN" in name.upper():
156
+ return "other", "Task Breakdown"
157
+ if "PROJECT-PROGRESS" in name.upper():
158
+ return "other", "Project Progress"
159
+ if "OKR" in name.upper():
160
+ return "okr", name_no_ext
161
+ return "other", name_no_ext
162
+
163
+
164
+ def scan_docs(project_root: Optional[str] = None) -> list:
165
+ """Scan docs/ for known document files. Returns list of {path, type, title}."""
166
+ if project_root is None:
167
+ # Walk up from bridge dir to find project root
168
+ project_root = str(BRIDGE_DIR.parent.parent)
169
+
170
+ docs_dir = os.path.join(project_root, "docs")
171
+ if not os.path.isdir(docs_dir):
172
+ return []
173
+
174
+ patterns = [
175
+ os.path.join(docs_dir, "PRD-*.md"),
176
+ os.path.join(docs_dir, "design", "SDD-*.md"),
177
+ os.path.join(docs_dir, "BREAKDOWN-*.md"),
178
+ os.path.join(docs_dir, "PROJECT-PROGRESS.md"),
179
+ ]
180
+
181
+ found = []
182
+ for pattern in patterns:
183
+ for filepath in glob.glob(pattern):
184
+ doc_type, title = _detect_doc_type(filepath)
185
+ found.append({"path": filepath, "type": doc_type, "title": title})
186
+
187
+ return found
188
+
189
+
190
+ def register_documents(
191
+ api_url: str, api_key: str, project_id: str, docs: list
192
+ ) -> list:
193
+ """Register local documents on the Flyee Platform. Returns list of results."""
194
+ MAX_CONTENT_SIZE = 500_000 # 500KB max per document
195
+ results = []
196
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/documents"
197
+
198
+ for doc in docs:
199
+ try:
200
+ with open(doc["path"], "r", encoding="utf-8") as f:
201
+ content = f.read()
202
+ except Exception as e:
203
+ results.append({**doc, "status": "error", "error": str(e)})
204
+ continue
205
+
206
+ if len(content) > MAX_CONTENT_SIZE:
207
+ content = content[:MAX_CONTENT_SIZE] + "\n\n[... truncated at 500KB ...]"
208
+
209
+ resp = api_request("POST", url, api_key, {
210
+ "title": doc["title"],
211
+ "type": doc["type"],
212
+ "content": content,
213
+ }, timeout=30)
214
+
215
+ if resp:
216
+ results.append({**doc, "status": "registered", "id": resp.get("id")})
217
+ else:
218
+ results.append({**doc, "status": "failed"})
219
+
220
+ return results
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Task Management β€” Create, update, list, get tasks on Flyee
225
+ # ---------------------------------------------------------------------------
226
+
227
+ def create_task(
228
+ api_url: str,
229
+ api_key: str,
230
+ project_id: str,
231
+ task_type: str = "implement_feature",
232
+ name: str = "",
233
+ description: str = "",
234
+ priority: str = "normal",
235
+ source: str = "system",
236
+ parent_task_id: Optional[str] = None,
237
+ meta: Optional[dict] = None,
238
+ is_backlog: bool = False,
239
+ ) -> Any:
240
+
241
+ """Create a task on the Flyee Platform.
242
+
243
+ Args:
244
+ task_type: One of create_prd, create_tdd, breakdown_tasks,
245
+ implement_feature, run_tests, generate_docs,
246
+ document_requirements, document_architecture,
247
+ design_system, implement_tests, verify_quality, generic
248
+ name: Human-readable task name (stored in input.name)
249
+ description: Task description (stored in input.description)
250
+ priority: One of low, normal, high, critical
251
+ source: One of api, slack, ui, system
252
+ meta: Additional metadata dict
253
+ """
254
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/tasks"
255
+ payload = {
256
+ "type": task_type,
257
+ "priority": priority,
258
+ "source": source,
259
+ "input": {
260
+ "name": name,
261
+ "description": description,
262
+ },
263
+ "meta": meta or {},
264
+ "max_retries": 0,
265
+ "timeout_seconds": 3600,
266
+ }
267
+ if is_backlog:
268
+ payload["is_backlog"] = True
269
+
270
+ if parent_task_id:
271
+ payload["parent_task_id"] = parent_task_id
272
+ return api_request("POST", url, api_key, payload)
273
+
274
+
275
+ def update_task(
276
+ api_url: str,
277
+ api_key: str,
278
+ task_id: str,
279
+ status: Optional[str] = None,
280
+ result_status: Optional[str] = None,
281
+ output: Optional[dict] = None,
282
+ metrics: Optional[dict] = None,
283
+ meta: Optional[dict] = None,
284
+ ) -> Any:
285
+ """Update a task on the Flyee Platform.
286
+
287
+ Args:
288
+ status: One of pending, queued, running, completed, failed, cancelled
289
+ result_status: One of success, partial, failed, error
290
+ output: Task output data (summary, files changed, etc.)
291
+ metrics: Execution metrics (time_spent, etc.)
292
+ meta: Additional metadata updates
293
+ """
294
+ url = f"{api_url.rstrip('/')}/flyee/tasks/{task_id}"
295
+ payload: dict = {}
296
+ if status:
297
+ payload["status"] = status
298
+ if result_status:
299
+ payload["result_status"] = result_status
300
+ if output:
301
+ payload["output"] = output
302
+ if metrics:
303
+ payload["metrics"] = metrics
304
+ if meta:
305
+ payload["meta"] = meta
306
+ return api_request("PUT", url, api_key, payload)
307
+
308
+
309
+ def list_tasks(
310
+ api_url: str,
311
+ api_key: str,
312
+ project_id: str,
313
+ status: Optional[str] = None,
314
+ ) -> Any:
315
+ """List tasks for a project on the Flyee Platform."""
316
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/tasks"
317
+ if status:
318
+ url += f"?status={status}"
319
+ return api_request("GET", url, api_key)
320
+
321
+
322
+ def get_task(api_url: str, api_key: str, task_id: str) -> Any:
323
+ """Get a single task by ID from the Flyee Platform."""
324
+ url = f"{api_url.rstrip('/')}/flyee/tasks/{task_id}"
325
+ return api_request("GET", url, api_key)
326
+
327
+
328
+ def create_okr(
329
+ api_url: str,
330
+ api_key: str,
331
+ project_id: str,
332
+ objective: str,
333
+ key_results: Optional[dict] = None,
334
+ period: Optional[str] = None,
335
+ owner: Optional[str] = None,
336
+ status: str = "active",
337
+ ) -> Any:
338
+ """Create an OKR on the Flyee Platform.
339
+
340
+ Args:
341
+ objective: The objective statement (e.g. 'Launch MVP by Q2 2026')
342
+ key_results: Dict of key results (e.g. {'kr1': '100 beta users', 'kr2': 'NPS > 40'})
343
+ period: Time period (e.g. 'Q1 2026')
344
+ owner: OKR owner name
345
+ status: One of draft, active, completed, cancelled
346
+ """
347
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/okrs"
348
+ payload = {
349
+ "objective": objective,
350
+ "status": status,
351
+ }
352
+ if key_results:
353
+ payload["key_results"] = key_results
354
+ if period:
355
+ payload["period"] = period
356
+ if owner:
357
+ payload["owner"] = owner
358
+ return api_request("POST", url, api_key, payload)
359
+
360
+
361
+ def list_okrs(
362
+ api_url: str,
363
+ api_key: str,
364
+ project_id: str,
365
+ ) -> Any:
366
+ """List OKRs for a project on the Flyee Platform."""
367
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/okrs"
368
+ return api_request("GET", url, api_key)
369
+
370
+
371
+ def create_decision(
372
+ api_url: str,
373
+ api_key: str,
374
+ project_id: str,
375
+ decision: str,
376
+ actor: str = "agent",
377
+ reason: Optional[str] = None,
378
+ impact: Optional[str] = None,
379
+ task_id: Optional[str] = None,
380
+ ) -> Any:
381
+ """Record a governance decision on the Flyee Platform.
382
+
383
+ Args:
384
+ decision: The decision taken (e.g. 'Use Next.js App Router')
385
+ actor: Who made it (e.g. 'agent', 'user', 'system')
386
+ reason: Rationale for the decision
387
+ impact: Expected impact of the decision
388
+ task_id: Related task ID, if any
389
+ """
390
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/decisions"
391
+ payload: dict = {
392
+ "actor": actor,
393
+ "decision": decision,
394
+ }
395
+ if reason:
396
+ payload["reason"] = reason
397
+ if impact:
398
+ payload["impact"] = impact
399
+ if task_id:
400
+ payload["task_id"] = task_id
401
+ return api_request("POST", url, api_key, payload)
402
+
403
+
404
+ def list_decisions(
405
+ api_url: str,
406
+ api_key: str,
407
+ project_id: str,
408
+ ) -> Any:
409
+ """List decisions for a project on the Flyee Platform."""
410
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/decisions"
411
+ return api_request("GET", url, api_key)
412
+
413
+
414
+ # ---------------------------------------------------------------------------
415
+ # API Helpers β€” Knowledge Hub (collections linked to a project)
416
+ # ---------------------------------------------------------------------------
417
+
418
+ def list_collections(
419
+ api_url: str,
420
+ api_key: str,
421
+ project_id: str,
422
+ ) -> Any:
423
+ """List Airweave collections linked to a project via Knowledge Hub."""
424
+ url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/collections"
425
+ result = api_request("GET", url, api_key)
426
+ # Distinguish None (API error) from [] (no collections)
427
+ return result
428
+
429
+
430
+ def search_collections(
431
+ api_url: str,
432
+ api_key: str,
433
+ project_id: str,
434
+ query: str,
435
+ limit: int = 5,
436
+ min_score: float = 0.01,
437
+ ) -> dict:
438
+ """Search all linked collections for context relevant to a query.
439
+
440
+ 1. Lists collections linked to the project.
441
+ 2. Searches each collection via Airweave Search API.
442
+ 3. Returns aggregated results filtered by min_score.
443
+ """
444
+ collections = list_collections(api_url, api_key, project_id)
445
+
446
+ # None means API error (auth, network, server error)
447
+ if collections is None:
448
+ return {
449
+ "status": "error",
450
+ "message": "Failed to list collections β€” check API key permissions and project_id",
451
+ "collections_searched": 0,
452
+ "results": [],
453
+ }
454
+
455
+ # Empty list means no collections linked to this project
456
+ if not collections:
457
+ return {
458
+ "status": "ok",
459
+ "message": "No collections linked to this project. Link collections via Knowledge Hub.",
460
+ "collections_searched": 0,
461
+ "results": [],
462
+ }
463
+
464
+ all_results = []
465
+ errors = []
466
+ for col in collections:
467
+ readable_id = col.get("collection_readable_id")
468
+ col_name = col.get("collection_name", "")
469
+ if not readable_id:
470
+ continue
471
+
472
+ search_url = f"{api_url.rstrip('/')}/collections/{readable_id}/search"
473
+ # Search with higher limit to capture all chunks of matching documents
474
+ search_body = {
475
+ "query": query,
476
+ "limit": 50,
477
+ "strategy": "hybrid",
478
+ }
479
+ # Increased timeout to 180s because backend uses rate-limited embedding APIs.
480
+ # Backend handles 429s with exponential backoff, which may take ~1-2 minutes.
481
+ resp = api_request("POST", search_url, api_key, search_body, timeout=180)
482
+ if not resp:
483
+ errors.append(f"Search failed for collection '{col_name}' ({readable_id})")
484
+ continue
485
+
486
+ # Group chunks by original document (original_entity_id) and reconstruct
487
+ # full documents by concatenating chunks sorted by chunk_index.
488
+ doc_groups = {}
489
+ for hit in resp.get("results", []):
490
+ score = hit.get("score", 0)
491
+ if score < min_score:
492
+ continue
493
+ sys_meta = hit.get("system_metadata", {})
494
+ src_fields = hit.get("source_fields", {})
495
+ entity_id = sys_meta.get("original_entity_id", hit.get("entity_id", ""))
496
+ chunk_idx = sys_meta.get("chunk_index", 0)
497
+ content = hit.get("textual_representation") or hit.get("md_content") or ""
498
+
499
+ if entity_id not in doc_groups:
500
+ doc_groups[entity_id] = {
501
+ "title": hit.get("name", src_fields.get("title", entity_id)),
502
+ "source": sys_meta.get("source_name", hit.get("source_name", "")),
503
+ "best_score": score,
504
+ "chunks": [],
505
+ }
506
+ doc = doc_groups[entity_id]
507
+ doc["best_score"] = max(doc["best_score"], score)
508
+ doc["chunks"].append((chunk_idx, content))
509
+
510
+ # Reconstruct full documents from sorted chunks
511
+ matches = []
512
+ for entity_id, doc in doc_groups.items():
513
+ doc["chunks"].sort(key=lambda c: c[0])
514
+ full_content = "\n".join(chunk[1] for chunk in doc["chunks"])
515
+ matches.append({
516
+ "title": doc["title"],
517
+ "content": full_content,
518
+ "score": round(doc["best_score"], 3),
519
+ "source": doc["source"],
520
+ "chunks_count": len(doc["chunks"]),
521
+ })
522
+ # Sort by best score descending
523
+ matches.sort(key=lambda m: m["score"], reverse=True)
524
+ if matches:
525
+ all_results.append({
526
+ "collection": col_name,
527
+ "readable_id": readable_id,
528
+ "matches": matches[:limit],
529
+ })
530
+
531
+ result = {
532
+ "status": "ok",
533
+ "collections_searched": len(collections),
534
+ "collections_found": [c.get("collection_name", "") for c in collections],
535
+ "results": all_results,
536
+ }
537
+ if errors:
538
+ result["warnings"] = errors
539
+ return result
540
+
541
+
542
+ def _suggest_project_name() -> str:
543
+ """Suggest a project name from the current directory or PROJECT-PROGRESS.md."""
544
+ project_root = str(BRIDGE_DIR.parent.parent)
545
+
546
+ # Try to extract from PROJECT-PROGRESS.md
547
+ progress_file = os.path.join(project_root, "docs", "PROJECT-PROGRESS.md")
548
+ if os.path.exists(progress_file):
549
+ try:
550
+ with open(progress_file, "r") as f:
551
+ for line in f:
552
+ match = re.search(r"\|\s*Projeto\s*\|\s*(.+?)\s*\|", line)
553
+ if match:
554
+ return match.group(1).strip()
555
+ except Exception:
556
+ pass
557
+
558
+ # Fallback to directory name
559
+ return os.path.basename(project_root).replace("-", " ").replace("_", " ").title()
560
+
561
+
562
+ # ---------------------------------------------------------------------------
563
+ # Interactive Setup
564
+ # ---------------------------------------------------------------------------
565
+
566
+ def setup_interactive() -> dict:
567
+ """Interactive setup with project selection/creation and doc registration."""
568
+ config = load_config()
569
+
570
+ print("\nπŸ”— Flyee Bridge β€” Setup")
571
+ print("=" * 40)
572
+ print()
573
+ print("O flyee-bridge conecta este projeto Γ  plataforma Flyee,")
574
+ print("enviando eventos de desenvolvimento (tasks, testes, deploys)")
575
+ print("e registrando documentaΓ§Γ£o automaticamente.")
576
+ print()
577
+
578
+ choice = input("Deseja integrar com a plataforma Flyee? (s/n): ").strip().lower()
579
+
580
+ if choice in ("n", "nao", "nΓ£o", "no"):
581
+ config["opted_out"] = True
582
+ config["enabled"] = False
583
+ save_config(config)
584
+ print("\nβœ… IntegraΓ§Γ£o desabilitada. Eventos NΓƒO serΓ£o enviados.")
585
+ print(" Para reconfigurar: python .agent/flyee-bridge/bridge.py --setup")
586
+ return config
587
+
588
+ # --- Step 1: Authentication ---
589
+ print("\nπŸ“‘ Passo 1/4 β€” AutenticaΓ§Γ£o")
590
+ print("-" * 30)
591
+
592
+ config["api_url"] = PROD_API_URL
593
+ print(f" API URL: {PROD_API_URL} (padrΓ£o prod)")
594
+
595
+ print()
596
+ print("πŸ“‹ Obtenha sua API Key na plataforma Flyee:")
597
+ print(" Settings β†’ API Keys β†’ Generate Key")
598
+ print()
599
+ api_key = input("API Key: ").strip()
600
+ if not api_key:
601
+ print("❌ API Key é obrigatória.")
602
+ return config
603
+ config["api_key"] = api_key
604
+
605
+ # --- Step 2: Project Selection/Creation ---
606
+ print("\nπŸ“‚ Passo 2/4 β€” Selecionar ou Criar Projeto")
607
+ print("-" * 30)
608
+
609
+ projects = list_projects(config["api_url"], api_key)
610
+
611
+ if projects is None:
612
+ print("⚠️ Não foi possível listar projetos. Verificar API URL e API Key.")
613
+ project_id = input("\nProject ID (UUID manual, ou Enter para criar novo): ").strip()
614
+ if not project_id:
615
+ project_id = _create_project_interactive(config["api_url"], api_key)
616
+ if not project_id:
617
+ return config
618
+ config["project_id"] = project_id
619
+ elif len(projects) == 0:
620
+ print("Nenhum projeto encontrado na plataforma.")
621
+ project_id = _create_project_interactive(config["api_url"], api_key)
622
+ if not project_id:
623
+ return config
624
+ config["project_id"] = project_id
625
+ else:
626
+ print(f"\n{'#':<4} {'Projeto':<30} {'Status':<12}")
627
+ print("-" * 50)
628
+ for i, p in enumerate(projects, 1):
629
+ name = p.get("name", "Sem nome")
630
+ status = p.get("status", "?")
631
+ print(f"{i:<4} {name:<30} {status:<12}")
632
+ print(f"{len(projects)+1:<4} {'βž• Criar novo projeto':<30}")
633
+
634
+ sel = input(f"\nSelecione [1-{len(projects)+1}]: ").strip()
635
+ try:
636
+ idx = int(sel)
637
+ if 1 <= idx <= len(projects):
638
+ config["project_id"] = str(projects[idx - 1]["id"])
639
+ print(f"βœ… Projeto selecionado: {projects[idx - 1]['name']}")
640
+ else:
641
+ project_id = _create_project_interactive(config["api_url"], api_key)
642
+ if not project_id:
643
+ return config
644
+ config["project_id"] = project_id
645
+ except (ValueError, IndexError):
646
+ project_id = _create_project_interactive(config["api_url"], api_key)
647
+ if not project_id:
648
+ return config
649
+ config["project_id"] = project_id
650
+
651
+ # --- Step 3: Document Registration ---
652
+ print("\nπŸ“„ Passo 3/4 β€” Registrar DocumentaΓ§Γ£o Existente")
653
+ print("-" * 30)
654
+
655
+ docs = scan_docs()
656
+ if docs:
657
+ print(f"Encontrados {len(docs)} documento(s) em docs/:")
658
+ for d in docs:
659
+ print(f" β€’ {os.path.basename(d['path'])} ({d['type']})")
660
+ print()
661
+ reg = input("Registrar estes documentos no Flyee? (s/n) [s]: ").strip().lower()
662
+ if reg not in ("n", "nao", "nΓ£o", "no"):
663
+ results = register_documents(
664
+ config["api_url"], api_key, config["project_id"], docs
665
+ )
666
+ print()
667
+ for r in results:
668
+ icon = "βœ…" if r["status"] == "registered" else "❌"
669
+ print(f" {icon} {r['title']} β€” {r['status']}")
670
+ else:
671
+ print("⏭️ Registro de documentos ignorado.")
672
+ else:
673
+ print("Nenhum documento encontrado em docs/.")
674
+ print("Documentos serΓ£o registrados automaticamente quando criados.")
675
+
676
+ # --- Step 4: Save Config ---
677
+ print("\nπŸ’Ύ Passo 4/4 β€” Salvar ConfiguraΓ§Γ£o")
678
+ print("-" * 30)
679
+
680
+ config["enabled"] = True
681
+ config["opted_out"] = False
682
+ save_config(config)
683
+
684
+ print("\nβœ… Flyee Bridge configurado com sucesso!")
685
+ print(f" API: {config['api_url']}")
686
+ print(f" Project: {config['project_id']}")
687
+ print(" Eventos serΓ£o enviados automaticamente pelos workflows.")
688
+ return config
689
+
690
+
691
+ def _create_project_interactive(api_url: str, api_key: str) -> Optional[str]:
692
+ """Interactive project creation. Returns project_id or None."""
693
+ suggested = _suggest_project_name()
694
+ name = input(f"Nome do projeto [{suggested}]: ").strip()
695
+ name = name or suggested
696
+
697
+ desc = input("DescriΓ§Γ£o (opcional): ").strip()
698
+
699
+ print(f"\nCriando projeto '{name}'...")
700
+ project = create_project(api_url, api_key, name, desc)
701
+ if project:
702
+ pid = str(project["id"])
703
+ print(f"βœ… Projeto criado: {name} (ID: {pid})")
704
+ return pid
705
+ else:
706
+ print("❌ Falha ao criar projeto.")
707
+ return None
708
+
709
+
710
+ def emit_event(
711
+ event_type: str,
712
+ payload: Optional[dict] = None,
713
+ config: Optional[dict] = None,
714
+ ) -> bool:
715
+ """Emit a structured event to the Flyee Platform.
716
+
717
+ Returns True if event was sent or queued, False if bridge is disabled.
718
+ """
719
+ if config is None:
720
+ config = load_config()
721
+
722
+ # Skip silently if opted out or not configured
723
+ if config.get("opted_out") or not is_configured(config):
724
+ return False
725
+
726
+ event_data = {
727
+ "project_id": config["project_id"],
728
+ "entity_type": event_type.split(".")[0],
729
+ "event_type": event_type,
730
+ "payload": {
731
+ **(payload or {}),
732
+ "_timestamp": datetime.now(timezone.utc).isoformat(),
733
+ "_agent_runtime": ".agent",
734
+ },
735
+ "source": "flyee-bridge",
736
+ }
737
+
738
+ # Try sending via HTTP
739
+ url = f"{config['api_url'].rstrip('/')}/flyee/events/ingest"
740
+ headers = {
741
+ "Content-Type": "application/json",
742
+ "X-API-Key": config["api_key"],
743
+ "X-Bridge-API-Key": config["api_key"],
744
+ }
745
+
746
+ max_retries = 3
747
+ for attempt in range(max_retries):
748
+ try:
749
+ import urllib.request
750
+
751
+ req = urllib.request.Request(
752
+ url,
753
+ data=json.dumps(event_data).encode("utf-8"),
754
+ headers=headers,
755
+ method="POST",
756
+ )
757
+ with urllib.request.urlopen(req, timeout=5) as resp:
758
+ if resp.status in (200, 201):
759
+ return True
760
+ except Exception as e:
761
+ if attempt < max_retries - 1:
762
+ wait = 2 ** attempt
763
+ time.sleep(wait)
764
+ else:
765
+ # Fallback: write to local file
766
+ _fallback_write(event_data, config)
767
+ return True
768
+
769
+ return False
770
+
771
+
772
+ def _fallback_write(event_data: dict, config: dict) -> None:
773
+ """Write event to local JSONL file as fallback."""
774
+ fallback = config.get("fallback_file", str(FALLBACK_PATH))
775
+ with open(fallback, "a") as f:
776
+ f.write(json.dumps(event_data) + "\n")
777
+
778
+
779
+ def emit_decision(
780
+ decision: str,
781
+ category: str,
782
+ reason: Optional[str] = None,
783
+ impact: Optional[str] = None,
784
+ task_id: Optional[str] = None,
785
+ config: Optional[dict] = None,
786
+ ) -> bool:
787
+ """Record a governance decision: emit event + persist via API.
788
+
789
+ Convenience wrapper that:
790
+ 1. Emits a ``dev.decision_detected`` event to the Activity feed
791
+ 2. Creates the decision record via the Decisions API
792
+
793
+ Args:
794
+ decision: Short decision statement (e.g. "Use Next.js App Router")
795
+ category: Domain category β€” one of: architecture, design_system,
796
+ deploy, refactoring, security, tech_debt, implementation
797
+ reason: Rationale for the decision
798
+ impact: Expected impact / scope
799
+ task_id: Optional related task UUID
800
+ config: Bridge config (auto-loaded if None)
801
+
802
+ Returns:
803
+ True if both event and decision record were successfully sent.
804
+ """
805
+ if config is None:
806
+ config = load_config()
807
+
808
+ if config.get("opted_out") or not is_configured(config):
809
+ return False
810
+
811
+ # 1. Emit event to Activity feed
812
+ emit_event(
813
+ "dev.decision_detected",
814
+ {
815
+ "decision": decision,
816
+ "category": category,
817
+ "reason": reason,
818
+ "impact": impact,
819
+ "task_id": task_id,
820
+ },
821
+ config,
822
+ )
823
+
824
+ # 2. Persist decision record via API
825
+ result = create_decision(
826
+ api_url=config["api_url"],
827
+ api_key=config["api_key"],
828
+ project_id=config["project_id"],
829
+ decision=f"[{category.upper()}] {decision}",
830
+ actor="agent",
831
+ reason=reason,
832
+ impact=impact,
833
+ task_id=task_id,
834
+ )
835
+
836
+ return result is not None
837
+
838
+
839
+ def test_connection(config: dict) -> None:
840
+ """Send a test event to verify connectivity."""
841
+ print(f"\nπŸ”— Testing connection to {config['api_url']}...")
842
+
843
+ if not is_configured(config):
844
+ print("❌ Bridge not configured. Run: python bridge.py --setup")
845
+ return
846
+
847
+ success = emit_event(
848
+ "dev.test_run",
849
+ {"test": True, "message": "flyee-bridge connectivity test"},
850
+ config,
851
+ )
852
+
853
+ if success:
854
+ print("βœ… Test event sent successfully!")
855
+ else:
856
+ print("❌ Failed to send test event. Check API URL and API key.")
857
+
858
+
859
+ def main():
860
+ args = sys.argv[1:]
861
+
862
+ if not args or "--help" in args:
863
+ print(__doc__)
864
+ return
865
+
866
+ if "--setup" in args:
867
+ setup_interactive()
868
+ return
869
+
870
+ config = load_config()
871
+
872
+ # First-run detection: if not configured and not opted out, prompt
873
+ if not config.get("opted_out") and not is_configured(config):
874
+ if sys.stdin.isatty():
875
+ print("\n⚠️ Flyee Bridge não estÑ configurado.")
876
+ config = setup_interactive()
877
+ if not is_configured(config):
878
+ return
879
+ else:
880
+ # Non-interactive: skip silently
881
+ return
882
+
883
+ if "--test" in args:
884
+ test_connection(config)
885
+ return
886
+
887
+ if "--list-projects" in args:
888
+ if not config.get("api_key"):
889
+ print("❌ API Key não configurada. Execute --setup primeiro.")
890
+ return
891
+ projects = list_projects(
892
+ config.get("api_url", "http://localhost:8001"), config["api_key"]
893
+ )
894
+ if projects:
895
+ print(f"\n{'#':<4} {'Projeto':<30} {'Status':<12} {'ID'}")
896
+ print("-" * 80)
897
+ for i, p in enumerate(projects, 1):
898
+ print(f"{i:<4} {p.get('name', '?'):<30} {p.get('status', '?'):<12} {p.get('id', '?')}")
899
+ else:
900
+ print("Nenhum projeto encontrado ou erro de conexΓ£o.")
901
+ return
902
+
903
+ if "--register-docs" in args:
904
+ if not is_configured(config):
905
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
906
+ return
907
+ docs = scan_docs()
908
+ if not docs:
909
+ print("Nenhum documento encontrado em docs/.")
910
+ return
911
+ print(f"Registrando {len(docs)} documento(s)...")
912
+ results = register_documents(
913
+ config["api_url"], config["api_key"], config["project_id"], docs
914
+ )
915
+ for r in results:
916
+ icon = "βœ…" if r["status"] == "registered" else "❌"
917
+ print(f" {icon} {r['title']} β€” {r['status']}")
918
+ return
919
+
920
+ if "--persist-plan" in args:
921
+ if not is_configured(config):
922
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
923
+ return
924
+ # Parse: --persist-plan <path> [--title <title>] [--type <type>] [--task-id <id>]
925
+ plan_path = ""
926
+ plan_title = ""
927
+ plan_type = "plan"
928
+ task_id_cli = "" # task_id via CLI flag
929
+ i = 0
930
+ while i < len(args):
931
+ if args[i] == "--persist-plan" and i + 1 < len(args):
932
+ plan_path = args[i + 1]
933
+ i += 2
934
+ elif args[i] == "--title" and i + 1 < len(args):
935
+ plan_title = args[i + 1]
936
+ i += 2
937
+ elif args[i] == "--type" and i + 1 < len(args):
938
+ plan_type = args[i + 1]
939
+ i += 2
940
+ elif args[i] == "--task-id" and i + 1 < len(args):
941
+ task_id_cli = args[i + 1]
942
+ i += 2
943
+ else:
944
+ i += 1
945
+
946
+ if not plan_path or not os.path.isfile(plan_path):
947
+ print(f"❌ Arquivo não encontrado: {plan_path}")
948
+ sys.exit(1)
949
+
950
+ try:
951
+ with open(plan_path, "r", encoding="utf-8") as f:
952
+ raw_content = f.read()
953
+ except Exception as e:
954
+ print(f"❌ Erro ao ler arquivo: {e}")
955
+ sys.exit(1)
956
+
957
+ # --- Frontmatter Parsing (YAML --- block) ---
958
+ frontmatter: dict = {}
959
+ body_content = raw_content
960
+ if raw_content.startswith("---"):
961
+ parts = raw_content.split("---", 2)
962
+ if len(parts) >= 3:
963
+ fm_block = parts[1].strip()
964
+ body_content = parts[2].strip()
965
+ for fm_line in fm_block.splitlines():
966
+ if ":" in fm_line:
967
+ k, _, v = fm_line.partition(":")
968
+ frontmatter[k.strip()] = v.strip()
969
+
970
+ # Resolve task_id: CLI flag takes priority, then frontmatter
971
+ task_id = task_id_cli or frontmatter.get("task_id", "")
972
+
973
+ # Anti-bypass: task_id is REQUIRED β€” never silently skip
974
+ if not task_id:
975
+ print(
976
+ "❌ ERRO: task_id não encontrado.\n"
977
+ " O implementation_plan.md deve conter frontmatter com task_id:\n"
978
+ " ---\n"
979
+ " task_id: <uuid>\n"
980
+ " iteration: 0\n"
981
+ " ---\n"
982
+ " Ou passe --task-id <uuid> via CLI.\n"
983
+ " Execute Fase 0 primeiro: bridge.py --create-task --status backlog"
984
+ )
985
+ sys.exit(1)
986
+
987
+ try:
988
+ current_iteration = int(frontmatter.get("iteration", "0"))
989
+ except ValueError:
990
+ current_iteration = 0
991
+
992
+ content = body_content if body_content else raw_content
993
+
994
+ if not plan_title:
995
+ for line in content.split("\n"):
996
+ if line.startswith("# "):
997
+ plan_title = line[2:].strip()
998
+ break
999
+ if not plan_title:
1000
+ plan_title = Path(plan_path).stem.replace("_", " ").replace("-", " ").title()
1001
+
1002
+ api_url = config["api_url"]
1003
+ api_key = config["api_key"]
1004
+ project_id = config["project_id"]
1005
+
1006
+ list_url = f"{api_url.rstrip('/')}/flyee/projects/{project_id}/documents"
1007
+ existing_docs = api_request("GET", list_url, api_key) or []
1008
+ target_doc = None
1009
+ for doc in existing_docs:
1010
+ if doc.get("type") == plan_type and doc.get("title") == plan_title:
1011
+ target_doc = doc
1012
+ break
1013
+
1014
+ meta = {
1015
+ "source": "bridge",
1016
+ "file_path": plan_path,
1017
+ "linked_task_id": task_id,
1018
+ "iteration": current_iteration + 1,
1019
+ }
1020
+
1021
+ doc_id = None
1022
+ new_version = None
1023
+ resp = None
1024
+
1025
+ if target_doc:
1026
+ doc_id = target_doc["id"]
1027
+ ver_url = f"{api_url.rstrip('/')}/flyee/documents/{doc_id}/versions"
1028
+ resp = api_request("POST", ver_url, api_key, {"content": content, "meta": meta}, timeout=30)
1029
+ if resp:
1030
+ new_version = resp.get("version", current_iteration + 1)
1031
+ skipped = resp.get("skipped", False)
1032
+ if skipped:
1033
+ print(json.dumps({
1034
+ "status": "skipped", "reason": "identical_content",
1035
+ "document_id": doc_id, "version_n": current_iteration,
1036
+ "sha256": resp.get("sha256", ""),
1037
+ }))
1038
+ else:
1039
+ print(json.dumps({
1040
+ "status": "ok", "created": False,
1041
+ "version_n": new_version, "document_id": doc_id,
1042
+ "sha256": resp.get("sha256", ""),
1043
+ }))
1044
+ else:
1045
+ print(f"❌ Erro ao criar versão para '{plan_title}'")
1046
+ sys.exit(1)
1047
+ else:
1048
+ resp = api_request("POST", list_url, api_key, {
1049
+ "title": plan_title, "type": plan_type, "content": content, "meta": meta,
1050
+ }, timeout=30)
1051
+ if resp:
1052
+ doc_id = resp.get("id", "?")
1053
+ new_version = 1
1054
+ print(json.dumps({
1055
+ "status": "ok", "created": True,
1056
+ "version_n": new_version, "document_id": doc_id,
1057
+ "sha256": resp.get("sha256", ""),
1058
+ }))
1059
+ else:
1060
+ print(f"❌ Erro ao criar documento '{plan_title}'")
1061
+ sys.exit(1)
1062
+
1063
+ # Link document to task via DocumentLink
1064
+ if doc_id and task_id and doc_id != "?":
1065
+ link_url = f"{api_url.rstrip('/')}/flyee/documents/{doc_id}/link-task"
1066
+ api_request("POST", link_url, api_key, {"task_id": task_id}, timeout=10)
1067
+
1068
+ # Increment iteration counter in frontmatter of the .md file
1069
+ skipped_update = resp and resp.get("skipped", False)
1070
+ if new_version is not None and not skipped_update:
1071
+ try:
1072
+ new_iteration = new_version
1073
+ if raw_content.startswith("---") and len(raw_content.split("---", 2)) >= 3:
1074
+ fm_raw = raw_content.split("---", 2)[1]
1075
+ if "iteration:" in fm_raw:
1076
+ fm_updated = re.sub(r"(iteration:\s*)\d+", f"\\g<1>{new_iteration}", fm_raw)
1077
+ else:
1078
+ fm_updated = fm_raw.rstrip() + f"\niteration: {new_iteration}\n"
1079
+ updated_file = f"---{fm_updated}---\n{body_content}"
1080
+ else:
1081
+ updated_file = (
1082
+ f"---\ntask_id: {task_id}\niteration: {new_iteration}\n---\n{raw_content}"
1083
+ )
1084
+ with open(plan_path, "w", encoding="utf-8") as f:
1085
+ f.write(updated_file)
1086
+ except Exception:
1087
+ pass # Non-fatal
1088
+ return
1089
+
1090
+
1091
+
1092
+ if "--create-task" in args:
1093
+ if not is_configured(config):
1094
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1095
+ return
1096
+ # Parse arguments
1097
+ name = ""
1098
+ task_type = "implement_feature"
1099
+ description = ""
1100
+ priority = "normal"
1101
+ is_backlog = False
1102
+
1103
+ i = 0
1104
+ while i < len(args):
1105
+ if args[i] == "--name" and i + 1 < len(args):
1106
+ name = args[i + 1]
1107
+ i += 2
1108
+ elif args[i] == "--type" and i + 1 < len(args):
1109
+ task_type = args[i + 1]
1110
+ i += 2
1111
+ elif args[i] == "--description" and i + 1 < len(args):
1112
+ description = args[i + 1]
1113
+ i += 2
1114
+ elif args[i] == "--priority" and i + 1 < len(args):
1115
+ priority = args[i + 1]
1116
+ i += 2
1117
+ elif args[i] == "--backlog":
1118
+ is_backlog = True
1119
+ i += 1
1120
+ else:
1121
+ i += 1
1122
+
1123
+ if not name:
1124
+ print("❌ --name é obrigatório. Ex: --create-task --name 'Fix login bug'")
1125
+ return
1126
+ result = create_task(
1127
+ config["api_url"],
1128
+ config["api_key"],
1129
+ config["project_id"],
1130
+ task_type=task_type,
1131
+ name=name,
1132
+ description=description,
1133
+ priority=priority,
1134
+ is_backlog=is_backlog,
1135
+ )
1136
+
1137
+ if result:
1138
+ task_id = result.get("id", "unknown")
1139
+ emit_event("task.created", {
1140
+ "task_id": task_id,
1141
+ "name": name,
1142
+ "type": task_type,
1143
+ "priority": priority,
1144
+ "actor": "agent",
1145
+ }, config)
1146
+ print(json.dumps({"status": "created", "task_id": task_id, "name": name}))
1147
+ else:
1148
+ print(json.dumps({"status": "error", "message": "Failed to create task"}))
1149
+ return
1150
+
1151
+ if "--update-task" in args:
1152
+ if not is_configured(config):
1153
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1154
+ return
1155
+ task_id = None
1156
+ status = None
1157
+ result_status = None
1158
+ i = 0
1159
+ while i < len(args):
1160
+ if args[i] == "--update-task" and i + 1 < len(args):
1161
+ # Accept: --update-task <UUID> (positional)
1162
+ next_val = args[i + 1]
1163
+ if not next_val.startswith("--"):
1164
+ task_id = next_val
1165
+ i += 2
1166
+ else:
1167
+ i += 1
1168
+ elif args[i] in ("--task_id", "--task-id") and i + 1 < len(args):
1169
+ task_id = args[i + 1]
1170
+ i += 2
1171
+ elif args[i] == "--status" and i + 1 < len(args):
1172
+ status = args[i + 1]
1173
+ i += 2
1174
+ elif args[i] == "--result" and i + 1 < len(args):
1175
+ result_status = args[i + 1]
1176
+ i += 2
1177
+ else:
1178
+ i += 1
1179
+ if not task_id:
1180
+ print("❌ task_id é obrigatório. Ex: --update-task <id> --status completed")
1181
+ return
1182
+ result = update_task(
1183
+ config["api_url"],
1184
+ config["api_key"],
1185
+ task_id,
1186
+ status=status,
1187
+ result_status=result_status,
1188
+ )
1189
+ if result:
1190
+ if status == "completed":
1191
+ emit_event("task.completed", {
1192
+ "task_id": task_id,
1193
+ "result": result_status or "success",
1194
+ "actor": "agent",
1195
+ }, config)
1196
+ elif status == "running":
1197
+ emit_event("task.started", {
1198
+ "task_id": task_id,
1199
+ "actor": "agent",
1200
+ }, config)
1201
+ print(json.dumps({"status": "updated", "task_id": task_id}))
1202
+ else:
1203
+ print(json.dumps({"status": "error", "message": "Failed to update task"}))
1204
+ return
1205
+
1206
+ if "--list-tasks" in args:
1207
+ if not is_configured(config):
1208
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1209
+ return
1210
+ status_filter = None
1211
+ i = 0
1212
+ while i < len(args):
1213
+ if args[i] == "--status" and i + 1 < len(args):
1214
+ status_filter = args[i + 1]
1215
+ i += 2
1216
+ else:
1217
+ i += 1
1218
+ tasks = list_tasks(
1219
+ config["api_url"],
1220
+ config["api_key"],
1221
+ config["project_id"],
1222
+ status=status_filter,
1223
+ )
1224
+ if tasks:
1225
+ print(f"\n{'#':<4} {'Task':<40} {'Status':<12} {'ID'}")
1226
+ print("-" * 100)
1227
+ for i, t in enumerate(tasks, 1):
1228
+ task_name = t.get("input", {}).get("name", t.get("type", "?"))
1229
+ print(f"{i:<4} {task_name:<40} {t.get('status', '?'):<12} {t.get('id', '?')}")
1230
+ else:
1231
+ print("Nenhuma task encontrada.")
1232
+ return
1233
+
1234
+ if "--create-okr" in args:
1235
+ if not is_configured(config):
1236
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1237
+ return
1238
+ objective = ""
1239
+ key_results_str = ""
1240
+ period = ""
1241
+ owner = ""
1242
+ okr_status = "active"
1243
+ i = 0
1244
+ while i < len(args):
1245
+ if args[i] == "--objective" and i + 1 < len(args):
1246
+ objective = args[i + 1]
1247
+ i += 2
1248
+ elif args[i] == "--key-results" and i + 1 < len(args):
1249
+ key_results_str = args[i + 1]
1250
+ i += 2
1251
+ elif args[i] == "--period" and i + 1 < len(args):
1252
+ period = args[i + 1]
1253
+ i += 2
1254
+ elif args[i] == "--owner" and i + 1 < len(args):
1255
+ owner = args[i + 1]
1256
+ i += 2
1257
+ elif args[i] == "--okr-status" and i + 1 < len(args):
1258
+ okr_status = args[i + 1]
1259
+ i += 2
1260
+ else:
1261
+ i += 1
1262
+ if not objective:
1263
+ print("❌ --objective é obrigatório. Ex: --create-okr --objective 'Lançar MVP'")
1264
+ return
1265
+ key_results = json.loads(key_results_str) if key_results_str else None
1266
+ result = create_okr(
1267
+ config["api_url"],
1268
+ config["api_key"],
1269
+ config["project_id"],
1270
+ objective=objective,
1271
+ key_results=key_results,
1272
+ period=period or None,
1273
+ owner=owner or None,
1274
+ status=okr_status,
1275
+ )
1276
+ if result:
1277
+ okr_id = result.get("id", "unknown")
1278
+ emit_event("decision.okr_created", {
1279
+ "okr_id": okr_id,
1280
+ "objective": objective,
1281
+ "period": period,
1282
+ "actor": "agent",
1283
+ }, config)
1284
+ print(json.dumps({"status": "created", "okr_id": okr_id, "objective": objective}))
1285
+ else:
1286
+ print(json.dumps({"status": "error", "message": "Failed to create OKR"}))
1287
+ return
1288
+
1289
+ if "--list-okrs" in args:
1290
+ if not is_configured(config):
1291
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1292
+ return
1293
+ okrs = list_okrs(
1294
+ config["api_url"],
1295
+ config["api_key"],
1296
+ config["project_id"],
1297
+ )
1298
+ if okrs:
1299
+ print(f"\n{'#':<4} {'Objective':<50} {'Status':<12} {'Progress':<10} {'ID'}")
1300
+ print("-" * 120)
1301
+ for i, o in enumerate(okrs, 1):
1302
+ progress = f"{o.get('progress', 0) * 100:.0f}%"
1303
+ print(f"{i:<4} {o.get('objective', '?')[:48]:<50} {o.get('status', '?'):<12} {progress:<10} {o.get('id', '?')}")
1304
+ else:
1305
+ print("Nenhum OKR encontrado.")
1306
+ return
1307
+
1308
+ if "--create-decision" in args:
1309
+ if not is_configured(config):
1310
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1311
+ return
1312
+ decision_text = ""
1313
+ actor = "agent"
1314
+ reason = ""
1315
+ impact = ""
1316
+ task_id_ref = ""
1317
+ category = "implementation"
1318
+ i = 0
1319
+ while i < len(args):
1320
+ if args[i] == "--decision" and i + 1 < len(args):
1321
+ decision_text = args[i + 1]
1322
+ i += 2
1323
+ elif args[i] == "--actor" and i + 1 < len(args):
1324
+ actor = args[i + 1]
1325
+ i += 2
1326
+ elif args[i] == "--reason" and i + 1 < len(args):
1327
+ reason = args[i + 1]
1328
+ i += 2
1329
+ elif args[i] == "--impact" and i + 1 < len(args):
1330
+ impact = args[i + 1]
1331
+ i += 2
1332
+ elif args[i] == "--task-id" and i + 1 < len(args):
1333
+ task_id_ref = args[i + 1]
1334
+ i += 2
1335
+ elif args[i] == "--category" and i + 1 < len(args):
1336
+ category = args[i + 1]
1337
+ i += 2
1338
+ else:
1339
+ i += 1
1340
+ if not decision_text:
1341
+ print("❌ --decision é obrigatório. Ex: --create-decision --decision 'Usar Next.js' --category architecture")
1342
+ return
1343
+ success = emit_decision(
1344
+ decision=decision_text,
1345
+ category=category,
1346
+ reason=reason or None,
1347
+ impact=impact or None,
1348
+ task_id=task_id_ref or None,
1349
+ config=config,
1350
+ )
1351
+ if success:
1352
+ print(json.dumps({"status": "created", "decision": decision_text, "category": category}))
1353
+ else:
1354
+ print(json.dumps({"status": "error", "message": "Failed to create decision"}))
1355
+ return
1356
+
1357
+ if "--list-decisions" in args:
1358
+ if not is_configured(config):
1359
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1360
+ return
1361
+ decisions = list_decisions(
1362
+ config["api_url"],
1363
+ config["api_key"],
1364
+ config["project_id"],
1365
+ )
1366
+ if decisions:
1367
+ print(f"\n{'#':<4} {'Decision':<45} {'Actor':<10} {'Date':<20} {'ID'}")
1368
+ print("-" * 130)
1369
+ for i, d in enumerate(decisions, 1):
1370
+ date_str = d.get("created_at", "?")[:19]
1371
+ print(f"{i:<4} {d.get('decision', '?')[:43]:<45} {d.get('actor', '?'):<10} {date_str:<20} {d.get('id', '?')}")
1372
+ else:
1373
+ print("Nenhuma decision encontrada.")
1374
+ return
1375
+
1376
+ if "--register-metrics" in args:
1377
+ if not is_configured(config):
1378
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1379
+ return
1380
+ metric_type = ""
1381
+ metric_payload = "{}"
1382
+ i = 0
1383
+ while i < len(args):
1384
+ if args[i] == "--type" and i + 1 < len(args):
1385
+ metric_type = args[i + 1]
1386
+ i += 2
1387
+ elif args[i] == "--data" and i + 1 < len(args):
1388
+ metric_payload = args[i + 1]
1389
+ i += 2
1390
+ else:
1391
+ i += 1
1392
+ if not metric_type:
1393
+ print("❌ --type é obrigatório. Tipos: session_started, files_changed, tests_passed")
1394
+ return
1395
+ event_type = f"dev.{metric_type}"
1396
+ payload_data = json.loads(metric_payload)
1397
+ success = emit_event(event_type, payload_data, config)
1398
+ if success:
1399
+ print(json.dumps({"status": "emitted", "event": event_type}))
1400
+ else:
1401
+ print(json.dumps({"status": "skipped", "event": event_type}))
1402
+ return
1403
+
1404
+ if "--list-collections" in args:
1405
+ if not is_configured(config):
1406
+ print("❌ Bridge não configurado. Execute --setup primeiro.")
1407
+ return
1408
+ collections = list_collections(
1409
+ config["api_url"],
1410
+ config["api_key"],
1411
+ config["project_id"],
1412
+ )
1413
+ if collections is None:
1414
+ print(json.dumps({"status": "error", "message": "Failed to list collections"}))
1415
+ elif not collections:
1416
+ print(json.dumps({"status": "ok", "collections": [], "total": 0}))
1417
+ else:
1418
+ print(json.dumps({
1419
+ "status": "ok",
1420
+ "total": len(collections),
1421
+ "collections": [
1422
+ {
1423
+ "id": c.get("collection_id", ""),
1424
+ "name": c.get("collection_name", ""),
1425
+ "readable_id": c.get("collection_readable_id", ""),
1426
+ }
1427
+ for c in collections
1428
+ ],
1429
+ }))
1430
+ return
1431
+
1432
+ if "--search-context" in args:
1433
+ if not is_configured(config):
1434
+ print(json.dumps({"status": "skipped", "reason": "bridge not configured"}))
1435
+ return
1436
+ query = ""
1437
+ limit = 5
1438
+ min_score = 0.01
1439
+ i = 0
1440
+ while i < len(args):
1441
+ if args[i] == "--search-context" and i + 1 < len(args):
1442
+ query = args[i + 1]
1443
+ i += 2
1444
+ elif args[i] == "--limit" and i + 1 < len(args):
1445
+ limit = int(args[i + 1])
1446
+ i += 2
1447
+ elif args[i] == "--min-score" and i + 1 < len(args):
1448
+ min_score = float(args[i + 1])
1449
+ i += 2
1450
+ else:
1451
+ i += 1
1452
+ if not query:
1453
+ print(json.dumps({"status": "error", "message": "Query is required after --search-context"}))
1454
+ return
1455
+ results = search_collections(
1456
+ config["api_url"],
1457
+ config["api_key"],
1458
+ config["project_id"],
1459
+ query=query,
1460
+ limit=limit,
1461
+ min_score=min_score,
1462
+ )
1463
+ print(json.dumps(results, ensure_ascii=False))
1464
+ return
1465
+
1466
+ # ── Test Checklist Commands ──────────────────────────────────
1467
+
1468
+ if "--generate-tests" in args:
1469
+ idx = args.index("--generate-tests")
1470
+ task_id = args[idx + 1] if idx + 1 < len(args) else None
1471
+ # Optional: --files flag to pass modified files list
1472
+ files_modified: list = []
1473
+ if "--files" in args:
1474
+ fidx = args.index("--files")
1475
+ if fidx + 1 < len(args):
1476
+ try:
1477
+ files_modified = json.loads(args[fidx + 1])
1478
+ except (json.JSONDecodeError, ValueError):
1479
+ files_modified = [f.strip() for f in args[fidx + 1].split(",") if f.strip()]
1480
+
1481
+ if not task_id:
1482
+ print(json.dumps({"status": "error", "message": "Usage: --generate-tests <task_id> [--files '[\"path1\",\"path2\"]']"}))
1483
+ return
1484
+ if not is_configured(config):
1485
+ print(json.dumps({"status": "skipped", "reason": "bridge not configured"}))
1486
+ return
1487
+
1488
+ base = config["api_url"].rstrip("/")
1489
+
1490
+ # If files_modified provided, persist them into meta first so the backend can classify them
1491
+ if files_modified:
1492
+ task_data = api_request("GET", f"{base}/flyee/tasks/{task_id}", config["api_key"])
1493
+ if task_data:
1494
+ meta = task_data.get("meta") or {}
1495
+ meta["files_modified"] = files_modified
1496
+ api_request("PUT", f"{base}/flyee/tasks/{task_id}", config["api_key"], {"meta": meta})
1497
+
1498
+ # Delegate to the backend endpoint which applies file-type heuristics
1499
+ result = api_request(
1500
+ "POST",
1501
+ f"{base}/flyee/tasks/{task_id}/test-checklist/generate",
1502
+ config["api_key"],
1503
+ )
1504
+ if result:
1505
+ checklist = (result.get("meta") or {}).get("test_checklist", {})
1506
+ steps_count = len(checklist.get("steps", []))
1507
+ print(json.dumps({"status": "ok", "task_id": task_id, "steps_generated": steps_count}))
1508
+ else:
1509
+ print(json.dumps({"status": "error", "message": "Failed to generate test checklist"}))
1510
+ return
1511
+
1512
+ if "--report-test" in args:
1513
+ idx = args.index("--report-test")
1514
+ remaining = args[idx + 1:]
1515
+ if len(remaining) < 3:
1516
+ print(json.dumps({"status": "error", "message": "Usage: --report-test <task_id> <step_id> passed|failed [comment]"}))
1517
+ return
1518
+ task_id, step_id, status = remaining[0], remaining[1], remaining[2]
1519
+ comment = remaining[3] if len(remaining) > 3 else None
1520
+ if status not in ("passed", "failed", "skipped"):
1521
+ print(json.dumps({"status": "error", "message": f"Invalid status: {status}. Use passed|failed|skipped"}))
1522
+ return
1523
+ if not is_configured(config):
1524
+ print(json.dumps({"status": "skipped", "reason": "bridge not configured"}))
1525
+ return
1526
+ base = config["api_url"].rstrip("/")
1527
+ payload = {"step_id": step_id, "status": status, "tested_by": "agent"}
1528
+ if comment:
1529
+ payload["result_comment"] = comment
1530
+ result = api_request("PUT", f"{base}/flyee/tasks/{task_id}/test-results", config["api_key"], payload)
1531
+ if result:
1532
+ tc = (result.get("meta") or {}).get("test_checklist", {})
1533
+ print(json.dumps({
1534
+ "status": "ok",
1535
+ "step_id": step_id,
1536
+ "step_status": status,
1537
+ "all_passed": tc.get("all_passed", False),
1538
+ }))
1539
+ else:
1540
+ print(json.dumps({"status": "error", "message": "Failed to update test result"}))
1541
+ return
1542
+
1543
+ if "--pending-tests" in args:
1544
+ idx = args.index("--pending-tests")
1545
+ task_id = args[idx + 1] if idx + 1 < len(args) else None
1546
+ if not task_id:
1547
+ print(json.dumps({"status": "error", "message": "Usage: --pending-tests <task_id>"}))
1548
+ return
1549
+ if not is_configured(config):
1550
+ print(json.dumps({"status": "skipped", "reason": "bridge not configured"}))
1551
+ return
1552
+ base = config["api_url"].rstrip("/")
1553
+ task_data = api_request("GET", f"{base}/flyee/tasks/{task_id}", config["api_key"])
1554
+ if not task_data:
1555
+ print(json.dumps({"status": "error", "message": f"Task {task_id} not found"}))
1556
+ return
1557
+ tc = (task_data.get("meta") or {}).get("test_checklist", {})
1558
+ steps = tc.get("steps", [])
1559
+ pending = [s for s in steps if s.get("status") in ("pending", "failed")]
1560
+ print(json.dumps({
1561
+ "status": "ok",
1562
+ "task_id": task_id,
1563
+ "total": len(steps),
1564
+ "pending_count": len(pending),
1565
+ "pending": [{"id": s["id"], "description": s["description"], "status": s["status"],
1566
+ "category": s.get("category", ""), "type": s.get("type", "")} for s in pending],
1567
+ }))
1568
+ return
1569
+
1570
+ if "--test-summary" in args:
1571
+ idx = args.index("--test-summary")
1572
+ task_id = args[idx + 1] if idx + 1 < len(args) else None
1573
+ if not task_id:
1574
+ print(json.dumps({"status": "error", "message": "Usage: --test-summary <task_id>"}))
1575
+ return
1576
+ if not is_configured(config):
1577
+ print(json.dumps({"status": "skipped", "reason": "bridge not configured"}))
1578
+ return
1579
+ base = config["api_url"].rstrip("/")
1580
+ task_data = api_request("GET", f"{base}/flyee/tasks/{task_id}", config["api_key"])
1581
+ if not task_data:
1582
+ print(json.dumps({"status": "error", "message": f"Task {task_id} not found"}))
1583
+ return
1584
+ tc = (task_data.get("meta") or {}).get("test_checklist", {})
1585
+ steps = tc.get("steps", [])
1586
+ passed = sum(1 for s in steps if s.get("status") == "passed")
1587
+ failed_steps = [s["id"] for s in steps if s.get("status") == "failed"]
1588
+ skipped = sum(1 for s in steps if s.get("status") == "skipped")
1589
+ pending = sum(1 for s in steps if s.get("status") == "pending")
1590
+ print(json.dumps({
1591
+ "status": "ok",
1592
+ "task_id": task_id,
1593
+ "total": len(steps),
1594
+ "passed": passed,
1595
+ "failed": len(failed_steps),
1596
+ "skipped": skipped,
1597
+ "pending": pending,
1598
+ "failed_ids": failed_steps,
1599
+ "all_passed": tc.get("all_passed", False),
1600
+ }))
1601
+ return
1602
+
1603
+ # ── Sprint Progress Command ──────────────────────────────────
1604
+
1605
+ if "--sprint-progress" in args:
1606
+ if not is_configured(config):
1607
+ print(json.dumps({"status": "skipped", "reason": "bridge not configured"}))
1608
+ return
1609
+ sprint_n = ""
1610
+ sprint_name = ""
1611
+ done = ""
1612
+ total = ""
1613
+ i = 0
1614
+ while i < len(args):
1615
+ if args[i] == "--sprint" and i + 1 < len(args):
1616
+ sprint_n = args[i + 1]
1617
+ i += 2
1618
+ elif args[i] == "--name" and i + 1 < len(args):
1619
+ sprint_name = args[i + 1]
1620
+ i += 2
1621
+ elif args[i] == "--done" and i + 1 < len(args):
1622
+ done = args[i + 1]
1623
+ i += 2
1624
+ elif args[i] == "--total" and i + 1 < len(args):
1625
+ total = args[i + 1]
1626
+ i += 2
1627
+ else:
1628
+ i += 1
1629
+ if not sprint_n or not total:
1630
+ print(json.dumps({"status": "error", "message": "Usage: --sprint-progress --sprint N --name 'Name' --done X --total Y"}))
1631
+ return
1632
+ done_n = int(done) if done else 0
1633
+ total_n = int(total)
1634
+ pct = round((done_n / total_n) * 100) if total_n > 0 else 0
1635
+ sprint_status = "completed" if done_n >= total_n else "in_progress"
1636
+ payload = {
1637
+ "sprint_number": int(sprint_n),
1638
+ "sprint_name": sprint_name or f"Sprint {sprint_n}",
1639
+ "tasks_done": done_n,
1640
+ "tasks_total": total_n,
1641
+ "pct_complete": pct,
1642
+ "status": sprint_status,
1643
+ }
1644
+ # 1. Emit event for DevActivityView
1645
+ emit_event("dev.sprint_progress", payload, config)
1646
+ # 2. Create/update sprint report document for Project Progress view
1647
+ api_url = config["api_url"].rstrip("/")
1648
+ api_key = config["api_key"]
1649
+ project_id = config["project_id"]
1650
+ report_title = f"Sprint {sprint_n} β€” {sprint_name or 'Progress'}"
1651
+ report_content = (
1652
+ f"# {report_title}\n\n"
1653
+ f"**Status:** {'βœ… ConcluΓ­da' if sprint_status == 'completed' else 'πŸ”„ Em progresso'}\n"
1654
+ f"**Progresso:** {done_n}/{total_n} tasks ({pct}%)\n\n"
1655
+ f"---\n\n"
1656
+ f"*Última atualização: bridge --sprint-progress*\n"
1657
+ )
1658
+ list_url = f"{api_url}/flyee/projects/{project_id}/documents"
1659
+ existing_docs = api_request("GET", list_url, api_key) or []
1660
+ target_doc = None
1661
+ for doc in existing_docs:
1662
+ if doc.get("type") == "sprint_report" and doc.get("title") == report_title:
1663
+ target_doc = doc
1664
+ break
1665
+ meta = {"source": "bridge", "sprint": int(sprint_n), "progress": payload}
1666
+ if target_doc:
1667
+ doc_id = target_doc["id"]
1668
+ ver_url = f"{api_url}/flyee/documents/{doc_id}/versions"
1669
+ resp = api_request("POST", ver_url, api_key, {"content": report_content, "meta": meta}, timeout=10)
1670
+ if resp and not resp.get("skipped"):
1671
+ print(json.dumps({"status": "ok", "event": "dev.sprint_progress", "document_id": doc_id, "updated": True, **payload}))
1672
+ elif resp and resp.get("skipped"):
1673
+ print(json.dumps({"status": "ok", "event": "dev.sprint_progress", "document_id": doc_id, "skipped": True, **payload}))
1674
+ else:
1675
+ print(json.dumps({"status": "ok", "event": "dev.sprint_progress", "document_only": False, **payload}))
1676
+ else:
1677
+ resp = api_request("POST", list_url, api_key, {
1678
+ "title": report_title, "type": "sprint_report", "content": report_content, "meta": meta,
1679
+ }, timeout=10)
1680
+ if resp:
1681
+ print(json.dumps({"status": "ok", "event": "dev.sprint_progress", "document_id": resp.get("id", "?"), "created": True, **payload}))
1682
+ else:
1683
+ print(json.dumps({"status": "ok", "event": "dev.sprint_progress", "document_only": False, **payload}))
1684
+ return
1685
+
1686
+ # ── Persist RETRO Command ────────────────────────────────────
1687
+
1688
+ if "--persist-retro" in args:
1689
+ if not is_configured(config):
1690
+ print(json.dumps({"status": "skipped", "reason": "bridge not configured"}))
1691
+ return
1692
+ retro_path = ""
1693
+ retro_title = ""
1694
+ i = 0
1695
+ while i < len(args):
1696
+ if args[i] == "--persist-retro" and i + 1 < len(args):
1697
+ retro_path = args[i + 1]
1698
+ i += 2
1699
+ elif args[i] == "--title" and i + 1 < len(args):
1700
+ retro_title = args[i + 1]
1701
+ i += 2
1702
+ else:
1703
+ i += 1
1704
+ if not retro_path or not os.path.isfile(retro_path):
1705
+ print(json.dumps({"status": "error", "message": f"File not found: {retro_path}"}))
1706
+ sys.exit(1)
1707
+ try:
1708
+ with open(retro_path, "r", encoding="utf-8") as f:
1709
+ raw_content = f.read()
1710
+ except Exception as e:
1711
+ print(json.dumps({"status": "error", "message": f"Cannot read file: {e}"}))
1712
+ sys.exit(1)
1713
+ # Strip frontmatter for content
1714
+ body = raw_content
1715
+ if raw_content.startswith("---"):
1716
+ parts = raw_content.split("---", 2)
1717
+ if len(parts) >= 3:
1718
+ body = parts[2].strip()
1719
+ if not retro_title:
1720
+ for line in body.split("\n"):
1721
+ if line.startswith("# "):
1722
+ retro_title = line[2:].strip()
1723
+ break
1724
+ if not retro_title:
1725
+ retro_title = Path(retro_path).stem.replace("_", " ").replace("-", " ").title()
1726
+ api_url = config["api_url"].rstrip("/")
1727
+ api_key = config["api_key"]
1728
+ project_id = config["project_id"]
1729
+ # Check if RETRO document already exists
1730
+ list_url = f"{api_url}/flyee/projects/{project_id}/documents"
1731
+ existing_docs = api_request("GET", list_url, api_key) or []
1732
+ target_doc = None
1733
+ for doc in existing_docs:
1734
+ if doc.get("type") == "retro" and doc.get("title") == retro_title:
1735
+ target_doc = doc
1736
+ break
1737
+ meta = {"source": "bridge", "file_path": retro_path}
1738
+ content = body if body else raw_content
1739
+ # SHA256 for dedup
1740
+ content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
1741
+ if target_doc:
1742
+ doc_id = target_doc["id"]
1743
+ ver_url = f"{api_url}/flyee/documents/{doc_id}/versions"
1744
+ resp = api_request("POST", ver_url, api_key, {"content": content, "meta": meta}, timeout=15)
1745
+ if resp:
1746
+ skipped = resp.get("skipped", False)
1747
+ if skipped:
1748
+ print(json.dumps({"status": "skipped", "reason": "identical_content", "document_id": doc_id, "sha256": content_hash}))
1749
+ else:
1750
+ print(json.dumps({"status": "ok", "document_id": doc_id, "updated": True, "version": resp.get("version", "?"), "sha256": content_hash}))
1751
+ else:
1752
+ print(json.dumps({"status": "error", "message": "Failed to create version"}))
1753
+ sys.exit(1)
1754
+ else:
1755
+ resp = api_request("POST", list_url, api_key, {
1756
+ "title": retro_title, "type": "retro", "content": content, "meta": meta,
1757
+ }, timeout=15)
1758
+ if resp:
1759
+ print(json.dumps({"status": "ok", "document_id": resp.get("id", "?"), "created": True, "sha256": content_hash}))
1760
+ else:
1761
+ print(json.dumps({"status": "error", "message": "Failed to create RETRO document"}))
1762
+ sys.exit(1)
1763
+ return
1764
+
1765
+ if args[0] == "emit" and len(args) >= 2:
1766
+ event_type = args[1]
1767
+ payload = json.loads(args[2]) if len(args) > 2 else {}
1768
+ success = emit_event(event_type, payload, config)
1769
+ if success:
1770
+ print(f"βœ… Event '{event_type}' emitted")
1771
+ else:
1772
+ print(f"⚠️ Event '{event_type}' skipped (bridge disabled)")
1773
+ return
1774
+
1775
+ print(f"Unknown command: {args}")
1776
+ print("Use --help for usage info")
1777
+
1778
+
1779
+ if __name__ == "__main__":
1780
+ main()