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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/bin/install.js +357 -0
- package/bridge/bridge.py +1780 -0
- package/bridge/local_tracker.py +722 -0
- package/core/agents/backend-specialist.md +266 -0
- package/core/agents/code-archaeologist.md +106 -0
- package/core/agents/database-architect.md +226 -0
- package/core/agents/debugger.md +225 -0
- package/core/agents/devops-engineer.md +323 -0
- package/core/agents/documentation-writer.md +104 -0
- package/core/agents/explorer-agent.md +73 -0
- package/core/agents/frontend-specialist.md +743 -0
- package/core/agents/game-developer.md +162 -0
- package/core/agents/mobile-developer.md +377 -0
- package/core/agents/orchestrator.md +416 -0
- package/core/agents/penetration-tester.md +188 -0
- package/core/agents/performance-optimizer.md +187 -0
- package/core/agents/product-manager.md +112 -0
- package/core/agents/product-owner.md +95 -0
- package/core/agents/project-planner.md +470 -0
- package/core/agents/qa-automation-engineer.md +103 -0
- package/core/agents/security-auditor.md +170 -0
- package/core/agents/seo-specialist.md +111 -0
- package/core/agents/stitch-designer.md +190 -0
- package/core/agents/tdd-reviewer.md +282 -0
- package/core/agents/test-engineer.md +158 -0
- package/core/scripts/auto_preview.py +148 -0
- package/core/scripts/checklist.py +243 -0
- package/core/scripts/cost_report.py +149 -0
- package/core/scripts/doc-sync-check.py +461 -0
- package/core/scripts/parse_user_stories.py +79 -0
- package/core/scripts/prepare_notion_updates.py +172 -0
- package/core/scripts/print_create_payload.py +18 -0
- package/core/scripts/session_manager.py +120 -0
- package/core/scripts/task_complete.py +127 -0
- package/core/scripts/verify_all.py +327 -0
- package/core/skills/analytics-strategy/SKILL.md +128 -0
- package/core/skills/api-patterns/SKILL.md +81 -0
- package/core/skills/api-patterns/api-style.md +42 -0
- package/core/skills/api-patterns/auth.md +24 -0
- package/core/skills/api-patterns/documentation.md +26 -0
- package/core/skills/api-patterns/graphql.md +41 -0
- package/core/skills/api-patterns/rate-limiting.md +31 -0
- package/core/skills/api-patterns/response.md +37 -0
- package/core/skills/api-patterns/rest.md +40 -0
- package/core/skills/api-patterns/scripts/api_validator.py +211 -0
- package/core/skills/api-patterns/security-testing.md +122 -0
- package/core/skills/api-patterns/trpc.md +41 -0
- package/core/skills/api-patterns/versioning.md +22 -0
- package/core/skills/app-builder/SKILL.md +75 -0
- package/core/skills/app-builder/agent-coordination.md +71 -0
- package/core/skills/app-builder/feature-building.md +53 -0
- package/core/skills/app-builder/project-detection.md +34 -0
- package/core/skills/app-builder/scaffolding.md +118 -0
- package/core/skills/app-builder/tech-stack.md +40 -0
- package/core/skills/app-builder/templates/SKILL.md +39 -0
- package/core/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
- package/core/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
- package/core/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
- package/core/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
- package/core/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
- package/core/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
- package/core/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
- package/core/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +82 -0
- package/core/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +100 -0
- package/core/skills/app-builder/templates/nextjs-static/TEMPLATE.md +106 -0
- package/core/skills/app-builder/templates/nuxt-app/TEMPLATE.md +101 -0
- package/core/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
- package/core/skills/app-builder/templates/react-native-app/TEMPLATE.md +93 -0
- package/core/skills/architecture/SKILL.md +55 -0
- package/core/skills/architecture/context-discovery.md +43 -0
- package/core/skills/architecture/examples.md +94 -0
- package/core/skills/architecture/pattern-selection.md +68 -0
- package/core/skills/architecture/patterns-reference.md +50 -0
- package/core/skills/architecture/trade-off-analysis.md +77 -0
- package/core/skills/atomic-design/SKILL.md +282 -0
- package/core/skills/atomic-design/references/classification-guide.md +132 -0
- package/core/skills/atomic-design/references/quality-checklist.md +60 -0
- package/core/skills/atomic-design/references/stacks/stack-blade.md +254 -0
- package/core/skills/atomic-design/references/stacks/stack-nextjs.md +272 -0
- package/core/skills/atomic-design/references/stacks/stack-react.md +239 -0
- package/core/skills/atomic-design/references/stacks/stack-vue.md +224 -0
- package/core/skills/bash-linux/SKILL.md +199 -0
- package/core/skills/behavioral-modes/SKILL.md +242 -0
- package/core/skills/brainstorming/SKILL.md +163 -0
- package/core/skills/brainstorming/dynamic-questioning.md +373 -0
- package/core/skills/checkpointing-patterns/SKILL.md +163 -0
- package/core/skills/clean-code/SKILL.md +201 -0
- package/core/skills/code-review-checklist/SKILL.md +109 -0
- package/core/skills/code-truth-validation/SKILL.md +149 -0
- package/core/skills/component-library-discovery/SKILL.md +154 -0
- package/core/skills/content-strategy/SKILL.md +222 -0
- package/core/skills/context-budget/SKILL.md +155 -0
- package/core/skills/context-gathering-patterns/SKILL.md +278 -0
- package/core/skills/cost-tracking/SKILL.md +206 -0
- package/core/skills/database-design/SKILL.md +52 -0
- package/core/skills/database-design/database-selection.md +43 -0
- package/core/skills/database-design/indexing.md +39 -0
- package/core/skills/database-design/migrations.md +48 -0
- package/core/skills/database-design/optimization.md +36 -0
- package/core/skills/database-design/orm-selection.md +30 -0
- package/core/skills/database-design/schema-design.md +56 -0
- package/core/skills/database-design/scripts/schema_validator.py +172 -0
- package/core/skills/deployment-procedures/SKILL.md +295 -0
- package/core/skills/design-md/README.md +34 -0
- package/core/skills/design-md/SKILL.md +172 -0
- package/core/skills/design-md/examples/DESIGN.md +154 -0
- package/core/skills/design-system-enforcement/SKILL.md +339 -0
- package/core/skills/doc.md +177 -0
- package/core/skills/document-registry/SKILL.md +130 -0
- package/core/skills/documentation-publishing/SKILL.md +174 -0
- package/core/skills/documentation-templates/SKILL.md +194 -0
- package/core/skills/enhance-prompt/README.md +34 -0
- package/core/skills/enhance-prompt/SKILL.md +204 -0
- package/core/skills/enhance-prompt/references/KEYWORDS.md +114 -0
- package/core/skills/frontend-design/SKILL.md +430 -0
- package/core/skills/frontend-design/animation-guide.md +331 -0
- package/core/skills/frontend-design/color-system.md +311 -0
- package/core/skills/frontend-design/decision-trees.md +418 -0
- package/core/skills/frontend-design/motion-graphics.md +306 -0
- package/core/skills/frontend-design/scripts/accessibility_checker.py +183 -0
- package/core/skills/frontend-design/scripts/ux_audit.py +722 -0
- package/core/skills/frontend-design/typography-system.md +345 -0
- package/core/skills/frontend-design/ux-psychology.md +541 -0
- package/core/skills/frontend-design/visual-effects.md +383 -0
- package/core/skills/game-development/2d-games/SKILL.md +119 -0
- package/core/skills/game-development/3d-games/SKILL.md +135 -0
- package/core/skills/game-development/SKILL.md +167 -0
- package/core/skills/game-development/game-art/SKILL.md +185 -0
- package/core/skills/game-development/game-audio/SKILL.md +190 -0
- package/core/skills/game-development/game-design/SKILL.md +129 -0
- package/core/skills/game-development/mobile-games/SKILL.md +108 -0
- package/core/skills/game-development/multiplayer/SKILL.md +132 -0
- package/core/skills/game-development/pc-games/SKILL.md +144 -0
- package/core/skills/game-development/vr-ar/SKILL.md +123 -0
- package/core/skills/game-development/web-games/SKILL.md +150 -0
- package/core/skills/geo-fundamentals/SKILL.md +156 -0
- package/core/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
- package/core/skills/git-workflow/SKILL.md +263 -0
- package/core/skills/history-check-patterns/SKILL.md +125 -0
- package/core/skills/i18n-localization/SKILL.md +154 -0
- package/core/skills/i18n-localization/scripts/i18n_checker.py +241 -0
- package/core/skills/integration-completeness/SKILL.md +219 -0
- package/core/skills/intelligent-routing/SKILL.md +370 -0
- package/core/skills/lint-and-validate/SKILL.md +45 -0
- package/core/skills/lint-and-validate/scripts/lint_runner.py +173 -0
- package/core/skills/lint-and-validate/scripts/type_coverage.py +173 -0
- package/core/skills/local-verification/SKILL.md +195 -0
- package/core/skills/mcp-builder/SKILL.md +176 -0
- package/core/skills/mobile-design/SKILL.md +394 -0
- package/core/skills/mobile-design/decision-trees.md +516 -0
- package/core/skills/mobile-design/mobile-backend.md +491 -0
- package/core/skills/mobile-design/mobile-color-system.md +420 -0
- package/core/skills/mobile-design/mobile-debugging.md +122 -0
- package/core/skills/mobile-design/mobile-design-thinking.md +357 -0
- package/core/skills/mobile-design/mobile-navigation.md +458 -0
- package/core/skills/mobile-design/mobile-performance.md +767 -0
- package/core/skills/mobile-design/mobile-testing.md +356 -0
- package/core/skills/mobile-design/mobile-typography.md +433 -0
- package/core/skills/mobile-design/platform-android.md +666 -0
- package/core/skills/mobile-design/platform-ios.md +561 -0
- package/core/skills/mobile-design/scripts/mobile_audit.py +670 -0
- package/core/skills/mobile-design/touch-psychology.md +537 -0
- package/core/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +312 -0
- package/core/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
- package/core/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
- package/core/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
- package/core/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
- package/core/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
- package/core/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
- package/core/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
- package/core/skills/nextjs-react-expert/SKILL.md +267 -0
- package/core/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
- package/core/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -0
- package/core/skills/nodejs-best-practices/SKILL.md +333 -0
- package/core/skills/notion-task-patterns/SKILL.md +2529 -0
- package/core/skills/page-specifications/SKILL.md +367 -0
- package/core/skills/parallel-agents/SKILL.md +175 -0
- package/core/skills/performance-profiling/SKILL.md +143 -0
- package/core/skills/performance-profiling/scripts/lighthouse_audit.py +76 -0
- package/core/skills/plan-writing/SKILL.md +190 -0
- package/core/skills/powershell-windows/SKILL.md +167 -0
- package/core/skills/project-foundation/SKILL.md +117 -0
- package/core/skills/project-setup/SKILL.md +141 -0
- package/core/skills/project-tracking-patterns/SKILL.md +357 -0
- package/core/skills/project-type-discovery/SKILL.md +239 -0
- package/core/skills/python-patterns/SKILL.md +441 -0
- package/core/skills/qa-test-generation/SKILL.md +156 -0
- package/core/skills/react-components/README.md +36 -0
- package/core/skills/react-components/SKILL.md +47 -0
- package/core/skills/react-components/examples/gold-standard-card.tsx +80 -0
- package/core/skills/react-components/package-lock.json +231 -0
- package/core/skills/react-components/package.json +16 -0
- package/core/skills/react-components/resources/architecture-checklist.md +15 -0
- package/core/skills/react-components/resources/component-template.tsx +37 -0
- package/core/skills/react-components/resources/stitch-api-reference.md +14 -0
- package/core/skills/react-components/resources/style-guide.json +27 -0
- package/core/skills/react-components/scripts/fetch-stitch.sh +30 -0
- package/core/skills/react-components/scripts/validate.js +68 -0
- package/core/skills/red-team-tactics/SKILL.md +199 -0
- package/core/skills/remotion/README.md +105 -0
- package/core/skills/remotion/SKILL.md +393 -0
- package/core/skills/remotion/examples/WalkthroughComposition.tsx +78 -0
- package/core/skills/remotion/examples/screens.json +56 -0
- package/core/skills/remotion/resources/composition-checklist.md +124 -0
- package/core/skills/remotion/resources/screen-slide-template.tsx +123 -0
- package/core/skills/remotion/scripts/download-stitch-asset.sh +38 -0
- package/core/skills/seo-fundamentals/SKILL.md +129 -0
- package/core/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
- package/core/skills/server-management/SKILL.md +161 -0
- package/core/skills/session-resilience/SKILL.md +199 -0
- package/core/skills/shadcn-ui/README.md +248 -0
- package/core/skills/shadcn-ui/SKILL.md +326 -0
- package/core/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/core/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/core/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/core/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/core/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/core/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/core/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/core/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/core/skills/state-machine/SKILL.md +264 -0
- package/core/skills/stitch-loop/README.md +54 -0
- package/core/skills/stitch-loop/SKILL.md +203 -0
- package/core/skills/stitch-loop/examples/SITE.md +73 -0
- package/core/skills/stitch-loop/examples/next-prompt.md +25 -0
- package/core/skills/stitch-loop/resources/baton-schema.md +61 -0
- package/core/skills/stitch-loop/resources/site-template.md +104 -0
- package/core/skills/systematic-debugging/SKILL.md +109 -0
- package/core/skills/tailwind-patterns/SKILL.md +284 -0
- package/core/skills/tdd-validation/SKILL.md +243 -0
- package/core/skills/tdd-workflow/SKILL.md +284 -0
- package/core/skills/testing-patterns/SKILL.md +196 -0
- package/core/skills/testing-patterns/scripts/test_runner.py +219 -0
- package/core/skills/ui-ux-discovery/SKILL.md +329 -0
- package/core/skills/ui-validation/SKILL.md +190 -0
- package/core/skills/ui-validation/scripts/ui_antipattern_check.py +317 -0
- package/core/skills/verification-gate/SKILL.md +205 -0
- package/core/skills/vulnerability-scanner/SKILL.md +276 -0
- package/core/skills/vulnerability-scanner/checklists.md +121 -0
- package/core/skills/vulnerability-scanner/scripts/security_scan.py +458 -0
- package/core/skills/web-design-guidelines/SKILL.md +57 -0
- package/core/skills/webapp-testing/SKILL.md +187 -0
- package/core/skills/webapp-testing/scripts/playwright_runner.py +173 -0
- package/core/templates/ARCHITECTURE.template.md +407 -0
- package/core/templates/project-resources.example.json +71 -0
- package/core/workflows/atomic.md +182 -0
- package/core/workflows/brainstorm.md +134 -0
- package/core/workflows/check-task.md +242 -0
- package/core/workflows/copy-collect.md +306 -0
- package/core/workflows/create-agent.md +33 -0
- package/core/workflows/create-skill.md +39 -0
- package/core/workflows/create-workflow.md +33 -0
- package/core/workflows/create.md +92 -0
- package/core/workflows/debug.md +186 -0
- package/core/workflows/demand.md +443 -0
- package/core/workflows/deploy.md +260 -0
- package/core/workflows/discovery.md +267 -0
- package/core/workflows/document.md +272 -0
- package/core/workflows/ds-components.md +296 -0
- package/core/workflows/ds-init.md +58 -0
- package/core/workflows/ds-refactor.md +245 -0
- package/core/workflows/ds-references.md +197 -0
- package/core/workflows/ds-styleguide.md +237 -0
- package/core/workflows/ds-token-diff.md +103 -0
- package/core/workflows/ds-tokens.md +317 -0
- package/core/workflows/ds-validate.md +309 -0
- package/core/workflows/execute.md +483 -0
- package/core/workflows/extract-template.md +278 -0
- package/core/workflows/fix-failed-tests.md +160 -0
- package/core/workflows/init-project.md +386 -0
- package/core/workflows/legacy-project.md +849 -0
- package/core/workflows/log.md +97 -0
- package/core/workflows/new-project.md +610 -0
- package/core/workflows/new-project.md.bak +3292 -0
- package/core/workflows/new-task.md +404 -0
- package/core/workflows/orchestrate.md +237 -0
- package/core/workflows/page-build.md +296 -0
- package/core/workflows/plan.md +89 -0
- package/core/workflows/prd.md +255 -0
- package/core/workflows/preview.md +81 -0
- package/core/workflows/review-page.md +304 -0
- package/core/workflows/status.md +86 -0
- package/core/workflows/stitch.md +226 -0
- package/core/workflows/task-complete.md +473 -0
- package/core/workflows/task-update.md +163 -0
- package/core/workflows/tdd.md +344 -0
- package/core/workflows/test.md +251 -0
- package/core/workflows/ui-ux-pro-max.md +437 -0
- package/core/workflows/ux-mobile-optimize.md +262 -0
- package/core/workflows/ux-mobile-validate.md +297 -0
- package/engine-files/GEMINI.md +69 -0
- package/package.json +47 -0
- package/runtime-adapters/antigravity.js +26 -0
- package/runtime-adapters/claude.js +57 -0
- package/runtime-adapters/codex.js +51 -0
- package/runtime-adapters/copilot.js +51 -0
- package/runtime-adapters/cursor.js +51 -0
- package/runtime-adapters/gemini-cli.js +30 -0
- package/runtime-adapters/opencode.js +51 -0
- package/runtime-adapters/windsurf.js +51 -0
package/bridge/bridge.py
ADDED
|
@@ -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()
|