From 9f9777f1a41743e359943454ce86a3e1a9dbf36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crist=C3=B3bal=20Ruz?= Date: Tue, 25 Aug 2026 23:01:53 -0600 Subject: [PATCH 01/27] refactor: simplify Project Brain to context core --- .../skills/project-brain-maintainer/SKILL.md | 89 - .../agents/openai.yaml | 4 - .eslint.devagent.config.mjs | 16 - .github/CODEOWNERS | 8 - .github/ISSUE_TEMPLATE/bug_report.md | 43 - .github/ISSUE_TEMPLATE/config.yml | 8 - .github/ISSUE_TEMPLATE/feature_request.md | 27 - .github/dependabot.yml | 19 - .github/pull_request_template.md | 22 - .github/workflows/ci.yml | 32 + .github/workflows/dependency-review.yml | 17 - .github/workflows/project-brain-ci.yml | 81 - .github/workflows/security-baseline.yml | 42 - .gitignore | 26 +- ACKNOWLEDGEMENTS.md | 26 - AGENTS.md | 30 + AI_CONTEXT/AGENTS.md | 16 - AI_CONTEXT/ANNOTATIONS.md | 3 - AI_CONTEXT/API_MAP.md | 13 - AI_CONTEXT/ARCHITECTURE.md | 12 - AI_CONTEXT/ARCHITECTURE_MAP.md | 54 - AI_CONTEXT/CONTEXT.md | 33 +- AI_CONTEXT/DECISIONS.md | 23 +- AI_CONTEXT/DEPENDENCY_GRAPH.md | 19 - AI_CONTEXT/ERRORS.md | 1 - AI_CONTEXT/LEARNINGS.md | 15 +- AI_CONTEXT/PROJECT_MODEL.md | 24 - AI_CONTEXT/RULES.md | 19 - AI_CONTEXT/STACK_PROFILE.md | 28 - AI_CONTEXT/STYLE_GUIDE.md | 3 - AI_CONTEXT/TASKS.md | 17 +- AI_REVIEW_START_HERE.md | 75 - CHANGELOG.md | 96 +- CITATION.cff | 12 - CODE_OF_CONDUCT.md | 27 - CONTRIBUTING.md | 55 - README.md | 508 +- SECURITY.md | 38 +- SUPPORT.md | 23 - agents/ai-support.ts | 152 - agents/architecture_agent/index.ts | 60 - agents/auth_agent/index.ts | 180 - agents/base-agent.ts | 187 - agents/catalog.ts | 158 - agents/dependency_agent/index.ts | 40 - agents/dev_agent/index.ts | 196 - agents/documentation_agent/index.ts | 119 - agents/infra_agent/index.ts | 171 - agents/legal_agent/index.ts | 38 - agents/observability_agent/index.ts | 50 - agents/optimization_agent/index.ts | 58 - agents/product_agent/index.ts | 43 - agents/product_owner_agent/index.ts | 43 - agents/prompts/architect.system.md | 26 - agents/prompts/coder.system.md | 26 - agents/prompts/documentation.system.md | 26 - agents/prompts/optimization.system.md | 26 - agents/prompts/qa.system.md | 26 - agents/prompts/ux-improvement.system.md | 50 - agents/prompts/ux.system.md | 47 - agents/qa_agent/index.ts | 73 - agents/security_agent/index.ts | 166 - agents/ux_agent/index.ts | 65 - agents/ux_improvement_agent/index.ts | 172 - analysis/api_scanner/index.ts | 58 - analysis/code_graph_v2/index.ts | 611 --- analysis/dependency_scanner/index.ts | 198 - analysis/impact_radius/index.ts | 490 -- analysis/infra_scanner/index.ts | 12 - analysis/metrics/metrics_collector.ts | 202 - analysis/repo_scanner/index.ts | 100 - analysis/repository_fact_graph/index.ts | 300 -- analysis/ux_task_generator/index.ts | 1433 ------ analysis/workspace_discovery/index.ts | 159 - bin/brain.mjs | 4 + cli/project-brain.ts | 1456 ------ cli/terminal-console.ts | 1017 ---- config/agents.json | 66 - config/models.json | 38 - core/ai_router/router.ts | 681 --- core/codebase_map/index.ts | 534 --- core/context_builder/index.ts | 52 - core/context_lite/index.ts | 3050 ------------ core/deepagents_swarm/index.ts | 864 ---- core/discovery_engine/index.ts | 110 - core/doctor/index.ts | 629 --- core/intent_router/index.ts | 224 - core/orchestrator/chief-agent.ts | 28 - core/orchestrator/main.ts | 1779 ------- core/orchestrator/scheduler.ts | 1 - core/reaction_engine/index.ts | 328 -- core/resume/index.ts | 346 -- core/scheduler/index.ts | 12 - core/security_audit/index.ts | 473 -- core/status/index.ts | 284 -- core/swarm_runtime/index.ts | 2232 --------- core/token_policy/index.ts | 75 - core/workflow_registry/index.ts | 343 -- docs/README.md | 49 - docs/agent-self-governance.md | 258 - docs/agents.md | 42 - docs/architecture.md | 45 - docs/assessments/agent-model-analysis.md | 241 - docs/assessments/final-score.md | 73 - docs/assessments/system-architecture-audit.md | 362 -- docs/assessments/weak-points.md | 164 - docs/backlog-commercial-hardening.md | 31 - docs/external-repository-integration.md | 44 - docs/first-analysis-5-min.md | 56 - docs/github-hardening.md | 70 - docs/installation.md | 89 - docs/output-contract.md | 85 - docs/product-blueprint.md | 755 --- docs/production-architecture-spec.md | 1034 ---- docs/reference-repo-analysis.md | 436 -- .../claude-mem-comparison.md | 167 - docs/release-checklist.md | 41 - docs/releases/0.2.0.md | 67 - docs/releases/0.2.2.md | 28 - docs/releases/0.2.3.md | 40 - docs/releases/0.2.4.md | 23 - docs/roadmap/evolution-architecture-v2.md | 779 --- docs/roadmap/evolution-plan.md | 440 -- docs/roadmap/fact-based-context-roadmap.md | 262 -- docs/roadmap/token-aware-orchestration.md | 158 - docs/self-improvement-framework.md | 736 --- docs/usage.md | 586 --- docs/user-test-script.md | 51 - eslint.config.mjs | 39 - governance/agent-council.ts | 140 - governance/agent-evaluator.ts | 64 - governance/agent-firewall.ts | 517 -- governance/agent-registry.ts | 58 - governance/agent-supervisor.ts | 129 - governance/autonomous-scheduler.ts | 48 - governance/message-center.ts | 118 - governance/proposal-consensus.ts | 92 - governance/self-governance-system.ts | 785 ---- governance/task-board.ts | 60 - integrations/ci/index.ts | 37 - integrations/git/index.ts | 7 - integrations/logs/index.ts | 29 - integrations/metrics/index.ts | 31 - integrations/ollama_adapter.ts | 142 - memory/annotations/index.ts | 102 - memory/context_registry/ecosystem_radar.ts | 771 --- memory/context_registry/index.ts | 550 --- memory/context_store/index.ts | 436 -- memory/executive_summary/index.ts | 212 - memory/fact_query/index.ts | 283 -- memory/knowledge_graph/index.ts | 406 -- memory/learning_store/index.ts | 129 - memory/learnings/index.ts | 94 - memory/memory_brief/index.ts | 246 - memory/preflight_facts/index.ts | 145 - memory/readiness/index.ts | 120 - memory/scope_store/index.ts | 311 -- memory/session_log/index.ts | 72 - operations/harness_audit/index.ts | 263 -- orchestrator/chief-agent.ts | 1 - orchestrator/main.ts | 1 - orchestrator/scheduler.ts | 1 - package-lock.json | 4186 +---------------- package.json | 92 +- planning/architecture_plan/index.ts | 361 -- planning/improvement_plan/index.ts | 227 - planning/project_seed/index.ts | 550 --- planning/runbook/index.ts | 135 - prompts/agent_prompts/README.md | 5 - .../architecture_review.prompt.md | 31 - .../context_bootstrap_master.md | 145 - .../frontend_analysis.prompt.md | 42 - .../performance_review.prompt.md | 31 - .../ux_improvement.prompt.md | 39 - reports/beta-readiness.md | 36 - reports/ci_status_report.md | 46 - reports/dev_architecture_analysis.md | 106 - reports/final-tag-gate-0.2.0.md | 323 -- reports/release-candidate-0.2.0.md | 115 - reports/templates/doctor.md | 32 - reports/templates/improvement_proposals.md | 7 - reports/templates/risk_report.md | 12 - reports/templates/weekly_system_report.md | 13 - reports/test_baseline_report.md | 57 - reports/test_coverage_initial.md | 50 - reports/validation-matrix.md | 51 - reports/validation-results.json | 31 - schema/context-contract.schema.json | 70 + schemas/preflight_facts.schema.json | 40 - schemas/repository_fact_graph.schema.json | 56 - schemas/validation_results.schema.json | 16 - scripts/README.md | 11 - scripts/check-commit-message.mjs | 38 - scripts/check-repo-safety.mjs | 224 - scripts/git-hooks/commit-msg | 7 - scripts/git-hooks/pre-commit | 8 - scripts/git-hooks/pre-push | 7 - scripts/install-hooks.mjs | 28 - scripts/self-analyze.sh | 8 - scripts/unused-exports-review.mjs | 90 - shared/fs-utils.ts | 184 - shared/logger.ts | 7 - shared/logger/logger.ts | 119 - shared/types.ts | 1312 ------ src/cli.mjs | 75 + src/context.mjs | 92 + src/contract.mjs | 29 + src/doctor.mjs | 944 ++++ src/fs.mjs | 110 + src/index.mjs | 5 + src/init.mjs | 39 + src/scanner.mjs | 535 +++ src/sync.mjs | 19 + src/templates.mjs | 17 + templates/AGENTS.md | 30 + templates/AI_CONTEXT/CONTEXT.md | 31 + templates/AI_CONTEXT/DECISIONS.md | 19 + templates/AI_CONTEXT/LEARNINGS.md | 18 + templates/AI_CONTEXT/TASKS.md | 18 + test-support/helpers.mjs | 20 + test/cli.test.mjs | 46 + test/context.test.mjs | 20 + test/doctor.test.mjs | 288 ++ test/init.test.mjs | 80 + test/scanner.test.mjs | 239 + test/scope.test.mjs | 51 + test/sync.test.mjs | 55 + test/templates.test.mjs | 147 + tests/fixtures/dev-agent-repo/package.json | 5 - tests/fixtures/dev-agent-repo/src/app.ts | 7 - .../fixtures/dev-agent-repo/src/repeated-a.ts | 6 - .../fixtures/dev-agent-repo/src/repeated-b.ts | 6 - tests/fixtures/dev-agent-repo/src/service.ts | 11 - tests/fixtures/dev-agent-repo/src/shared.ts | 5 - tests/fixtures/dev-agent-repo/tsconfig.json | 10 - .../CashCalculator/app.py | 8 - .../CashCalculator/requirements.txt | 1 - .../multi-repo-workspace/ERP/package.json | 7 - .../multi-repo-workspace/ERP/src/server.ts | 9 - .../FrontendPortal/package.json | 7 - .../FrontendPortal/src/app.tsx | 3 - .../project-brain/cli/main.ts | 3 - .../project-brain/package.json | 7 - .../AI_CONTEXT/ARCHITECTURE.md | 3 - .../AI_CONTEXT/vendor_notes.md | 3 - tests/fixtures/next-prisma-repo/README.md | 3 - tests/fixtures/next-prisma-repo/app/API.md | 29 - .../next-prisma-repo/app/ARCHITECTURE.md | 13 - .../next-prisma-repo/app/BUSINESS_RULES.md | 25 - tests/fixtures/next-prisma-repo/app/FLOWS.md | 14 - .../app/backups/vendor_snapshot.json | 4 - .../next-prisma-repo/app/docs/FEATURES/2fa.md | 5 - .../next-prisma-repo/app/docs/README.md | 3 - .../app/docs/technical/ACCESS_CONTROL.md | 4 - .../20260101000000_init/migration.sql | 4 - .../next-prisma-repo/app/prisma/schema.prisma | 25 - .../dashboard/vendor/page.tsx | 3 - .../app/src/app/(dashboard-vendor)/layout.tsx | 27 - .../app/src/app/(public)/page.tsx | 3 - .../src/app/(public)/u/[username]/page.tsx | 3 - .../app/src/app/api/auth/login/route.ts | 3 - .../src/app/api/notifications/list/route.ts | 3 - .../app/src/app/api/profile/public/route.ts | 7 - .../app/src/app/api/reports/route.ts | 3 - .../app/api/users/[username]/public/route.ts | 3 - .../app/components/vendor/VendorSidebar.tsx | 18 - .../app/components/vendor/vendorNavConfig.ts | 15 - .../app/src/components/ui/Button.tsx | 3 - .../app/src/lib/auth/permissions.ts | 3 - .../app/src/lib/auth/session.ts | 6 - .../app/src/services/publicProfileService.ts | 3 - .../docs/notifications/definition.md | 16 - .../docs/vendor-dashboard/README.md | 13 - .../docs/vendor-dashboard/decisions.md | 9 - tests/fixtures/next-prisma-repo/package.json | 11 - .../sample-repo/.github/workflows/test.yml | 8 - tests/fixtures/sample-repo/Dockerfile | 5 - tests/fixtures/sample-repo/README.md | 3 - tests/fixtures/sample-repo/openapi.yaml | 10 - tests/fixtures/sample-repo/package.json | 14 - tests/fixtures/sample-repo/schema.graphql | 3 - tests/fixtures/sample-repo/src/index.ts | 3 - tests/fixtures/sample-repo/tests/app.test.ts | 9 - tests/helpers.ts | 19 - tests/integration/ai-agent-reporting.test.ts | 59 - tests/integration/architecture-plan.test.ts | 51 - tests/integration/ask-intent-routing.test.ts | 317 -- tests/integration/code-graph-v2.test.ts | 77 - tests/integration/codebase-map.test.ts | 52 - tests/integration/context-annotations.test.ts | 42 - tests/integration/context-lite.test.ts | 206 - tests/integration/context-registry.test.ts | 62 - tests/integration/dev-agent-analysis.test.ts | 34 - .../dev-agent-patch-proposals.test.ts | 123 - tests/integration/discovery-engine.test.ts | 84 - .../discovery-fixture-filtering.test.ts | 61 - tests/integration/ecosystem-radar.test.ts | 144 - tests/integration/fact-query.test.ts | 170 - .../integration/frontend-ux-targeting.test.ts | 175 - tests/integration/impact-radius.test.ts | 127 - tests/integration/multi-repo-analysis.test.ts | 74 - tests/integration/orchestrator-cycle.test.ts | 121 - tests/integration/plan-improvements.test.ts | 48 - tests/integration/project-seed.test.ts | 112 - tests/integration/report-quality.test.ts | 110 - tests/integration/runbook.test.ts | 44 - tests/integration/security-audit.test.ts | 49 - tests/integration/swarm-runtime.test.ts | 1158 ----- .../ux-agent-operational-focus.test.ts | 80 - .../ux-implementation-tasks.test.ts | 123 - tests/smoke/cli-workflows.test.ts | 145 - tests/unit/agent-behavior.test.ts | 185 - tests/unit/agent-registry.test.ts | 32 - tests/unit/ai-router.test.ts | 172 - tests/unit/cli-command-parsing.test.ts | 305 -- tests/unit/deepagents-swarm.test.ts | 166 - tests/unit/doctor.test.ts | 136 - tests/unit/executive-summary.test.ts | 154 - tests/unit/fs-utils.test.ts | 53 - tests/unit/intent-router.test.ts | 18 - tests/unit/learning-store.test.ts | 80 - tests/unit/memory-store.test.ts | 192 - tests/unit/ollama-adapter.test.ts | 112 - .../unit/orchestrator-initialization.test.ts | 27 - tests/unit/preflight-facts.test.ts | 167 - tests/unit/repo-safety.test.ts | 61 - tests/unit/resume.test.ts | 134 - tests/unit/session-log.test.ts | 108 - tests/unit/start-workflow-registry.test.ts | 38 - tests/unit/status.test.ts | 67 - tests/unit/swarm-engine-routing.test.ts | 64 - tests/unit/swarm-runtime-budgeting.test.ts | 38 - tests/unit/terminal-console.test.ts | 46 - tests/unit/token-policy.test.ts | 30 - tests/unit/workspace-discovery.test.ts | 26 - tools/dev_analysis_tools/contracts.ts | 204 - tools/dev_analysis_tools/index.ts | 352 -- tools/dev_analysis_tools/runners.ts | 203 - tools/dev_analysis_tools/snapshots.ts | 110 - tools/git_tools/index.ts | 43 - tools/infra_tools/index.ts | 59 - tools/openapi_tools/index.ts | 29 - tools/patch_proposal_tools/index.ts | 458 -- tsconfig.json | 31 - vitest.config.ts | 9 - 345 files changed, 3237 insertions(+), 52493 deletions(-) delete mode 100644 .agents/skills/project-brain-maintainer/SKILL.md delete mode 100644 .agents/skills/project-brain-maintainer/agents/openai.yaml delete mode 100644 .eslint.devagent.config.mjs delete mode 100644 .github/CODEOWNERS delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/dependabot.yml delete mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/dependency-review.yml delete mode 100644 .github/workflows/project-brain-ci.yml delete mode 100644 .github/workflows/security-baseline.yml delete mode 100644 ACKNOWLEDGEMENTS.md create mode 100644 AGENTS.md delete mode 100644 AI_CONTEXT/AGENTS.md delete mode 100644 AI_CONTEXT/ANNOTATIONS.md delete mode 100644 AI_CONTEXT/API_MAP.md delete mode 100644 AI_CONTEXT/ARCHITECTURE.md delete mode 100644 AI_CONTEXT/ARCHITECTURE_MAP.md delete mode 100644 AI_CONTEXT/DEPENDENCY_GRAPH.md delete mode 100644 AI_CONTEXT/ERRORS.md delete mode 100644 AI_CONTEXT/PROJECT_MODEL.md delete mode 100644 AI_CONTEXT/RULES.md delete mode 100644 AI_CONTEXT/STACK_PROFILE.md delete mode 100644 AI_CONTEXT/STYLE_GUIDE.md delete mode 100644 AI_REVIEW_START_HERE.md delete mode 100644 CITATION.cff delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md delete mode 100644 SUPPORT.md delete mode 100644 agents/ai-support.ts delete mode 100644 agents/architecture_agent/index.ts delete mode 100644 agents/auth_agent/index.ts delete mode 100644 agents/base-agent.ts delete mode 100644 agents/catalog.ts delete mode 100644 agents/dependency_agent/index.ts delete mode 100644 agents/dev_agent/index.ts delete mode 100644 agents/documentation_agent/index.ts delete mode 100644 agents/infra_agent/index.ts delete mode 100644 agents/legal_agent/index.ts delete mode 100644 agents/observability_agent/index.ts delete mode 100644 agents/optimization_agent/index.ts delete mode 100644 agents/product_agent/index.ts delete mode 100644 agents/product_owner_agent/index.ts delete mode 100644 agents/prompts/architect.system.md delete mode 100644 agents/prompts/coder.system.md delete mode 100644 agents/prompts/documentation.system.md delete mode 100644 agents/prompts/optimization.system.md delete mode 100644 agents/prompts/qa.system.md delete mode 100644 agents/prompts/ux-improvement.system.md delete mode 100644 agents/prompts/ux.system.md delete mode 100644 agents/qa_agent/index.ts delete mode 100644 agents/security_agent/index.ts delete mode 100644 agents/ux_agent/index.ts delete mode 100644 agents/ux_improvement_agent/index.ts delete mode 100644 analysis/api_scanner/index.ts delete mode 100644 analysis/code_graph_v2/index.ts delete mode 100644 analysis/dependency_scanner/index.ts delete mode 100644 analysis/impact_radius/index.ts delete mode 100644 analysis/infra_scanner/index.ts delete mode 100644 analysis/metrics/metrics_collector.ts delete mode 100644 analysis/repo_scanner/index.ts delete mode 100644 analysis/repository_fact_graph/index.ts delete mode 100644 analysis/ux_task_generator/index.ts delete mode 100644 analysis/workspace_discovery/index.ts create mode 100755 bin/brain.mjs delete mode 100644 cli/project-brain.ts delete mode 100644 cli/terminal-console.ts delete mode 100644 config/agents.json delete mode 100644 config/models.json delete mode 100644 core/ai_router/router.ts delete mode 100644 core/codebase_map/index.ts delete mode 100644 core/context_builder/index.ts delete mode 100644 core/context_lite/index.ts delete mode 100644 core/deepagents_swarm/index.ts delete mode 100644 core/discovery_engine/index.ts delete mode 100644 core/doctor/index.ts delete mode 100644 core/intent_router/index.ts delete mode 100644 core/orchestrator/chief-agent.ts delete mode 100644 core/orchestrator/main.ts delete mode 100644 core/orchestrator/scheduler.ts delete mode 100644 core/reaction_engine/index.ts delete mode 100644 core/resume/index.ts delete mode 100644 core/scheduler/index.ts delete mode 100644 core/security_audit/index.ts delete mode 100644 core/status/index.ts delete mode 100644 core/swarm_runtime/index.ts delete mode 100644 core/token_policy/index.ts delete mode 100644 core/workflow_registry/index.ts delete mode 100644 docs/README.md delete mode 100644 docs/agent-self-governance.md delete mode 100644 docs/agents.md delete mode 100644 docs/architecture.md delete mode 100644 docs/assessments/agent-model-analysis.md delete mode 100644 docs/assessments/final-score.md delete mode 100644 docs/assessments/system-architecture-audit.md delete mode 100644 docs/assessments/weak-points.md delete mode 100644 docs/backlog-commercial-hardening.md delete mode 100644 docs/external-repository-integration.md delete mode 100644 docs/first-analysis-5-min.md delete mode 100644 docs/github-hardening.md delete mode 100644 docs/installation.md delete mode 100644 docs/output-contract.md delete mode 100644 docs/product-blueprint.md delete mode 100644 docs/production-architecture-spec.md delete mode 100644 docs/reference-repo-analysis.md delete mode 100644 docs/reference-repo-analysis/claude-mem-comparison.md delete mode 100644 docs/release-checklist.md delete mode 100644 docs/releases/0.2.0.md delete mode 100644 docs/releases/0.2.2.md delete mode 100644 docs/releases/0.2.3.md delete mode 100644 docs/releases/0.2.4.md delete mode 100644 docs/roadmap/evolution-architecture-v2.md delete mode 100644 docs/roadmap/evolution-plan.md delete mode 100644 docs/roadmap/fact-based-context-roadmap.md delete mode 100644 docs/roadmap/token-aware-orchestration.md delete mode 100644 docs/self-improvement-framework.md delete mode 100644 docs/usage.md delete mode 100644 docs/user-test-script.md delete mode 100644 eslint.config.mjs delete mode 100644 governance/agent-council.ts delete mode 100644 governance/agent-evaluator.ts delete mode 100644 governance/agent-firewall.ts delete mode 100644 governance/agent-registry.ts delete mode 100644 governance/agent-supervisor.ts delete mode 100644 governance/autonomous-scheduler.ts delete mode 100644 governance/message-center.ts delete mode 100644 governance/proposal-consensus.ts delete mode 100644 governance/self-governance-system.ts delete mode 100644 governance/task-board.ts delete mode 100644 integrations/ci/index.ts delete mode 100644 integrations/git/index.ts delete mode 100644 integrations/logs/index.ts delete mode 100644 integrations/metrics/index.ts delete mode 100644 integrations/ollama_adapter.ts delete mode 100644 memory/annotations/index.ts delete mode 100644 memory/context_registry/ecosystem_radar.ts delete mode 100644 memory/context_registry/index.ts delete mode 100644 memory/context_store/index.ts delete mode 100644 memory/executive_summary/index.ts delete mode 100644 memory/fact_query/index.ts delete mode 100644 memory/knowledge_graph/index.ts delete mode 100644 memory/learning_store/index.ts delete mode 100644 memory/learnings/index.ts delete mode 100644 memory/memory_brief/index.ts delete mode 100644 memory/preflight_facts/index.ts delete mode 100644 memory/readiness/index.ts delete mode 100644 memory/scope_store/index.ts delete mode 100644 memory/session_log/index.ts delete mode 100644 operations/harness_audit/index.ts delete mode 100644 orchestrator/chief-agent.ts delete mode 100644 orchestrator/main.ts delete mode 100644 orchestrator/scheduler.ts delete mode 100644 planning/architecture_plan/index.ts delete mode 100644 planning/improvement_plan/index.ts delete mode 100644 planning/project_seed/index.ts delete mode 100644 planning/runbook/index.ts delete mode 100644 prompts/agent_prompts/README.md delete mode 100644 prompts/context_templates/architecture_review.prompt.md delete mode 100644 prompts/context_templates/context_bootstrap_master.md delete mode 100644 prompts/context_templates/frontend_analysis.prompt.md delete mode 100644 prompts/context_templates/performance_review.prompt.md delete mode 100644 prompts/context_templates/ux_improvement.prompt.md delete mode 100644 reports/beta-readiness.md delete mode 100644 reports/ci_status_report.md delete mode 100644 reports/dev_architecture_analysis.md delete mode 100644 reports/final-tag-gate-0.2.0.md delete mode 100644 reports/release-candidate-0.2.0.md delete mode 100644 reports/templates/doctor.md delete mode 100644 reports/templates/improvement_proposals.md delete mode 100644 reports/templates/risk_report.md delete mode 100644 reports/templates/weekly_system_report.md delete mode 100644 reports/test_baseline_report.md delete mode 100644 reports/test_coverage_initial.md delete mode 100644 reports/validation-matrix.md delete mode 100644 reports/validation-results.json create mode 100644 schema/context-contract.schema.json delete mode 100644 schemas/preflight_facts.schema.json delete mode 100644 schemas/repository_fact_graph.schema.json delete mode 100644 schemas/validation_results.schema.json delete mode 100644 scripts/README.md delete mode 100644 scripts/check-commit-message.mjs delete mode 100644 scripts/check-repo-safety.mjs delete mode 100755 scripts/git-hooks/commit-msg delete mode 100755 scripts/git-hooks/pre-commit delete mode 100755 scripts/git-hooks/pre-push delete mode 100644 scripts/install-hooks.mjs delete mode 100755 scripts/self-analyze.sh delete mode 100755 scripts/unused-exports-review.mjs delete mode 100644 shared/fs-utils.ts delete mode 100644 shared/logger.ts delete mode 100644 shared/logger/logger.ts delete mode 100644 shared/types.ts create mode 100644 src/cli.mjs create mode 100644 src/context.mjs create mode 100644 src/contract.mjs create mode 100644 src/doctor.mjs create mode 100644 src/fs.mjs create mode 100644 src/index.mjs create mode 100644 src/init.mjs create mode 100644 src/scanner.mjs create mode 100644 src/sync.mjs create mode 100644 src/templates.mjs create mode 100644 templates/AGENTS.md create mode 100644 templates/AI_CONTEXT/CONTEXT.md create mode 100644 templates/AI_CONTEXT/DECISIONS.md create mode 100644 templates/AI_CONTEXT/LEARNINGS.md create mode 100644 templates/AI_CONTEXT/TASKS.md create mode 100644 test-support/helpers.mjs create mode 100644 test/cli.test.mjs create mode 100644 test/context.test.mjs create mode 100644 test/doctor.test.mjs create mode 100644 test/init.test.mjs create mode 100644 test/scanner.test.mjs create mode 100644 test/scope.test.mjs create mode 100644 test/sync.test.mjs create mode 100644 test/templates.test.mjs delete mode 100644 tests/fixtures/dev-agent-repo/package.json delete mode 100644 tests/fixtures/dev-agent-repo/src/app.ts delete mode 100644 tests/fixtures/dev-agent-repo/src/repeated-a.ts delete mode 100644 tests/fixtures/dev-agent-repo/src/repeated-b.ts delete mode 100644 tests/fixtures/dev-agent-repo/src/service.ts delete mode 100644 tests/fixtures/dev-agent-repo/src/shared.ts delete mode 100644 tests/fixtures/dev-agent-repo/tsconfig.json delete mode 100644 tests/fixtures/multi-repo-workspace/CashCalculator/app.py delete mode 100644 tests/fixtures/multi-repo-workspace/CashCalculator/requirements.txt delete mode 100644 tests/fixtures/multi-repo-workspace/ERP/package.json delete mode 100644 tests/fixtures/multi-repo-workspace/ERP/src/server.ts delete mode 100644 tests/fixtures/multi-repo-workspace/FrontendPortal/package.json delete mode 100644 tests/fixtures/multi-repo-workspace/FrontendPortal/src/app.tsx delete mode 100644 tests/fixtures/multi-repo-workspace/project-brain/cli/main.ts delete mode 100644 tests/fixtures/multi-repo-workspace/project-brain/package.json delete mode 100644 tests/fixtures/next-prisma-repo/AI_CONTEXT/ARCHITECTURE.md delete mode 100644 tests/fixtures/next-prisma-repo/AI_CONTEXT/vendor_notes.md delete mode 100644 tests/fixtures/next-prisma-repo/README.md delete mode 100644 tests/fixtures/next-prisma-repo/app/API.md delete mode 100644 tests/fixtures/next-prisma-repo/app/ARCHITECTURE.md delete mode 100644 tests/fixtures/next-prisma-repo/app/BUSINESS_RULES.md delete mode 100644 tests/fixtures/next-prisma-repo/app/FLOWS.md delete mode 100644 tests/fixtures/next-prisma-repo/app/backups/vendor_snapshot.json delete mode 100644 tests/fixtures/next-prisma-repo/app/docs/FEATURES/2fa.md delete mode 100644 tests/fixtures/next-prisma-repo/app/docs/README.md delete mode 100644 tests/fixtures/next-prisma-repo/app/docs/technical/ACCESS_CONTROL.md delete mode 100644 tests/fixtures/next-prisma-repo/app/prisma/migrations/20260101000000_init/migration.sql delete mode 100644 tests/fixtures/next-prisma-repo/app/prisma/schema.prisma delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/dashboard/vendor/page.tsx delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/layout.tsx delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/(public)/page.tsx delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/(public)/u/[username]/page.tsx delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/api/auth/login/route.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/api/notifications/list/route.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/api/profile/public/route.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/api/reports/route.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/api/users/[username]/public/route.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/components/vendor/VendorSidebar.tsx delete mode 100644 tests/fixtures/next-prisma-repo/app/src/app/components/vendor/vendorNavConfig.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/components/ui/Button.tsx delete mode 100644 tests/fixtures/next-prisma-repo/app/src/lib/auth/permissions.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/lib/auth/session.ts delete mode 100644 tests/fixtures/next-prisma-repo/app/src/services/publicProfileService.ts delete mode 100644 tests/fixtures/next-prisma-repo/docs/notifications/definition.md delete mode 100644 tests/fixtures/next-prisma-repo/docs/vendor-dashboard/README.md delete mode 100644 tests/fixtures/next-prisma-repo/docs/vendor-dashboard/decisions.md delete mode 100644 tests/fixtures/next-prisma-repo/package.json delete mode 100644 tests/fixtures/sample-repo/.github/workflows/test.yml delete mode 100644 tests/fixtures/sample-repo/Dockerfile delete mode 100644 tests/fixtures/sample-repo/README.md delete mode 100644 tests/fixtures/sample-repo/openapi.yaml delete mode 100644 tests/fixtures/sample-repo/package.json delete mode 100644 tests/fixtures/sample-repo/schema.graphql delete mode 100644 tests/fixtures/sample-repo/src/index.ts delete mode 100644 tests/fixtures/sample-repo/tests/app.test.ts delete mode 100644 tests/helpers.ts delete mode 100644 tests/integration/ai-agent-reporting.test.ts delete mode 100644 tests/integration/architecture-plan.test.ts delete mode 100644 tests/integration/ask-intent-routing.test.ts delete mode 100644 tests/integration/code-graph-v2.test.ts delete mode 100644 tests/integration/codebase-map.test.ts delete mode 100644 tests/integration/context-annotations.test.ts delete mode 100644 tests/integration/context-lite.test.ts delete mode 100644 tests/integration/context-registry.test.ts delete mode 100644 tests/integration/dev-agent-analysis.test.ts delete mode 100644 tests/integration/dev-agent-patch-proposals.test.ts delete mode 100644 tests/integration/discovery-engine.test.ts delete mode 100644 tests/integration/discovery-fixture-filtering.test.ts delete mode 100644 tests/integration/ecosystem-radar.test.ts delete mode 100644 tests/integration/fact-query.test.ts delete mode 100644 tests/integration/frontend-ux-targeting.test.ts delete mode 100644 tests/integration/impact-radius.test.ts delete mode 100644 tests/integration/multi-repo-analysis.test.ts delete mode 100644 tests/integration/orchestrator-cycle.test.ts delete mode 100644 tests/integration/plan-improvements.test.ts delete mode 100644 tests/integration/project-seed.test.ts delete mode 100644 tests/integration/report-quality.test.ts delete mode 100644 tests/integration/runbook.test.ts delete mode 100644 tests/integration/security-audit.test.ts delete mode 100644 tests/integration/swarm-runtime.test.ts delete mode 100644 tests/integration/ux-agent-operational-focus.test.ts delete mode 100644 tests/integration/ux-implementation-tasks.test.ts delete mode 100644 tests/smoke/cli-workflows.test.ts delete mode 100644 tests/unit/agent-behavior.test.ts delete mode 100644 tests/unit/agent-registry.test.ts delete mode 100644 tests/unit/ai-router.test.ts delete mode 100644 tests/unit/cli-command-parsing.test.ts delete mode 100644 tests/unit/deepagents-swarm.test.ts delete mode 100644 tests/unit/doctor.test.ts delete mode 100644 tests/unit/executive-summary.test.ts delete mode 100644 tests/unit/fs-utils.test.ts delete mode 100644 tests/unit/intent-router.test.ts delete mode 100644 tests/unit/learning-store.test.ts delete mode 100644 tests/unit/memory-store.test.ts delete mode 100644 tests/unit/ollama-adapter.test.ts delete mode 100644 tests/unit/orchestrator-initialization.test.ts delete mode 100644 tests/unit/preflight-facts.test.ts delete mode 100644 tests/unit/repo-safety.test.ts delete mode 100644 tests/unit/resume.test.ts delete mode 100644 tests/unit/session-log.test.ts delete mode 100644 tests/unit/start-workflow-registry.test.ts delete mode 100644 tests/unit/status.test.ts delete mode 100644 tests/unit/swarm-engine-routing.test.ts delete mode 100644 tests/unit/swarm-runtime-budgeting.test.ts delete mode 100644 tests/unit/terminal-console.test.ts delete mode 100644 tests/unit/token-policy.test.ts delete mode 100644 tests/unit/workspace-discovery.test.ts delete mode 100644 tools/dev_analysis_tools/contracts.ts delete mode 100644 tools/dev_analysis_tools/index.ts delete mode 100644 tools/dev_analysis_tools/runners.ts delete mode 100644 tools/dev_analysis_tools/snapshots.ts delete mode 100644 tools/git_tools/index.ts delete mode 100644 tools/infra_tools/index.ts delete mode 100644 tools/openapi_tools/index.ts delete mode 100644 tools/patch_proposal_tools/index.ts delete mode 100644 tsconfig.json delete mode 100644 vitest.config.ts diff --git a/.agents/skills/project-brain-maintainer/SKILL.md b/.agents/skills/project-brain-maintainer/SKILL.md deleted file mode 100644 index bf18fb3..0000000 --- a/.agents/skills/project-brain-maintainer/SKILL.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: project-brain-maintainer -description: Use when working inside the project-brain repository to improve, debug, or extend the bounded swarm runtime, deepagents engine, AI router, CLI flows, memory artifacts, governance pipeline, or repo-specific maintenance workflows. Applies to tasks about token usage, task scheduling, provider routing, resume/ask continuity, review-only safeguards, and runtime verification in this repo. ---- - -# Project Brain Maintainer - -Use this skill only inside the `project-brain` repository. - -## Core invariants - -- Keep `project-brain` analysis-first and review-only by default. -- Preserve the governed pipeline: discovery -> context -> firewall -> governance -> memory -> reports. -- Treat `deepagents` as experimental; do not replace the bounded swarm wholesale unless the user explicitly asks for that direction. -- Prefer cheap/local execution for broad scans and reserve expensive/cloud execution for planning, synthesis, or clearly accuracy-sensitive work. - -## Primary files - -- CLI entrypoint: `cli/project-brain.ts` -- Main orchestrator: `core/orchestrator/main.ts` -- Bounded swarm runtime: `core/swarm_runtime/index.ts` -- Experimental deepagents engine: `core/deepagents_swarm/index.ts` -- AI router: `core/ai_router/router.ts` -- Resume and status recovery: `core/resume/index.ts`, `core/status/index.ts` -- Runtime memory and artifacts: `AI_CONTEXT/`, `memory/`, `reports/` -- Governance layer: `governance/`, `docs/agent-self-governance.md` - -## Task routing - -### Swarm runtime work - -If the task touches swarm economics, scope chunking, retries, cache, learning, or delegation: - -1. Read `core/swarm_runtime/index.ts`. -2. Read `shared/types.ts`. -3. Read `tests/integration/swarm-runtime.test.ts`. -4. Preserve bounded execution, explicit budgets, and persisted artifacts under `memory/swarm/` and `reports/swarm_run.md`. - -### Provider or model routing work - -If the task touches provider choice, local/cloud behavior, or cost/residency: - -1. Read `core/ai_router/router.ts`. -2. Inspect where `selectModel()` and `ask()` diverge. -3. Keep fallback behavior explicit and observable. -4. Verify that cloud-capable logic does not silently break local-only runs. - -### Ask or resume continuity work - -If the task touches user continuation, workflow reuse, or saved state: - -1. Read `core/orchestrator/main.ts`. -2. Read `core/resume/index.ts`. -3. Read `core/intent_router/index.ts`. -4. Read `tests/integration/ask-intent-routing.test.ts`, `tests/unit/resume.test.ts`, and `tests/unit/status.test.ts`. - -### Governance or safety work - -If the task touches policy packs, approvals, or agent outputs: - -1. Read `governance/`. -2. Read `docs/agent-self-governance.md`. -3. Preserve human-review gates and the firewall boundary. -4. Do not let optimization bypass governance. - -## Working rules - -- Prefer narrow fixes over broad rewrites. -- Reuse existing artifact paths and report formats instead of creating new top-level outputs. -- When adding memory, prefer compact structured JSON for runtime state and keep human-readable Markdown reports aligned with it. -- When changing planner or worker prompts, update the closest integration tests. -- When changing CLI behavior, keep current commands stable: `ask`, `resume`, `swarm`, `review-delta`, `firewall`, `status`. - -## Verification - -Run the smallest relevant set first: - -- `npm run typecheck` -- `npm test -- --run tests/integration/swarm-runtime.test.ts` -- `npm test -- --run tests/integration/ask-intent-routing.test.ts tests/unit/resume.test.ts tests/unit/status.test.ts` -- `npm run build` - -Use broader `npm test` only when the change spans multiple subsystems. - -## Avoid - -- Do not convert `project-brain` into an autonomous code-writing engine without an explicit product change. -- Do not import external runtimes wholesale when extracting a smaller pattern is enough. -- Do not add heavy memory infrastructure or distributed-worker complexity unless the task explicitly justifies the operational cost. diff --git a/.agents/skills/project-brain-maintainer/agents/openai.yaml b/.agents/skills/project-brain-maintainer/agents/openai.yaml deleted file mode 100644 index 6512019..0000000 --- a/.agents/skills/project-brain-maintainer/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Project Brain Maintainer" - short_description: "Maintain and evolve project-brain safely" - default_prompt: "Use $project-brain-maintainer to improve this repo without breaking governance, resume flows, or bounded swarm behavior." diff --git a/.eslint.devagent.config.mjs b/.eslint.devagent.config.mjs deleted file mode 100644 index dae51e9..0000000 --- a/.eslint.devagent.config.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import tsParser from "@typescript-eslint/parser"; - -export default [ - { - files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"], - languageOptions: { - parser: tsParser, - ecmaVersion: "latest", - sourceType: "module" - }, - rules: { - complexity: ["warn", 12], - "max-lines": ["warn", { max: 500, skipBlankLines: true, skipComments: true }] - } - } -]; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 5af05e2..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,8 +0,0 @@ -* @ruzer - -/.github/ @ruzer -/cli/ @ruzer -/core/ @ruzer -/governance/ @ruzer -/integrations/ @ruzer -/scripts/ @ruzer diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c02314a..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: Bug report -about: Report a reproducible problem in project-brain -title: "[bug] " -labels: bug -assignees: "" ---- - -## Summary - -Describe the problem in one or two sentences. - -## Command - -```bash -# Paste the exact command -``` - -## Target context - -- Repository type: -- Output path: -- Trigger or mode: - -## Expected behavior - -What did you expect to happen? - -## Actual behavior - -What happened instead? - -## Evidence - -- Relevant report path: -- Relevant log snippet: -- Screenshots or extra notes: - -## Environment - -- OS: -- Node version: -- project-brain version or commit: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 4aff5e3..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Security report - url: https://github.com/ruzer/project-brain/security/policy - about: Report vulnerabilities privately instead of opening a public issue. - - name: Support and usage - url: https://github.com/ruzer/project-brain/blob/main/SUPPORT.md - about: Read support guidance before opening a new issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 1aa0f97..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Feature request -about: Propose a new capability or workflow improvement -title: "[feature] " -labels: enhancement -assignees: "" ---- - -## Problem - -What workflow is blocked or too expensive today? - -## Proposed capability - -Describe the command, report, or behavior you want. - -## Why this belongs in project-brain - -Explain why this fits the product instead of a one-off script or downstream tool. - -## Acceptance signal - -How would we know the feature is good enough? - -## Related inspiration - -Mention any upstream project, paper, or tool that influenced this idea. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 6bc1aad..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: 2 -updates: - - package-ecosystem: npm - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 - labels: - - dependencies - - security - - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 - labels: - - dependencies - - github-actions diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 5fd16c9..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,22 +0,0 @@ -## Summary - -Describe the change and the problem it solves. - -## Validation - -- [ ] `npm run lint` -- [ ] `npm run typecheck` -- [ ] `npm run build` -- [ ] Relevant `vitest` coverage was executed -- [ ] `npm run security:repo` passed if touched config, scripts, workflows, or auth-related code - -## Scope check - -- [ ] The change remains non-destructive for analyzed target repositories -- [ ] Docs were updated if CLI behavior or generated artifacts changed -- [ ] Upstream inspiration or adapted content was credited when relevant -- [ ] No generated/local-only files or secrets were added - -## Notes - -Add follow-ups, tradeoffs, or review guidance here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b7625ab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - codex/project-brain-lite + +permissions: + contents: read + +jobs: + check: + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + node-version: 20 + - os: ubuntu-latest + node-version: 22 + - os: windows-latest + node-version: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - run: npm ci + - run: npm run check diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index 47b7b9c..0000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: dependency-review - -on: - pull_request: - -permissions: - contents: read - pull-requests: read - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: Dependency review - uses: actions/dependency-review-action@v4 - with: - fail-on-severity: high diff --git a/.github/workflows/project-brain-ci.yml b/.github/workflows/project-brain-ci.yml deleted file mode 100644 index ed3f15b..0000000 --- a/.github/workflows/project-brain-ci.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: project-brain-ci - -on: - push: - branches: - - main - pull_request: - -concurrency: - group: project-brain-ci-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - quality-gates: - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - node-version: - - 20 - - 22 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint - - - name: Build - run: npm run build - - - name: Typecheck - run: npm run typecheck - - - name: Run tests - run: npm run test - - - name: Run smoke tests - run: npm run test:smoke - - - name: High severity audit - run: npm audit --audit-level=high - - - name: Repository safety scan - run: npm run security:repo - - - name: Generate reports - if: always() - run: | - mkdir -p ci-artifacts - for file in \ - reports/ci_status_report.md \ - reports/test_baseline_report.md \ - reports/test_coverage_initial.md - do - if [ -f "$file" ]; then - cp "$file" ci-artifacts/ - fi - done - - - name: Upload reports - if: always() - uses: actions/upload-artifact@v4 - with: - name: project-brain-ci-reports-node-${{ matrix.node-version }} - path: ci-artifacts/ - if-no-files-found: ignore diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml deleted file mode 100644 index 50fb3df..0000000 --- a/.github/workflows/security-baseline.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: security-baseline - -on: - pull_request: - push: - branches: - - main - schedule: - - cron: "0 15 * * 1" - -permissions: - contents: read - -jobs: - security-baseline: - runs-on: ubuntu-latest - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - node-version: - - 20 - - 22 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Repository safety scan - run: npm run security:repo - - - name: Production dependency audit - run: npm run security:audit diff --git a/.gitignore b/.gitignore index 337ea56..810bdad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,10 @@ node_modules/ -pb-output/ -project-brain/pb-output/ -.env -.env.local -logs/ -tmp/ -.tmp/ -.tmp-ai-fixture/ +coverage/ dist/ build/ -.cache/ -cache/ -coverage/ .DS_Store -sample-output/ +.brain/ BRAIN/ -.project-brain-local/ - -# Local agent/runtime state -.claude/ - -# Generated runtime diagnostics -AI_CONTEXT/doctor/ -reports/doctor.md -.project-brain/runtime/ +graphify-out/ +.graphify/ +.obsidian/ diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md deleted file mode 100644 index 7b26a28..0000000 --- a/ACKNOWLEDGEMENTS.md +++ /dev/null @@ -1,26 +0,0 @@ -# Acknowledgements - -`project-brain` is an independent project. Some ideas in the product direction, workflow design, and CLI ergonomics were informed by other open source projects, but the implementation in this repository is specific to `project-brain`. - -## Upstream influences - -- [`gsd-build/get-shit-done`](https://github.com/gsd-build/get-shit-done) - Inspiration for structured context engineering, guided repository workflows, and operator-facing repo onboarding. License: MIT. - -- [`andrewyng/context-hub`](https://github.com/andrewyng/context-hub) - Inspiration for persistent annotations, curated context artifacts, and fetch-oriented context workflows. License: MIT. - -- [`tirth8205/code-review-graph`](https://github.com/tirth8205/code-review-graph) - Inspiration for local code graph ideas, impact radius analysis, and minimal review-context generation. License: MIT. - -- [`nyldn/claude-octopus`](https://github.com/nyldn/claude-octopus) - Inspiration for multi-agent orchestration patterns, explicit workflow stages, and consensus-style governance gates. License: MIT. - -- [`zai-org/GLM-OCR`](https://github.com/zai-org/GLM-OCR) - Reference point for future document-ingestion and OCR-oriented context pipelines. License: Apache-2.0. - -## Attribution policy - -- Ideas, workflows, and product patterns influenced by other projects are credited in this file and may also be referenced from the README. -- If source code, templates, or substantial file content are ever adapted from another project, the original license and attribution notices must be preserved in the adapted files and documented in the introducing commit or pull request. -- No affiliation, sponsorship, or endorsement by the projects above is implied. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a77b9bb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# Instrucciones del repositorio + +Empieza por [AI_CONTEXT/CONTEXT.md](AI_CONTEXT/CONTEXT.md). Consulta decisiones, tareas o aprendizajes solo cuando sean relevantes para el trabajo actual. + +## Contexto mínimo + +- [Contexto verificable](AI_CONTEXT/CONTEXT.md): propósito, stack e inventario comprobable. +- [Decisiones](AI_CONTEXT/DECISIONS.md): decisiones vigentes y su motivo. +- [Tareas](AI_CONTEXT/TASKS.md): trabajo activo; no es un historial. +- [Aprendizajes](AI_CONTEXT/LEARNINGS.md): hallazgos reutilizables y confirmados. + +## Responsabilidades de las herramientas + +- **Project Brain** actualiza únicamente los hechos verificables dentro del bloque generado de `CONTEXT.md`. +- **Graphify** deriva relaciones y visualizaciones a partir del repositorio; `graphify-out/` es reconstruible y no es contexto fuente. +- **Obsidian** navega y permite editar estos mismos archivos Markdown; ninguna función depende de wikilinks ni de plugins. + +## Reglas de trabajo + +- Distingue hechos observados de propuestas o supuestos. +- No edites el bloque generado de `CONTEXT.md`; usa `brain sync .`. +- Conserva el contenido manual fuera de los marcadores generados. +- Actualiza únicamente el archivo cuyo propósito corresponda al cambio. +- Usa el historial de Git para el pasado; evita duplicar bitácoras o reportes. +- No guardes credenciales, tokens, llaves privadas ni evidencia restringida. +- No borres datos, publiques ramas ni despliegues sin autorización explícita. + +## Validación + +Ejecuta `brain sync .` y después `brain doctor .` cuando cambien hechos verificables del repositorio. diff --git a/AI_CONTEXT/AGENTS.md b/AI_CONTEXT/AGENTS.md deleted file mode 100644 index eebdca1..0000000 --- a/AI_CONTEXT/AGENTS.md +++ /dev/null @@ -1,16 +0,0 @@ -# AGENTS - -## ChiefAgent - -Coordinates the specialist agents and consolidates their reports. - -## Specialist agents - -- ProductAgent: UX, workflow friction, backlog opportunities -- QAAgent: testing depth, untested surfaces, likely defects -- SecurityAgent: secrets exposure, dependency hygiene, container hardening -- ObservabilityAgent: logs, metrics, alerting, operational readiness -- LegalAgent: license posture and compliance documentation gaps -- OptimizationAgent: performance, dependency bloat, runtime efficiency -- DocumentationAgent: architecture, API and runbook generation -- DevAgent: refactor and maintainability recommendations diff --git a/AI_CONTEXT/ANNOTATIONS.md b/AI_CONTEXT/ANNOTATIONS.md deleted file mode 100644 index 0e2aa9a..0000000 --- a/AI_CONTEXT/ANNOTATIONS.md +++ /dev/null @@ -1,3 +0,0 @@ -# ANNOTATIONS - -- None recorded. diff --git a/AI_CONTEXT/API_MAP.md b/AI_CONTEXT/API_MAP.md deleted file mode 100644 index e2cc264..0000000 --- a/AI_CONTEXT/API_MAP.md +++ /dev/null @@ -1,13 +0,0 @@ -# API_MAP - -## API styles - -- None detected - -## API-related files - -- None detected - -## OpenAPI summaries - -- No OpenAPI summaries available diff --git a/AI_CONTEXT/ARCHITECTURE.md b/AI_CONTEXT/ARCHITECTURE.md deleted file mode 100644 index d233522..0000000 --- a/AI_CONTEXT/ARCHITECTURE.md +++ /dev/null @@ -1,12 +0,0 @@ -# ARCHITECTURE - -## Current snapshot - -- Repository: Agentes -- Project type: Backend API service -- Languages: TypeScript -- Frameworks: Express -- API styles: GraphQL, OpenAPI, REST -- Infrastructure: Dockerfile -- CI/CD: GitHub Actions -- Observability: logging=pino, metrics=prom-client diff --git a/AI_CONTEXT/ARCHITECTURE_MAP.md b/AI_CONTEXT/ARCHITECTURE_MAP.md deleted file mode 100644 index 7fa9d6b..0000000 --- a/AI_CONTEXT/ARCHITECTURE_MAP.md +++ /dev/null @@ -1,54 +0,0 @@ -# ARCHITECTURE_MAP - -## Top-level directories - -- .agents -- .eslint.devagent.config.mjs -- .github -- .gitignore -- ACKNOWLEDGEMENTS.md -- agents -- AI_CONTEXT -- analysis -- CITATION.cff -- cli -- CODE_OF_CONDUCT.md -- config -- CONTRIBUTING.md -- core -- docs -- eslint.config.mjs -- governance -- integrations -- LICENSE -- memory -- orchestrator -- package-lock.json -- package.json -- planning -- prompts -- README.md -- reports -- scripts -- SECURITY.md -- shared -- SUPPORT.md -- tests -- tools -- tsconfig.json -- vitest.config.ts - -## Structure signals - -- Source files: 125 -- Test files: 40 -- Nested subrepos: 0 -- Git submodules: 0 - -## Runtime hints - -- Frameworks: Unknown -- Infrastructure: Not detected -- CI providers: GitHub Actions -- Logging: Not detected -- Metrics: Not detected diff --git a/AI_CONTEXT/CONTEXT.md b/AI_CONTEXT/CONTEXT.md index 212b9b1..1297dd1 100644 --- a/AI_CONTEXT/CONTEXT.md +++ b/AI_CONTEXT/CONTEXT.md @@ -1 +1,32 @@ -# CONTEXT +--- +project_brain: 1 +role: context +--- + +# Contexto + + +## Hechos verificados del repositorio + +- Archivos analizados: 31 +- Huella del inventario: `sha256:aaec898c7b00bf09bc2468b1e44c877ae974698220f667353690c272826ab213` +- Stack: Node.js +- Raíces principales: .github, bin, schema, src, templates, test, test-support +- Lenguajes: JavaScript +- Manifiestos: [package-lock.json](../package-lock.json), [package.json](../package.json) + +### Comandos de validación detectados + +- `npm run check` +- `npm run test` + + +## Contexto manual + +- **Propósito:** mantener un contrato compacto de contexto verificable para repositorios de software. +- **Alcance actual:** tres comandos, cinco archivos canónicos y cero runtimes de IA. +- **Restricciones:** no tocar `main`, no ejecutar migraciones y conservar contenido manual. + +## Navegación + +Las reglas de trabajo viven en [AGENTS.md](../AGENTS.md). Continúa con [Decisiones](DECISIONS.md), [Tareas](TASKS.md) o [Aprendizajes](LEARNINGS.md) según lo que necesites. diff --git a/AI_CONTEXT/DECISIONS.md b/AI_CONTEXT/DECISIONS.md index 566457a..1c734a9 100644 --- a/AI_CONTEXT/DECISIONS.md +++ b/AI_CONTEXT/DECISIONS.md @@ -1,3 +1,22 @@ -# DECISIONS +--- +project_brain: 1 +role: decisions +--- -- Adopt non-destructive analysis as the operating mode. +# Decisiones + +## Núcleo ligero + +- **Estado:** aceptada +- **Decisión:** Project Brain solo administra contexto verificable con `init`, `sync` y `doctor`. +- **Motivo:** el uso real requiere orden y continuidad, no otra plataforma multiagente. +- **Consecuencia:** Graphify posee relaciones, Obsidian navega Markdown y Git conserva el historial. + +## Adopción explícita + +- **Estado:** aceptada +- **Decisión:** no incluir migraciones automáticas desde formatos anteriores. +- **Motivo:** cada repositorio conserva necesidades y datos con distinto nivel de sensibilidad. +- **Consecuencia:** `brain init` se ejecuta únicamente donde se autorice adoptar el contrato. + +Consulta primero el [Contexto](CONTEXT.md). diff --git a/AI_CONTEXT/DEPENDENCY_GRAPH.md b/AI_CONTEXT/DEPENDENCY_GRAPH.md deleted file mode 100644 index e9537c5..0000000 --- a/AI_CONTEXT/DEPENDENCY_GRAPH.md +++ /dev/null @@ -1,19 +0,0 @@ -# DEPENDENCY_GRAPH - -## package.json - -- Ecosystem: node -- Dependencies tracked: 13 -- @langchain/ollama -- @types/node -- @typescript-eslint/parser -- commander -- deepagents -- dependency-cruiser -- eslint -- langchain -- ts-node -- ts-prune -- typescript -- vitest -- zod diff --git a/AI_CONTEXT/ERRORS.md b/AI_CONTEXT/ERRORS.md deleted file mode 100644 index 77d52bb..0000000 --- a/AI_CONTEXT/ERRORS.md +++ /dev/null @@ -1 +0,0 @@ -# ERRORS diff --git a/AI_CONTEXT/LEARNINGS.md b/AI_CONTEXT/LEARNINGS.md index f8c2a97..c504f3a 100644 --- a/AI_CONTEXT/LEARNINGS.md +++ b/AI_CONTEXT/LEARNINGS.md @@ -1 +1,14 @@ -# LEARNINGS +--- +project_brain: 1 +role: learnings +--- + +# Aprendizajes + +## Un dueño por responsabilidad + +- **Evidencia:** los proyectos consumidores acumularon notas, grafos y reportes duplicados. +- **Aplicación:** mantener aquí solo hallazgos durables; delegar relaciones a Graphify e historial a Git. +- **Límite:** una necesidad avanzada debe justificarse como herramienta separada, no ampliar el núcleo por defecto. + +Relaciona el hallazgo con una [Decisión](DECISIONS.md) solo si cambia una regla vigente. diff --git a/AI_CONTEXT/PROJECT_MODEL.md b/AI_CONTEXT/PROJECT_MODEL.md deleted file mode 100644 index 7bcf623..0000000 --- a/AI_CONTEXT/PROJECT_MODEL.md +++ /dev/null @@ -1,24 +0,0 @@ -# PROJECT_MODEL - -Project: project-brain - -Type: -Software project - -Languages: -TypeScript - -Frameworks: -Unknown - -APIs: -Not detected - -Testing: -Vitest - -Infrastructure: -Not detected - -Git: -codex/swarm-runtime-console (c58fce4 docs: expand project run instructions and verification steps) diff --git a/AI_CONTEXT/RULES.md b/AI_CONTEXT/RULES.md deleted file mode 100644 index 2db3ab0..0000000 --- a/AI_CONTEXT/RULES.md +++ /dev/null @@ -1,19 +0,0 @@ -# RULES - -1. Never modify target code automatically without human approval. -2. Understand the repository before proposing changes. -3. Preserve project context and decisions across runs. -4. Record errors, corrections, and learnings in durable memory. -5. Generate documentation as a first-class artifact. -6. Keep recommendations stack-aware and portable. - -## Coding Discipline - -These rules are adapted from the Karpathy-style coding guidelines reference and apply to Project Brain agents, generated `CLAUDE.md` files, and implementation work: - -1. State assumptions before changing code. -2. Prefer the smallest implementation that satisfies the confirmed goal. -3. Do not refactor adjacent code unless the task requires it. -4. Every changed line should trace to the current task or accepted decision. -5. Define verification before implementation. -6. If context is missing or contradictory, stop and ask instead of guessing. diff --git a/AI_CONTEXT/STACK_PROFILE.md b/AI_CONTEXT/STACK_PROFILE.md deleted file mode 100644 index 9c615d4..0000000 --- a/AI_CONTEXT/STACK_PROFILE.md +++ /dev/null @@ -1,28 +0,0 @@ -# STACK_PROFILE - -## Languages - -- TypeScript - -## Frameworks - -- None detected - -## APIs - -- None detected - -## Infrastructure - -- None detected - -## Testing - -- Vitest - -## Cross-cutting integrations - -- CI/CD: GitHub Actions -- Structured logging: No -- Metrics: Not detected -- Alerts: Not detected diff --git a/AI_CONTEXT/STYLE_GUIDE.md b/AI_CONTEXT/STYLE_GUIDE.md deleted file mode 100644 index 7a890cf..0000000 --- a/AI_CONTEXT/STYLE_GUIDE.md +++ /dev/null @@ -1,3 +0,0 @@ -# STYLE_GUIDE - -- Prefer strict typing, small modules, and explicit boundary contracts. diff --git a/AI_CONTEXT/TASKS.md b/AI_CONTEXT/TASKS.md index 58391ba..083b28f 100644 --- a/AI_CONTEXT/TASKS.md +++ b/AI_CONTEXT/TASKS.md @@ -1 +1,16 @@ -# TASKS +--- +project_brain: 1 +role: tasks +--- + +# Tareas + +## En curso + +- Ninguna. + +## Bloqueos + +- Ninguno. + +Las restricciones vigentes viven en [Contexto](CONTEXT.md) y las elecciones duraderas en [Decisiones](DECISIONS.md). diff --git a/AI_REVIEW_START_HERE.md b/AI_REVIEW_START_HERE.md deleted file mode 100644 index 9644692..0000000 --- a/AI_REVIEW_START_HERE.md +++ /dev/null @@ -1,75 +0,0 @@ -# AI Review Start Here - -This repository is `project-brain`, a repository intelligence engine. Its goal is to analyze software projects, persist useful context, coordinate bounded agents, and produce review-only recommendations. - -Release context: `0.2.0` prioritizes guided `go`, progressive memory, deterministic preflight facts, bounded swarm presets, output contracts, and review-only safety for internal beta validation. - -## Read Order - -Start here before reading large source files: - -1. `docs/architecture.md` -2. `docs/roadmap/token-aware-orchestration.md` -3. `docs/roadmap/fact-based-context-roadmap.md` -4. `docs/reference-repo-analysis/claude-mem-comparison.md` -5. `docs/reference-repo-analysis/graphify-comparison.md` if present -6. `AI_CONTEXT/MEMORY_BRIEF.md` when analyzing a generated output directory -7. `AI_CONTEXT/EXECUTIVE_SUMMARY.md` when available in an output directory -8. `memory/scopes/*.json` for fresh/stale scoped memory -9. `memory/knowledge_graph/repository_fact_graph.json` when available in an output directory - -## Source Entry Points - -Read these source files next: - -1. `cli/project-brain.ts` -2. `core/orchestrator/main.ts` -3. `core/status/index.ts` -4. `core/resume/index.ts` -5. `core/swarm_runtime/index.ts` -6. `core/ai_router/router.ts` -7. `core/token_policy/index.ts` -8. `memory/memory_brief/index.ts` -9. `memory/preflight_facts/index.ts` -10. `memory/scope_store/index.ts` -11. `memory/executive_summary/index.ts` -12. `memory/context_store/index.ts` -13. `governance/self-governance-system.ts` - -## Operating Rules - -- Prefer `go`, `status`, `resume`, `runbook`, and `fact-query` before broad `analyze` or model-heavy swarm work. -- Do not infer repository behavior from filenames alone. -- Use generated memory before broad source reading. -- Treat `AI_CONTEXT/MEMORY_BRIEF.md` as the compact handoff. -- Treat `AI_CONTEXT/EXECUTIVE_SUMMARY.md` as the human-facing release/state summary. -- Use `preflightFacts` before model-heavy ask or agent workflows when evidence may already exist. -- Treat `memory/knowledge_graph/repository_fact_graph.json` as structural evidence. -- Treat stale scope memory as a delta target, not truth. -- Use `UNKNOWN` when evidence is missing. -- Recommend narrow, staged changes before broad rewrites. -- Preserve review-only behavior unless the user explicitly asks for implementation. - -## Token-Safe First Commands - -```bash -npm run build -node dist/cli/project-brain.js start "optimize analysis and cost" . --output ./sample-output/self-optimization -node dist/cli/project-brain.js go "understand this project" . --output ./sample-output/self-optimization -node dist/cli/project-brain.js status . --output ./sample-output/self-optimization -node dist/cli/project-brain.js runbook "optimize analysis and cost" . --output ./sample-output/self-optimization -node dist/cli/project-brain.js code-graph . --output ./sample-output/self-optimization -node dist/cli/project-brain.js fact-query "swarm runtime token cache" . --output ./sample-output/self-optimization -node dist/cli/project-brain.js resume . --output ./sample-output/self-optimization -``` - -## Optimization Focus - -The highest-value optimization work is: - -1. keep `preflightFacts` and `fact-query` ahead of model-heavy work -2. keep `AI_CONTEXT/CONTEXT.md`, `LEARNINGS.md`, `ERRORS.md`, and `DECISIONS.md` alive instead of skeletal -3. prefer factual graph, executive summary, and scope memory over long markdown reports -4. update decisions, learnings, corrections, and unknowns incrementally -5. keep swarm prompts short, scoped, and cacheable -6. preserve review-only behavior and deterministic fallbacks when Ollama/Claude are unavailable diff --git a/CHANGELOG.md b/CHANGELOG.md index de2334b..c82225d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,93 +1,11 @@ # Changelog -## 0.2.4 - 2026-05-13 +## 0.3.0 - 2026-08-25 -### Added -- `npm run review:exports` generates a review-only `ts-prune` report for unused export candidates. -- Dedicated agent behavior tests cover QA, security, observability, and optimization signals. +- Reduce el producto a `brain init`, `brain sync` y `brain doctor`. +- Define un contrato de cinco archivos Markdown compatible con Obsidian. +- Conserva contexto manual y actualiza solo hechos verificables. +- Excluye salidas de Graphify y elimina grafos, agentes y runtimes duplicados. +- Añade validaciones de tamaño, enlaces, duplicados y datos sensibles. -### Fixed -- Swarm workers now quarantine code/script-only local model responses as unknowns instead of promoting them into findings. - -### Tests -- Full suite passes: 51 files, 142 tests. - -## 0.2.3 - 2026-05-12 - -### Added - -- `project-brain new` seeds new projects with guided `AI_CONTEXT`, architecture docs, project memory, initial backlog, and `CLAUDE.md`. -- Interactive and non-interactive project seed inputs cover archetype, audience, stack, features, auth, roles, data entities, integrations, priority, and language. - -### Changed - -- Package metadata, README, and output contract are aligned with the `v0.2.3` release. - -## 0.2.2 - 2026-05-12 - -### Changed - -- README now includes direct run instructions from source, compiled `dist`, and optional `npm link`. -- CLI version reporting now reads from package metadata instead of a hardcoded string. - -### Fixed - -- Package metadata, lockfile, README, and output contract are aligned for the `v0.2.2` release. - -## 0.2.1 - 2026-05-12 - -### Added - -- `project-brain architecture-plan` generates an evidence-backed architecture blueprint, evolution state, agent context, and compact JSON memory. -- Default CLI output now writes to `BRAIN/` under the target repository when `--output` is omitted. - -### Fixed - -- CLI, package metadata, lockfile, README, and output contract versions now align with the `v0.2.1` release tag. - -## 0.2.0 - 2026-05-08 - -### Added - -- Progressive memory release path with `MEMORY_BRIEF`, scoped memory, `EXECUTIVE_SUMMARY`, and repository fact graph artifacts. -- `preflightFacts` as the deterministic memory/facts gate before expensive analysis paths. -- Guided `project-brain go` entry point for beta users. -- Bounded swarm presets: `cheap`, `balanced`, and `thorough`. -- Runtime artifact policy for generated local outputs, diagnostics, and templates. -- Output contract documentation and JSON schemas for key consumable artifacts. -- Beta validation reports covering backend, frontend, mobile, monorepo, and low-documentation repositories. - -### Changed - -- Package version is prepared as `0.2.0` for an internal beta/release candidate. -- CI is prepared to run on Node 20 and Node 22. -- Package metadata now declares Node `>=20`, publishable files, and a `prepack` build step. -- Documentation now leads users through `project-brain go` before advanced commands. - -### Fixed - -- Local `npm audit` is clean after dependency lockfile updates. -- Runtime artifacts are isolated from source-controlled project state. -- Memory regression tests cover stale scope memory, deleted files, truncated hash tracking, dedupe, stale fact-query behavior, and real executive summary scopes. - -### Security - -- `npm audit` reports zero local vulnerabilities. -- CI includes `npm audit --audit-level=high` in the main quality gate. -- Dependency review fails pull requests with high-severity dependency issues. - -### Tests - -- Full local test suite passes: 44 files, 113 tests. -- Release smoke validation covers `go`, `status`, `resume`, `fact-query`, `runbook`, and bounded swarm presets on representative targets. - -### Docs - -- Added installation, first-analysis, output-contract, user-test, release checklist, release notes, commercial hardening backlog, beta readiness, and release candidate reports. - -### Known limitations - -- GitHub Dependabot may need to refresh alerts after lockfile changes are pushed. -- `swarm thorough` was not executed in beta validation because cost/time was not justified for release-candidate gating. -- Model-heavy behavior depends on local Ollama or configured cloud/API providers. -- This is not a commercial 1.0 release; it is an internal beta/release candidate. +No se incluyen migraciones automáticas para estructuras anteriores. diff --git a/CITATION.cff b/CITATION.cff deleted file mode 100644 index e6d137c..0000000 --- a/CITATION.cff +++ /dev/null @@ -1,12 +0,0 @@ -cff-version: 1.2.0 -title: project-brain -message: "If you use project-brain in research, engineering notes, or public analysis, please cite it." -type: software -license: MIT -authors: - - family-names: Ruiz - given-names: Ruzer -repository-code: "https://github.com/ruzer/project-brain" -url: "https://github.com/ruzer/project-brain" -abstract: "A control-tower repository analysis engine for discovery, context, review, and improvement planning." -version: 0.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 91f9483..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,27 +0,0 @@ -# Code of Conduct - -`project-brain` expects technically serious, respectful collaboration. - -## Expected behavior - -- Be precise, constructive, and evidence-driven. -- Critique code, behavior, or design decisions without attacking people. -- Keep security reports responsible and private when they involve exploitable issues. -- Credit upstream ideas and sources clearly. -- Respect maintainer time by including commands, outputs, and reproduction context. - -## Unacceptable behavior - -- Harassment, intimidation, or personal attacks. -- Malicious pull requests, hidden payloads, prompt injection attempts, or intentionally destructive changes. -- Spam, low-effort issue flooding, or deceptive benchmark and security claims. -- Publishing private vulnerabilities before maintainers have a chance to assess them. - -## Enforcement - -Maintainers may remove comments, close discussions, reject contributions, or block participants who violate these rules. - -## Reporting - -For conduct problems, open a private maintainer contact on GitHub if possible. -For security issues, follow [SECURITY.md](SECURITY.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 500a459..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,55 +0,0 @@ -# Contributing to project-brain - -Thanks for contributing. - -## Before opening an issue - -- Confirm the behavior on the current `main` branch. -- Include the exact command you ran, the target repository shape, and the generated artifact or report that looks wrong. -- If the problem is really a feature request, say what workflow it unlocks and why the current commands are not enough. - -## Before opening a pull request - -- Keep `project-brain` non-destructive. Changes must not automatically modify analyzed target repositories. -- Prefer deterministic analysis and explicit artifacts over opaque behavior. -- If you add or change a CLI command, update the README, `docs/usage.md`, and tests. -- If you adapt code or file content from another project, preserve its license notices and document the source clearly. - -## Development checklist - -Run these before asking for review: - -```bash -npm run hooks:install -npm run lint -npm run typecheck -npm run build -npm run test -npm run test:smoke -``` - -Local hooks now block: - -- weak commit messages like `wip` or `tmp` -- accidental secrets in staged changes -- generated or local-only paths such as `dist/`, `sample-output/`, `pb-output/`, `.env*` -- pushes that fail the quick verification gate - -If hooks are missing after cloning, run `npm run hooks:install`. - -## Pull request expectations - -Each PR should explain: - -- what problem it solves -- the commands or flows affected -- how it was validated -- whether documentation changed -- whether any upstream project influenced the implementation - -## Good first contributions - -- improve repository discovery signals -- add analyzers for more ecosystems -- improve report quality without making execution destructive -- add tests for edge cases in generated context or governance flows diff --git a/README.md b/README.md index 3451507..7cddddf 100644 --- a/README.md +++ b/README.md @@ -1,501 +1,59 @@ -# project-brain +# Project Brain Lite -Current release candidate: `0.2.3` internal beta. +Project Brain mantiene contexto útil para agentes sin convertirse en otra plataforma. Inspecciona hechos verificables del repositorio y deja las decisiones humanas en cuatro notas Markdown pequeñas. -`project-brain` is a non-destructive repository analysis engine for software systems and AI-assisted engineering workflows. It analyzes target repositories, builds durable context, runs specialist agents, generates reports, and produces review-only patch proposals. +## Inicio rápido -Recommended beta entry point: +Requiere Node.js 20 o posterior. ```bash -project-brain go "understand this project and suggest the next safe step" /path/to/repo +npm install --save-dev @ruzer/project-brain +npx brain init . ``` -By default, generated artifacts are grouped under `/path/to/repo/BRAIN/` so `AI_CONTEXT/`, runtime `memory/`, `reports/`, `tasks/`, and generated docs do not spread across the target repository root. Use `--output /path/to/output` when you want a different location. - -Typical use cases include: - -- frontend usability analysis -- architecture review -- backlog generation -- context prompt generation for downstream coding agents - -`project-brain` never applies code changes automatically to the target repository. - -## What it does - -- scans repositories and workspaces -- builds `AI_CONTEXT` memory artifacts -- maps repositories into structured onboarding docs -- preserves local repository annotations across runs -- computes blast radius and minimal review sets from file changes -- classifies agent tasks into firewall policy packs and persists task packets -- runs specialist agents for QA, UX, architecture, optimization, documentation, and development review -- generates backlog-style implementation tasks -- applies a proposal consensus gate before elevating recommendations -- produces review-only patch proposals for human approval -- exports reusable prompt templates for external repositories and downstream coding agents - -## Architecture - -Current runtime flow: +Eso crea, sin sobrescribir archivos existentes: ```text -CLI -> Orchestrator -> Discovery -> Context Builder -> Agent Firewall -> Code Graph / Agents -> Consensus Gate -> Reports -> Patch Proposals -``` - -Key modules: - -- `agents/`: specialist analysis agents -- `analysis/`: repository scanners and deterministic analyzers -- `core/`: orchestration, routing, and runtime coordination -- `memory/`: context, learnings, and persistent analysis state -- `tools/`: patch proposal and repo inspection utilities -- `cli/`: command entrypoints -- `prompts/context_templates/`: reusable prompt templates for external projects - -The codebase intentionally keeps the existing top-level runtime layout for compatibility. A physical move into `src/` was not performed because that would require import-path and build refactors. - -## Repository layout - -```text -project-brain/ - agents/ - analysis/ - cli/ - config/ - core/ - docs/ - governance/ - integrations/ - memory/ - orchestrator/ - prompts/ - agent_prompts/ - context_templates/ - scripts/ - shared/ - tools/ -``` - -## Installation - -Requisitos: - -- Node.js 20+ (project-brain runtime) -- `npm` (paquete, `build`, `lint`, etc.) -- `git` -- Ollama opcional para runtime local de modelos. -- Cloud/API provider opcional para planeacion o sintesis avanzada. -- Toolchains abiertos según el lenguaje del repositorio analizado (por ejemplo `npm`, `python3`, `go`, `cargo`, `java`, `php`, `ruby`, `dotnet`). - -Instalación rápida: - -```bash -npm install -npm run build -``` - -For a complete installation guide, see `docs/installation.md`. - -For a first analysis in five minutes, see `docs/first-analysis-5-min.md`. - -Si quieres validar la instalación completa localmente: - -```bash -project-brain doctor . -project-brain models -project-brain console --target /ruta/al/repo -``` - -Instalación recomendada de Ollama (si aún no lo tienes): - -```bash -# Linux -curl -fsSL https://ollama.com/install.sh | sh - -# macOS -brew install ollama - -# Descarga un modelo por defecto -ollama pull qwen2.5-coder:7b -ollama pull deepseek-coder:6.7b -ollama pull llama3.1:8b -``` - -Para entornos sin `brew`, usa la guía oficial de Ollama para tu sistema para completar la instalación. - -## Como correrlo - -Desde este repositorio local: - -1. Install dependencies and build once: - -```bash -npm install -npm run build -``` - -2. Ejecuta el CLI compilado: - -```bash -node dist/cli/project-brain.js --help -node dist/cli/project-brain.js doctor . -node dist/cli/project-brain.js go "understand this project and suggest the next safe step" /path/to/repo -``` - -3. Opcionalmente enlaza los comandos globales para usar `project-brain` o el alias corto `brain` directamente: - -```bash -npm link -project-brain --version -brain --version -brain help -project-brain console --target /path/to/repo -``` - -4. Prueba rápida de compilación en tu máquina: - -```bash -npm run typecheck -npm run build +AGENTS.md +AI_CONTEXT/ +├── CONTEXT.md +├── DECISIONS.md +├── TASKS.md +└── LEARNINGS.md ``` -If you changed CLI behavior, run this sequence before running a repo: +Ejemplo de uso cotidiano: ```bash -npm run build -node dist/cli/project-brain.js doctor . -``` +# Después de cambiar stack, estructura o scripts: +npx brain sync . -Por defecto, si no pasas `--output`, los artefactos se escriben en `/path/to/repo/BRAIN/`. Usa `--output /path/to/output` para separar completamente los resultados del repositorio analizado. - -## Validation - -```bash -npm run hooks:install -npm run lint -npm run typecheck -npm run verify +# Antes de confirmar cambios de contexto: +npx brain doctor . ``` -The minimal compilation validation is: +## Los tres comandos -```bash -npm run typecheck -npm run build -``` +- `brain init [ruta]`: crea únicamente los cinco archivos que falten y sincroniza el inventario. +- `brain sync [ruta]`: actualiza hechos comprobables dentro del bloque generado de `CONTEXT.md`; conserva byte por byte el contenido manual restante. +- `brain doctor [ruta]`: detecta exceso de contexto, enlaces rotos, duplicados, archivos extra y posibles datos sensibles. Usa `--json` para automatización. -## Typical usage +El escáner usa primero el inventario de Git y respeta `.gitignore`; fuera de Git hace un recorrido local seguro. No llama modelos, servicios cloud ni procesos autónomos. La huella es determinista y no incluye marcas de tiempo. -Recommended entry point for non-technical or day-to-day use: +## Responsabilidades claras -```bash -project-brain go "understand this project and suggest the next safe step" /path/to/repo -``` +- **Project Brain:** crea el contrato y actualiza únicamente el bloque verificable de `CONTEXT.md`; el contenido manual pertenece al equipo. +- **Graphify:** relaciones y grafos. Mantén su salida reconstruible en `graphify-out/`. +- **Obsidian:** navegación y edición humana de Markdown. +- **Git:** historial; no dupliques bitácoras en el contexto activo. -Use the console when you want a guided menu: +El escáner de Project Brain excluye `graphify-out/`, `.graphify/` y `.obsidian/` para evitar ciclos; no instala ni configura Graphify u Obsidian. No se necesita migración: adopta este contrato solo en proyectos donde decidas ejecutar `brain init`. -```bash -project-brain console --target /path/to/repo -``` - -Create a new project context before writing application code: - -```bash -project-brain new ./my-new-project -project-brain new ./my-new-project --yes --name "Inventory SaaS" --problem "Track workshop inventory" --audience "small repair shops" --type saas-webapp --stack "Next.js + PostgreSQL" -``` - -`new` asks for the basic product context, then creates `AI_CONTEXT/`, `docs/architecture_plan/`, `memory/project_seed/`, `tasks/initial_backlog.md`, and `CLAUDE.md`. It is context-only in this version; it does not generate application source code. - -Map an existing repository before deeper analysis: +## Desarrollo ```bash -project-brain map-codebase /path/to/repo +npm test +npm run check ``` -Generate a lightweight `AI_CONTEXT/` pack for a smaller app without running the full governed pipeline: - -```bash -project-brain context-lite /path/to/repo -``` - -Use plain language and let `project-brain` route the workflow: - -```bash -project-brain ask "identifica este proyecto" /path/to/repo -``` - -Run a structured, evidence-based security audit with verified architecture and a coordinated agent team: - -```bash -project-brain security-audit /path/to/repo --output /path/to/output -``` - -Open an interactive terminal console when you want one place to configure target paths, swarm defaults, and the main workflows: - -```bash -project-brain console --target /path/to/repo --output /path/to/output -``` - -Inside the console, the setup panel now shows whether Ollama is installed and which open-source language toolchains are missing or available so `project-brain` can expand beyond static analysis and run stack-specific commands on free/local runtimes. - -Persist a project-level improvement roadmap from the current analysis state: - -```bash -project-brain plan-improvements /path/to/repo --trigger repository-change --output /path/to/output -``` - -Generate an architecture evidence plan (blueprint + evolution state + execution context) before major refactors: - -```bash -project-brain architecture-plan /path/to/repo --output /path/to/output -``` - -Search curated stack guidance and materialize it into project context: - -```bash -project-brain context-search "express observability" /path/to/repo --output /path/to/output -project-brain context-get node-express-api /path/to/repo --output /path/to/output -``` - -Scan GitHub for repos that can improve `project-brain` and materialize them into the local context registry: - -```bash -project-brain ecosystem-radar /path/to/repo --output /path/to/output -project-brain ecosystem-radar /path/to/repo --bucket memory --limit 4 --output /path/to/output -``` - -Attach persistent local context for future runs: - -```bash -project-brain annotate /path/to/repo "This repo has a fragile legacy auth boundary" --output /path/to/output -``` - -Compute blast radius for a file or a set of files: - -```bash -project-brain impact-radius /path/to/repo --files src/core/service.ts,src/api/router.ts --output /path/to/output -``` - -## Progressive memory and token reduction - -`project-brain` reduces repeated analysis by reading generated memory before -model-heavy work: - -- `AI_CONTEXT/MEMORY_BRIEF.md`: compact handoff for agents and humans. -- `AI_CONTEXT/EXECUTIVE_SUMMARY.md`: current project state, risks, scopes, and next actions. -- `memory/scopes/*.json`: scoped memory with freshness status. -- `memory/knowledge_graph/repository_fact_graph.json`: factual repository graph. -- `preflightFacts`: deterministic memory/fact gate before expensive model-heavy workflows. -- `fact-query`: deterministic factual query before model-heavy work. - -## Release and beta docs - -- `docs/installation.md`: install and validate from source. -- `docs/first-analysis-5-min.md`: first guided run for a new target repository. -- `docs/output-contract.md`: stable and internal output contracts. -- `docs/release-checklist.md`: local and remote gates before tagging. -- `docs/releases/0.2.0.md`: release notes. -- `docs/user-test-script.md`: non-technical user test. -- `reports/validation-matrix.md`: beta validation targets and commands. -- `reports/beta-readiness.md`: readiness assessment. - -Fresh and complete scope memory can reduce bounded swarm work. Stale or partial -memory is treated as a delta target, not as current truth. - -## Review-only safety - -The default product posture is analysis and review. Generated patch proposals are -not applied automatically. Runtime diagnostics, local model inventory, and -machine-specific reports are ignored by git; stable templates live under -`reports/templates/`. - -## Troubleshooting - -- Vulnerabilities: run `npm audit`, then `npm audit fix` when fixes stay within safe semver. -- Ollama unavailable: `doctor` reports the missing local runtime and the CLI still runs deterministic memory/graph workflows. -- Claude/API unavailable: model-assisted refinement degrades; factual memory and graph commands still work. -- Memory stale: rerun `status`, `code-graph`, `fact-query`, or a targeted cheap swarm. -- Missing executive summary: run `project-brain status /path/to/repo --output /path/to/output`. - -Build or refresh the persistent code graph directly: - -```bash -project-brain code-graph /path/to/repo --output /path/to/output -``` - -Review the latest git delta with an import graph-backed review set: - -```bash -project-brain review-delta /path/to/repo --base HEAD~1 --head HEAD --output /path/to/output -``` - -Inspect the current agent policy, approvals, and task packets before deeper analysis: - -```bash -project-brain firewall /path/to/repo --trigger repository-change --output /path/to/output -``` - -Analyze a repository in place: - -```bash -project-brain analyze /path/to/repo -``` - -Keep generated artifacts outside the target repository: - -```bash -project-brain analyze /path/to/repo --output /path/to/output -``` - -Analyze a frontend repository with local AI routing: - -```bash -project-brain analyze \ - /path/to/frontend-repo \ - --output /path/to/output \ - --trigger repository-change \ - --ollama-timeout 240000 \ - --verbose -``` - -## Local AI runtime - -`project-brain` supports local inference through Ollama and can operate offline when local models are available. - -Current default model roles: - -- `worker`: `qwen2.5-coder:7b` -- `reviewer`: `deepseek-coder:6.7b` -- `reasoning`: `llama3.1:8b` -- `planner`: `kimi-k2.5:cloud` -- `synthesizer`: `llama3.1:8b` - -That means routine repo work stays on local models, while strategic intent routing and architecture-heavy asks can use `kimi-k2.5:cloud` through Ollama when allowed. - -Timeout precedence: - -1. `project-brain analyze --ollama-timeout ` -2. `OLLAMA_TIMEOUT_MS` -3. `config/models.json -> ollama_timeout_ms` -4. built-in default: `180000` - -Inspect configured models: - -```bash -project-brain models -``` - -Run a health check for the local environment, model config, git, and swarm readiness: - -```bash -project-brain doctor /path/to/repo --output /path/to/output -``` - -`doctor` now emits prioritized suggested next actions after the checks, so the output can move straight into the next control-tower step. - -Show the current operational snapshot for an output folder, including doctor/swarm/plan artifacts: - -```bash -project-brain status /path/to/repo --output /path/to/output -``` - -`status` also emits suggested follow-up commands based on the artifacts present or missing in that output path. - -Recover the latest useful checkpoint for an output folder and continue from there: - -```bash -project-brain resume /path/to/repo --output /path/to/output -``` - -`resume` detects the latest persisted artifact, tells you which stage the project was in, and suggests the most logical next command. - -`project-brain ask` now uses the planner profile only for strategic or ambiguous requests, then falls back cleanly if that model is unavailable. -If the user asks to continue or resume, `ask` now reuses the latest saved output state instead of starting from scratch. -When that next step is clear, `ask` now executes one guided continuation stage automatically, for example `swarm -> plan-improvements` or `plan-improvements -> review-delta`. - -You can also run a bounded delegated analysis: - -```bash -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --output /path/to/output -project-brain swarm "revisa core/swarm_runtime y prioriza mejoras reales" /path/to/repo --output /path/to/output -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --parallel 3 --chunk-size 1 -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --parallel 3 --chunk-size 1 --task-timeout-ms 12000 --max-retries 1 -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --parallel 2 --chunk-size 1 --planner-timeout-ms 8000 --synthesis-timeout-ms 8000 --run-timeout-ms 30000 --max-queued-tasks 8 -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --engine deepagents --output /path/to/output -project-brain self-improve /path/to/repo --output /path/to/output -``` - -Swarm runs now salvage labeled Markdown/text responses from local models when JSON is imperfect, and they honor explicit scope hints in the user intent such as `core/swarm_runtime`. - -`swarm` uses the planner to split the request into small tasks, then further shards those tasks into small repo-area chunks for local workers. It writes the merged result to `reports/swarm_run.md`. -By default it adapts parallel workers and queue budget to the local CPU/load/memory profile, uses a round-robin queue so small budgets touch multiple task types first, and can force planner/synthesis onto local models when the run budget is short. In that short-budget mode it also clamps auto-selected concurrency so local workers do not oversubscribe the machine. When a large scoped area times out, the swarm now splits that area into immediate child scopes before retrying instead of re-running the same broad directory. You can override worker count with `--parallel `, force smaller repo slices with `--chunk-size `, set a per-worker budget with `--task-timeout-ms`, cap planner/synthesis/global runtime with `--planner-timeout-ms`, `--synthesis-timeout-ms`, and `--run-timeout-ms`, limit queue growth with `--max-queued-tasks`, and allow bounded retries with `--max-retries`. - -`swarm --engine deepagents` keeps the repo read-only, gives the agent an isolated scratch filesystem under `memory/swarm/deepagents_workspace`, and exposes only controlled repository inspection tools. It is intended as an experimental evolution path for more autonomous planning and subagent delegation without replacing the governed `project-brain` pipeline. - -`self-improve` is the simplest way to point that swarm back at a repository, including `project-brain` itself, with bounded defaults for local runs while still letting the runtime shrink queue pressure automatically on a busy machine. It also switches the swarm to a `source-first` scope bias so the first chunks prefer product code over `tests/` and dotfiles. - -## Prompt-first workflow - -The repository now includes reusable templates in `prompts/context_templates/` for: - -- context bootstrap / AI_CONTEXT refresh -- frontend analysis -- UX improvement planning -- architecture review -- performance review - -These templates are designed to be copied into other repositories or used as context prompts for downstream coding agents. - -## Inspiration and attribution - -`project-brain` openly credits the projects that influenced specific ideas in its workflow and product design. The goal is to be explicit about inspiration without blurring implementation ownership. - -See [ACKNOWLEDGEMENTS.md](ACKNOWLEDGEMENTS.md). - -## Community - -If you want to contribute, report problems, or propose new analysis primitives, start with: - -- [CONTRIBUTING.md](CONTRIBUTING.md) -- [SECURITY.md](SECURITY.md) -- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) -- [SUPPORT.md](SUPPORT.md) -- [CITATION.cff](CITATION.cff) -- [docs/github-hardening.md](docs/github-hardening.md) - -## Open Source Hardening - -`project-brain` is now set up so the public GitHub repo can be opened with stronger defaults: - -- local git hooks block weak commit messages, staged secrets, and generated/local-only files -- CI now runs lint, typecheck, build, tests, smoke tests, and a repository safety scan -- GitHub dependency review runs on pull requests -- a weekly security baseline checks repository safety rules and production dependency audit -- `CODEOWNERS` and Dependabot config are committed in-repo - -The last mile still lives in GitHub settings, because branch protection and secret-scanning policies cannot be fully enforced from code alone. Use [docs/github-hardening.md](docs/github-hardening.md) after publishing the repository. - -## Safety rules - -- never modify target repositories automatically -- keep patch proposals review-only -- downgrade weakly corroborated proposals to human review -- never push from generated proposals -- require human approval before implementation -- constrain generated patches to the approved surface area -- classify tasks into `safe-readonly`, `review`, or `edit-limited` policy packs before execution - -## Documentation - -- [Documentation Index](docs/README.md) -- [Architecture](docs/architecture.md) -- [Agents](docs/agents.md) -- [Usage](docs/usage.md) -- [External Repository Integration](docs/external-repository-integration.md) -- [Production Architecture Spec](docs/production-architecture-spec.md) -- [Self-Governance](docs/agent-self-governance.md) -- [Roadmap](docs/roadmap/evolution-plan.md) -- [Architecture Assessments](docs/assessments/system-architecture-audit.md) -- [Acknowledgements](ACKNOWLEDGEMENTS.md) +El contrato verificable vive en [`schema/context-contract.schema.json`](schema/context-contract.schema.json). diff --git a/SECURITY.md b/SECURITY.md index 37a7bcc..60c0e2a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,34 +1,10 @@ -# Security Policy +# Seguridad -## Supported versions +Project Brain Lite opera sobre archivos locales, no ejecuta modelos y no transmite el contenido del repositorio. -Security fixes are expected on the latest state of `main`. +- Revisa rutas canónicas antes de escribir y rechaza enlaces simbólicos. +- `init` no sobrescribe archivos existentes. +- `sync` realiza una sustitución atómica limitada al bloque generado. +- `doctor` busca patrones comunes de credenciales y datos personales, pero no sustituye un escáner de secretos dedicado. -## Reporting a vulnerability - -- Do not open a public issue for an exploitable vulnerability. -- Prefer GitHub private vulnerability reporting if it is enabled for this repository. -- If private reporting is not available, contact the maintainer directly through GitHub with a minimal reproduction, impact summary, and affected files or commands. - -## Scope - -Please report issues such as: - -- command injection or unsafe shell execution -- unsafe file writes or path traversal in generated artifacts -- leakage of secrets or credentials in reports, logs, or generated context -- unsafe agent behavior that could cause destructive repository changes - -Non-sensitive hardening suggestions can still be opened as normal issues. - -## Baseline controls - -The repository now includes: - -- local `pre-commit`, `pre-push`, and `commit-msg` hooks -- repository safety scanning for staged changes and CI -- GitHub dependency review on pull requests -- a security baseline workflow with production dependency audit -- `CODEOWNERS` for sensitive areas - -Branch protection, required reviews, secret scanning, and GitHub private vulnerability reporting still need to be enabled in the repository settings. See `docs/github-hardening.md`. +No guardes tokens, contraseñas, llaves privadas ni información restringida en `AGENTS.md` o `AI_CONTEXT/`. Reporta vulnerabilidades mediante un aviso privado de seguridad en GitHub, no mediante un issue público. diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index 7d1247f..0000000 --- a/SUPPORT.md +++ /dev/null @@ -1,23 +0,0 @@ -# Support - -## Where to ask what - -- Bugs: open a bug report in GitHub Issues. -- Feature ideas: open a feature request in GitHub Issues. -- Usage questions and design discussion: use GitHub Discussions once enabled. -- Security problems: follow [SECURITY.md](SECURITY.md) instead of opening a public issue. - -## What to include - -- exact command -- target repository shape -- output path -- generated artifact or report that looks wrong -- current commit or release version - -## What maintainers will usually ask for - -- minimal reproduction -- expected behavior -- actual behavior -- relevant logs or report excerpts diff --git a/agents/ai-support.ts b/agents/ai-support.ts deleted file mode 100644 index 2ff3bc3..0000000 --- a/agents/ai-support.ts +++ /dev/null @@ -1,152 +0,0 @@ -import path from "node:path"; -import { readFileSync } from "node:fs"; - -import { readTextSafe, uniqueSorted } from "../shared/fs-utils"; -import type { ProjectContext, RiskLevel } from "../shared/types"; - -export interface AgentAIIssue { - severity: string; - description: string; -} - -export interface AgentAIImprovement { - type: string; - proposal: string; -} - -export interface AgentAIResponse { - issues: AgentAIIssue[]; - proposed_improvements: AgentAIImprovement[]; -} - -function resolveProjectBrainRoot(startDir: string): string { - let current = startDir; - - while (true) { - const candidate = path.join(current, "package.json"); - - try { - const content = readFileSync(candidate, "utf8"); - const parsed = JSON.parse(content) as { name?: string }; - if (parsed.name === "project-brain") { - return current; - } - } catch { - // keep walking - } - - const parent = path.dirname(current); - if (parent === current) { - return process.cwd(); - } - current = parent; - } -} - -export async function loadAgentSystemPrompt(fileName: string): Promise { - const root = resolveProjectBrainRoot(__dirname); - const promptPath = path.join(root, "agents", "prompts", fileName); - return readTextSafe(promptPath); -} - -export function buildRepoSummary(context: ProjectContext): string { - const { discovery } = context; - const memoryBrief = buildMemoryBriefSummary(context); - return [ - "Memory contract: read this MEMORY_BRIEF before using repository details. Treat missing evidence as UNKNOWN.", - memoryBrief, - `Repository: ${context.repoName}`, - `Languages: ${discovery.languages.join(", ") || "Unknown"}`, - `Frameworks: ${discovery.frameworks.join(", ") || "Unknown"}`, - `APIs: ${discovery.apis.join(", ") || "Not detected"}`, - `Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"}`, - `Testing: ${discovery.testing.join(", ") || "Not detected"}`, - `CI/CD: ${discovery.ci.providers.join(", ") || "Not detected"}`, - `Top-level directories: ${discovery.structure.topLevelDirectories.join(", ") || "Unknown"}`, - `Source files: ${discovery.structure.sourceFileCount}`, - `Test files: ${discovery.structure.testFileCount}`, - `Recommendations: ${discovery.recommendations.join(" | ") || "None"}` - ].join("\n"); -} - -export function buildMemoryBriefSummary(context: ProjectContext, maxLines = 28): string { - try { - const briefPath = path.join(context.memoryDir, "MEMORY_BRIEF.md"); - const content = readFileSync(briefPath, "utf8"); - const lines = content - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !/^#/.test(line)) - .filter((line) => !/^- Generated:/i.test(line)) - .slice(0, maxLines); - - return lines.length > 0 ? `Memory brief:\n${lines.join("\n")}` : "Memory brief: Not available"; - } catch { - return "Memory brief: Not available"; - } -} - -function stripMarkdownFences(raw: string): string { - const trimmed = raw.trim(); - if (!trimmed.startsWith("```")) { - return trimmed; - } - - return trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim(); -} - -export function parseAgentAIResponse(raw: string): AgentAIResponse | undefined { - const normalized = stripMarkdownFences(raw); - const objectStart = normalized.indexOf("{"); - const objectEnd = normalized.lastIndexOf("}"); - const candidate = objectStart >= 0 && objectEnd > objectStart ? normalized.slice(objectStart, objectEnd + 1) : normalized; - - try { - const parsed = JSON.parse(candidate) as Partial; - return { - issues: (parsed.issues ?? []).filter( - (issue): issue is AgentAIIssue => Boolean(issue && typeof issue.description === "string") - ), - proposed_improvements: (parsed.proposed_improvements ?? []).filter( - (improvement): improvement is AgentAIImprovement => Boolean(improvement && typeof improvement.proposal === "string") - ) - }; - } catch { - return undefined; - } -} - -export function normalizeAIInsight(issue: AgentAIIssue): string { - const severity = issue.severity?.trim().toLowerCase() || "medium"; - return `[${severity.toUpperCase()}] ${issue.description.trim()}`; -} - -export function normalizeAIImprovement(improvement: AgentAIImprovement): string { - const type = improvement.type?.trim(); - return type ? `${type}: ${improvement.proposal.trim()}` : improvement.proposal.trim(); -} - -export function combineRecommendations(...lists: string[][]): string[] { - return uniqueSorted( - lists - .flat() - .map((value) => value.trim()) - .filter(Boolean) - ); -} - -export function mergeRiskLevel(base: RiskLevel, aiIssues: AgentAIIssue[]): RiskLevel { - if (base === "high") { - return base; - } - - if (aiIssues.some((issue) => issue.severity?.toLowerCase() === "high")) { - return "high"; - } - - if (base === "medium" || aiIssues.some((issue) => issue.severity?.toLowerCase() === "medium")) { - return "medium"; - } - - return "low"; -} diff --git a/agents/architecture_agent/index.ts b/agents/architecture_agent/index.ts deleted file mode 100644 index 2209898..0000000 --- a/agents/architecture_agent/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext } from "../../shared/types"; - -export class ArchitectureAgent extends BaseAgent { - constructor() { - super("architecture-agent", "architecture_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const deterministicFindings: string[] = []; - const recommendations: string[] = []; - const { discovery } = context; - - if (discovery.structure.subrepos.length > 2) { - deterministicFindings.push("Multiple nested packages suggest growing architectural complexity."); - recommendations.push("Define explicit module boundaries and ownership across nested packages."); - } - - if (discovery.languages.length > 2) { - deterministicFindings.push("Polyglot architecture detected with potential coordination overhead."); - recommendations.push("Document cross-runtime contracts and integration ownership."); - } - - if (discovery.infrastructure.includes("Kubernetes") && discovery.ci.providers.length === 0) { - deterministicFindings.push("Operational infrastructure exists without visible release orchestration controls."); - recommendations.push("Introduce deployment governance and architecture runbooks for cluster changes."); - } - - if (discovery.structure.topLevelDirectories.length > 8 && !discovery.files.some((file) => file.startsWith("docs/architecture"))) { - deterministicFindings.push("Repository surface is broad but architecture documentation is shallow."); - recommendations.push("Maintain a living architecture map with bounded contexts and critical data flows."); - } - - const aiResponse = await this.requestStructuredAI(context, { - task: "architecture-review", - systemPromptFile: "architect.system.md", - analysisPrompt: [ - `Review the repository architecture for structural complexity and redesign pressure.`, - `Languages: ${discovery.languages.join(", ") || "Unknown"}.`, - `Frameworks: ${discovery.frameworks.join(", ") || "Unknown"}.`, - `Top-level directories: ${discovery.structure.topLevelDirectories.join(", ") || "Unknown"}.`, - `Subrepos: ${discovery.structure.subrepos.join(", ") || "None"}.`, - `Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"}.`, - `Deterministic findings: ${deterministicFindings.join(" | ") || "None"}.` - ].join("\n") - }); - - return this.buildAIEnhancedEvaluation( - { - title: "Architecture Report", - summary: "ArchitectureAgent evaluated structure, boundaries, and architectural drift signals.", - deterministicFindings, - recommendations, - riskLevel: deterministicFindings.length >= 3 ? "high" : deterministicFindings.length >= 1 ? "medium" : "low" - }, - aiResponse - ); - } -} diff --git a/agents/auth_agent/index.ts b/agents/auth_agent/index.ts deleted file mode 100644 index 12c56ad..0000000 --- a/agents/auth_agent/index.ts +++ /dev/null @@ -1,180 +0,0 @@ -import path from "node:path"; - -import { BaseAgent } from "../base-agent"; -import { readTextSafe } from "../../shared/fs-utils"; - -import type { - AgentEvaluation, - ProjectContext, - SecurityCoverageStatus, - SecurityFinding -} from "../../shared/types"; - -const AUTH_FILE_PATTERN = /(^|\/)(auth|session|permissions?|roles?|access|acl|rbac|middleware).*\.(ts|tsx|js|jsx)$/i; -const AUTH_ROUTE_PATTERN = /(^|\/)(src\/)?app\/api\/auth\/.+\/route\.(ts|tsx|js|jsx)$/i; - -function pushFinding( - target: SecurityFinding[], - finding: SecurityFinding, - findings: string[], - recommendations: string[] -): void { - target.push(finding); - findings.push(`[${finding.severity.toUpperCase()}] ${finding.title}: ${finding.impact}`); - recommendations.push(finding.fix); -} - -function extractSnippet(content: string, pattern: RegExp): string { - const match = content.match(pattern); - if (!match?.[0]) { - return "No se pudo extraer un fragmento corto adicional."; - } - - return match[0].replace(/\s+/g, " ").trim(); -} - -export class AuthAgent extends BaseAgent { - constructor() { - super("auth-agent", "auth_security_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const securityFindings: SecurityFinding[] = []; - const coverage: SecurityCoverageStatus[] = []; - const authFiles = context.discovery.files.filter((filePath) => AUTH_FILE_PATTERN.test(filePath)).slice(0, 24); - const authRoutes = context.discovery.files.filter((filePath) => AUTH_ROUTE_PATTERN.test(filePath)).slice(0, 12); - const authorizationFiles = authFiles.filter((filePath) => /permissions?|roles?|access|acl|rbac/i.test(filePath)); - - for (const filePath of authFiles) { - const absolutePath = path.join(context.targetPath, filePath); - const content = await readTextSafe(absolutePath); - - if ( - /return\s*{\s*[^}]*\bid\s*:\s*["'`][^"'`]+["'`][^}]*\brole\s*:\s*["'`][^"'`]+["'`][^}]*}/is.test(content) && - /session|auth/i.test(filePath) - ) { - pushFinding( - securityFindings, - { - area: "auth_sessions", - severity: "high", - title: "Contexto de sesión autenticada hardcodeado", - location: filePath, - evidence: `Fragmento detectado: ${extractSnippet(content, /return\s*{[\s\S]*?}/i)}`, - attackVector: [ - "Una ruta del backend consume el helper de sesión comprometido.", - "La función entrega un usuario o rol fijo sin verificar credenciales.", - "El atacante obtiene contexto autenticado o privilegios sin validación real." - ], - impact: "Bypass de autenticación y decisiones de autorización basadas en identidad simulada o fija.", - fix: "Reemplazar el helper por lectura de sesión real desde cookie/JWT verificado y devolver `null` cuando no exista una sesión válida.", - references: ["CWE-287", "OWASP A07:2021", "ASVS 3.2.2"], - effort: "medium", - problemType: "code", - agentId: this.agentId - }, - findings, - recommendations - ); - } - - if (/(localStorage|sessionStorage)\.(setItem|getItem)\([^)]*(token|session|auth)/i.test(content)) { - pushFinding( - securityFindings, - { - area: "auth_sessions", - severity: "medium", - title: "Token o sesión accesible desde Web Storage", - location: filePath, - evidence: `Fragmento detectado: ${extractSnippet(content, /(localStorage|sessionStorage)\.(setItem|getItem)\([^)]*\)/i)}`, - attackVector: [ - "El atacante consigue ejecutar JavaScript en el navegador (por XSS o script de tercero).", - "Lee el token almacenado en Web Storage.", - "Reutiliza el token para secuestrar sesión o llamar APIs." - ], - impact: "Mayor exposición de sesiones o JWT frente a XSS del lado cliente.", - fix: "Mover las credenciales de sesión a cookies `httpOnly`, `Secure` y `SameSite` apropiadas, evitando almacenar tokens sensibles en `localStorage` o `sessionStorage`.", - references: ["CWE-922", "OWASP A07:2021", "ASVS 3.4.2"], - effort: "medium", - problemType: "code", - agentId: this.agentId - }, - findings, - recommendations - ); - } - - if (/process\.env\.[A-Z0-9_]+\s*\|\|\s*["'`][^"'`]+["'`]/i.test(content) && /(jwt|token|secret|session|auth)/i.test(content)) { - pushFinding( - securityFindings, - { - area: "auth_sessions", - severity: "high", - title: "Secreto de sesión con fallback hardcodeado", - location: filePath, - evidence: `Fragmento detectado: ${extractSnippet(content, /process\.env\.[A-Z0-9_]+\s*\|\|\s*["'`][^"'`]+["'`]/i)}`, - attackVector: [ - "El despliegue carece de la variable esperada.", - "La aplicación cae al secreto hardcodeado o predecible.", - "Un atacante firma o verifica tokens con el valor conocido." - ], - impact: "Compromiso de integridad de sesiones o JWT cuando el entorno no inyecta el secreto correcto.", - fix: "Eliminar el fallback hardcodeado y abortar el arranque si falta la variable de entorno crítica de autenticación.", - references: ["CWE-798", "OWASP A07:2021", "ASVS 3.5.3"], - effort: "low", - problemType: "code+configuration", - agentId: this.agentId - }, - findings, - recommendations - ); - } - } - - coverage.push({ - area: "auth_sessions", - status: - securityFindings.some((entry) => entry.area === "auth_sessions") - ? "finding" - : authFiles.length > 0 || authRoutes.length > 0 - ? "ok" - : "not-reviewed", - note: - authFiles.length > 0 || authRoutes.length > 0 - ? `Se revisaron señales de sesión/auth en: ${[...authRoutes, ...authFiles].slice(0, 6).join(", ")}` - : "No se confirmaron módulos de sesión o auth suficientes para revisar el manejo real de sesiones.", - agentId: this.agentId - }); - - coverage.push({ - area: "authorization", - status: - authorizationFiles.length === 0 - ? "not-reviewed" - : securityFindings.some((entry) => entry.area === "authorization") - ? "finding" - : "ok", - note: - authorizationFiles.length > 0 - ? `Se detectaron controles o helpers de permisos en: ${authorizationFiles.slice(0, 6).join(", ")}` - : "No se confirmaron archivos suficientes de permisos/roles para revisar IDOR o escalación horizontal/vertical con detalle.", - agentId: this.agentId - }); - - return { - title: "Auth Security Report", - summary: "AuthAgent revisó sesión, permisos y señales de autenticación/autorización para detectar bypasses evidentes y zonas no revisadas.", - findings, - recommendations, - riskLevel: securityFindings.some((entry) => entry.severity === "high") - ? "high" - : securityFindings.some((entry) => entry.severity === "medium") - ? "medium" - : "low", - securityFindings, - coverage - }; - } -} diff --git a/agents/base-agent.ts b/agents/base-agent.ts deleted file mode 100644 index 75df754..0000000 --- a/agents/base-agent.ts +++ /dev/null @@ -1,187 +0,0 @@ -import path from "node:path"; - -import { - buildRepoSummary, - combineRecommendations, - loadAgentSystemPrompt, - mergeRiskLevel, - normalizeAIImprovement, - normalizeAIInsight, - parseAgentAIResponse, - type AgentAIResponse -} from "./ai-support"; -import { AIRouter, type AIRouterTask } from "../core/ai_router/router"; -import { writeFileEnsured } from "../shared/fs-utils"; -import { StructuredLogger } from "../shared/logger"; -import type { AgentEvaluation, AgentReport, ProjectContext } from "../shared/types"; - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -export function renderAgentReport(evaluation: AgentEvaluation): string { - if (evaluation.deterministicFindings || evaluation.aiInsights || evaluation.combinedRecommendations) { - return `# ${evaluation.title} - -## Summary - -${evaluation.summary} - -## Human Deterministic Findings - -${renderList(evaluation.deterministicFindings ?? evaluation.findings)} - -## AI Insights - -${renderList(evaluation.aiInsights ?? ["AI insights were unavailable for this cycle."])} - -## Combined Recommendations - -${renderList(evaluation.combinedRecommendations ?? evaluation.recommendations)} -`; - } - - return `# ${evaluation.title} - -## Summary - -${evaluation.summary} - -## Findings - -${renderList(evaluation.findings)} - -## Recommendations - -${renderList(evaluation.recommendations)} -`; -} - -export abstract class BaseAgent { - protected readonly logger: StructuredLogger; - protected readonly aiRouter: AIRouter; - - constructor( - public readonly agentId: string, - private readonly reportFileName: string - ) { - this.logger = new StructuredLogger("agent", { agent: agentId }); - this.aiRouter = new AIRouter(); - } - - protected abstract evaluate(context: ProjectContext): Promise; - - protected buildRepoSummary(context: ProjectContext): string { - return buildRepoSummary(context); - } - - protected async requestStructuredAI( - context: ProjectContext, - input: { - task: AIRouterTask; - systemPromptFile: string; - analysisPrompt: string; - } - ): Promise { - try { - const systemPrompt = await loadAgentSystemPrompt(input.systemPromptFile); - const prompt = [ - systemPrompt.trim(), - "", - "Repository context:", - this.buildRepoSummary(context), - "", - "Deterministic analysis:", - input.analysisPrompt.trim(), - "", - "Return JSON only." - ].join("\n"); - const response = await this.aiRouter.ask({ - task: input.task, - prompt, - context: this.buildRepoSummary(context) - }); - const parsed = parseAgentAIResponse(response); - - if (!parsed) { - this.logger.warn("AI response was not valid structured JSON", { - action: "ai_parse_failed", - task: input.task - }); - } - - return parsed; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - this.logger.warn("AI analysis fallback engaged", { - action: "ai_fallback", - error: message, - task: input.task - }); - return undefined; - } - } - - protected buildAIEnhancedEvaluation( - base: { - title: string; - summary: string; - deterministicFindings: string[]; - recommendations: string[]; - riskLevel: AgentEvaluation["riskLevel"]; - securityFindings?: AgentEvaluation["securityFindings"]; - coverage?: AgentEvaluation["coverage"]; - }, - aiResponse: AgentAIResponse | undefined - ): AgentEvaluation { - const aiInsights = aiResponse?.issues.map(normalizeAIInsight) ?? ["AI insights were unavailable for this cycle."]; - const aiRecommendations = aiResponse?.proposed_improvements.map(normalizeAIImprovement) ?? []; - const combinedRecommendations = combineRecommendations(base.recommendations, aiRecommendations); - - return { - title: base.title, - summary: base.summary, - deterministicFindings: base.deterministicFindings, - aiInsights, - combinedRecommendations, - findings: combineRecommendations(base.deterministicFindings, aiResponse?.issues.map(normalizeAIInsight) ?? []), - recommendations: combinedRecommendations, - riskLevel: mergeRiskLevel(base.riskLevel, aiResponse?.issues ?? []), - securityFindings: base.securityFindings, - coverage: base.coverage - }; - } - - async run(context: ProjectContext): Promise { - this.logger.info("Agent analysis started", { - action: "analysis_start", - repoName: context.repoName, - outputPath: context.reportsDir - }); - const evaluation = await this.evaluate(context); - const outputPath = path.join(context.reportsDir, this.reportFileName); - const content = evaluation.content ?? renderAgentReport(evaluation); - - await writeFileEnsured(outputPath, content); - - this.logger.info("Agent analysis completed", { - action: "analysis_complete", - repoName: context.repoName, - findings: evaluation.findings.length, - recommendations: evaluation.recommendations.length, - reportPath: outputPath - }); - - return { - agentId: this.agentId, - title: evaluation.title, - summary: evaluation.summary, - findings: evaluation.findings, - recommendations: evaluation.recommendations, - riskLevel: evaluation.riskLevel, - outputPath, - securityFindings: evaluation.securityFindings, - coverage: evaluation.coverage - }; - } -} diff --git a/agents/catalog.ts b/agents/catalog.ts deleted file mode 100644 index 49e3c26..0000000 --- a/agents/catalog.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { ArchitectureAgent } from "./architecture_agent"; -import { AuthAgent } from "./auth_agent"; -import { BaseAgent } from "./base-agent"; -import { DependencyAgent } from "./dependency_agent"; -import { DevAgent } from "./dev_agent"; -import { DocumentationAgent } from "./documentation_agent"; -import { InfraAgent } from "./infra_agent"; -import { LegalAgent } from "./legal_agent"; -import { ObservabilityAgent } from "./observability_agent"; -import { OptimizationAgent } from "./optimization_agent"; -import { ProductOwnerAgent } from "./product_owner_agent"; -import { QAAgent } from "./qa_agent"; -import { SecurityAgent } from "./security_agent"; -import { UXAgent } from "./ux_agent"; -import { UXImprovementAgent } from "./ux_improvement_agent"; - -import type { AgentDescriptor } from "../shared/types"; - -export interface AgentCatalogEntry { - agent: BaseAgent; - descriptor: AgentDescriptor; -} - -function define( - agent: BaseAgent, - descriptor: Omit -): AgentCatalogEntry { - return { - agent, - descriptor: { - agentId: agent.agentId, - ...descriptor - } - }; -} - -export function buildAgentCatalog(): AgentCatalogEntry[] { - return [ - define(new ProductOwnerAgent(), { - displayName: "ProductOwnerAgent", - version: "1.0.0", - capabilities: ["product-analysis", "backlog-prioritization", "proposal-ranking"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "repository-change", "weekly-review"], - requiresHumanApprovalFor: ["structural changes", "product reprioritization"] - }), - define(new QAAgent(), { - displayName: "QAAgent", - version: "1.0.0", - capabilities: ["test-gap-detection", "bug-risk-analysis", "coverage-review"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "repository-change", "security-audit", "incident-detection", "weekly-review"], - requiresHumanApprovalFor: ["quality gate policy changes"] - }), - define(new UXAgent(), { - displayName: "UXAgent", - version: "1.0.0", - capabilities: ["ux-audit", "workflow-friction-analysis", "navigation-clarity-review"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "repository-change", "weekly-review"], - requiresHumanApprovalFor: ["product workflow changes"] - }), - define(new UXImprovementAgent(), { - displayName: "UXImprovementAgent", - version: "1.0.0", - capabilities: ["ux-implementation-planning", "navigation-simplification", "form-simplification"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "repository-change", "weekly-review"], - requiresHumanApprovalFor: ["product workflow changes", "frontend information architecture changes"] - }), - define(new SecurityAgent(), { - displayName: "SecurityAgent", - version: "1.0.0", - capabilities: ["secret-detection", "dependency-hygiene", "container-review"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "security-audit", "security-advisory", "dependency-update", "weekly-review"], - requiresHumanApprovalFor: ["security-sensitive proposals", "authentication changes"] - }), - define(new AuthAgent(), { - displayName: "AuthAgent", - version: "1.0.0", - capabilities: ["session-review", "authorization-analysis", "identity-boundary-review"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "security-audit", "security-advisory"], - requiresHumanApprovalFor: ["authentication changes", "authorization model changes"] - }), - define(new DependencyAgent(), { - displayName: "DependencyAgent", - version: "1.0.0", - capabilities: ["dependency-governance", "manifest-analysis", "update-risk-review"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "security-audit", "security-advisory", "dependency-update", "weekly-review"], - requiresHumanApprovalFor: ["dependency policy changes"] - }), - define(new InfraAgent(), { - displayName: "InfraAgent", - version: "1.0.0", - capabilities: ["container-hardening", "deployment-surface-review", "headers-and-proxy-audit"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "security-audit", "security-advisory", "architecture-review"], - requiresHumanApprovalFor: ["infra changes", "header policy changes"] - }), - define(new ArchitectureAgent(), { - displayName: "ArchitectureAgent", - version: "1.0.0", - capabilities: ["architecture-analysis", "boundary-review", "drift-detection"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "architecture-review", "weekly-review", "incident-detection"], - requiresHumanApprovalFor: ["architectural decisions", "structural changes"] - }), - define(new ObservabilityAgent(), { - displayName: "ObservabilityAgent", - version: "1.0.0", - capabilities: ["observability-analysis", "telemetry-review", "alert-readiness"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "architecture-review", "incident-detection", "weekly-review"], - requiresHumanApprovalFor: ["alert policy changes"] - }), - define(new LegalAgent(), { - displayName: "LegalAgent", - version: "1.0.0", - capabilities: ["license-review", "compliance-gap-analysis", "notice-tracking"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "weekly-review"], - requiresHumanApprovalFor: ["compliance-sensitive proposals"] - }), - define(new OptimizationAgent(), { - displayName: "OptimizationAgent", - version: "1.0.0", - capabilities: ["performance-analysis", "dependency-optimization", "build-efficiency-review"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "architecture-review", "weekly-review", "incident-detection"], - requiresHumanApprovalFor: ["performance-sensitive infra changes"] - }), - define(new DocumentationAgent(), { - displayName: "DocumentationAgent", - version: "1.0.0", - capabilities: ["documentation-generation", "runbook-refresh", "api-doc-sync"], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "architecture-review", "weekly-review", "repository-change"], - requiresHumanApprovalFor: ["documentation publication policies"] - }), - define(new DevAgent(), { - displayName: "DevAgent", - version: "1.1.0", - capabilities: [ - "refactor-analysis", - "maintainability-review", - "architecture-risk-detection", - "static-analysis", - "engineering-task-proposals" - ], - allowedActions: ["analyze", "propose", "report"], - triggers: ["manual", "repository-change", "security-audit", "architecture-review", "weekly-review"], - requiresHumanApprovalFor: ["structural changes", "architectural decisions"] - }) - ]; -} diff --git a/agents/dependency_agent/index.ts b/agents/dependency_agent/index.ts deleted file mode 100644 index 67a4997..0000000 --- a/agents/dependency_agent/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext } from "../../shared/types"; - -export class DependencyAgent extends BaseAgent { - constructor() { - super("dependency-agent", "dependency_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const { discovery } = context; - const manifestCount = discovery.manifests.length; - const dependencyCount = discovery.dependencies.reduce((sum, manifest) => sum + manifest.dependencies.length, 0); - - if (manifestCount > 3) { - findings.push(`Multiple dependency manifests detected (${manifestCount}), increasing dependency governance complexity.`); - recommendations.push("Centralize dependency ownership and standardize update cadence across ecosystems."); - } - - if (dependencyCount > 120) { - findings.push(`Dependency volume is high (${dependencyCount} declared dependencies).`); - recommendations.push("Create a quarterly dependency reduction review focused on low-value or duplicate packages."); - } - - if (discovery.testing.length === 0 && dependencyCount > 0) { - findings.push("Dependency changes may be risky because no automated test framework was detected."); - recommendations.push("Introduce smoke tests before expanding dependency update automation."); - } - - return { - title: "Dependency Report", - summary: "DependencyAgent evaluated manifest sprawl and dependency governance signals.", - findings, - recommendations, - riskLevel: findings.length >= 2 ? "medium" : findings.length === 1 ? "low" : "low" - }; - } -} diff --git a/agents/dev_agent/index.ts b/agents/dev_agent/index.ts deleted file mode 100644 index 870d42e..0000000 --- a/agents/dev_agent/index.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { BaseAgent } from "../base-agent"; -import { analyzeDevelopmentArchitecture } from "../../tools/dev_analysis_tools"; -import { generatePatchProposals } from "../../tools/patch_proposal_tools"; -import { readTextSafe, writeFileEnsured } from "../../shared/fs-utils"; - -import type { AgentEvaluation, AgentReport, PatchProposalArtifact, ProjectContext } from "../../shared/types"; - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderMetricList( - items: Array<{ - filePath: string; - lineCount: number; - couplingScore: number; - complexityScore: number; - changeSignal: string; - changeFrequency: number; - }> -): string { - return items.length > 0 - ? items - .map( - (item) => - `- ${item.filePath} (${item.lineCount} lines, coupling ${item.couplingScore}, complexity ${item.complexityScore}, change ${item.changeFrequency} via ${item.changeSignal})` - ) - .join("\n") - : "- None"; -} - -function renderRiskList( - items: Array<{ - severity: string; - title: string; - problemDescription: string; - affectedFiles: string[]; - suggestedChange: string; - estimatedDifficulty: string; - confidenceScore: number; - }> -): string { - return items.length > 0 - ? items - .map( - (item, index) => `### ${index + 1}. [${item.severity.toUpperCase()}] ${item.title} - -- Problem: ${item.problemDescription} -- Affected files: ${item.affectedFiles.join(", ") || "None"} -- Suggested change: ${item.suggestedChange} -- Estimated difficulty: ${item.estimatedDifficulty} -- Confidence: ${item.confidenceScore}` - ) - .join("\n\n") - : "No architecture risks were identified in this cycle."; -} - -export class DevAgent extends BaseAgent { - constructor() { - super("dev-agent", "dev_architecture_analysis.md"); - } - - async run(context: ProjectContext): Promise { - const report = await super.run(context); - const patchProposals = await generatePatchProposals(context, this.agentId); - - await this.appendPatchProposalStage(report, patchProposals); - - if (patchProposals.length > 0) { - this.logger.info("Workflow stage completed", { - action: "workflow_stage_complete", - stage: "PROPOSE_PATCHES", - repoName: context.repoName, - patchProposals: patchProposals.length, - patchProposalDir: context.patchProposalDir - }); - } else { - this.logger.info("Workflow stage skipped", { - action: "workflow_stage_skipped", - stage: "PROPOSE_PATCHES", - repoName: context.repoName, - reason: "No UX_IMPLEMENTATION_TASKS.md source file was available for patch proposal generation." - }); - } - - return report; - } - - private async appendPatchProposalStage(report: AgentReport, patchProposals: PatchProposalArtifact[]): Promise { - const currentContent = await readTextSafe(report.outputPath); - const stageContent = - patchProposals.length > 0 - ? `## PROPOSE_PATCHES - -- Source: UX_IMPLEMENTATION_TASKS.md -- Human approval required: yes -- Patch proposals generated: ${patchProposals.length} - -${renderList( - patchProposals.map( - (proposal) => `${proposal.patchId} -> ${proposal.targetFile} | path=${proposal.filePath}` - ) - )} -` - : `## PROPOSE_PATCHES - -- Source: UX_IMPLEMENTATION_TASKS.md -- Human approval required: yes -- Patch proposals generated: 0 -- Stage result: skipped because no implementation task report was available. -`; - - await writeFileEnsured(report.outputPath, `${currentContent.trim()}\n\n${stageContent}`); - } - - protected async evaluate(context: ProjectContext): Promise { - const analysis = await analyzeDevelopmentArchitecture(context); - const findings = analysis.topArchitectureRisks.map( - (risk) => `[${risk.severity.toUpperCase()}] ${risk.title}: ${risk.problemDescription}` - ); - const recommendations = analysis.actionableProposals.map( - (proposal) => - `${proposal.title} -> ${proposal.suggestedChange} (files: ${proposal.affectedFiles.join(", ") || "None"}; difficulty: ${proposal.estimatedDifficulty}; confidence: ${proposal.confidenceScore})` - ); - const riskLevel = - analysis.topArchitectureRisks.some((risk) => risk.severity === "high") - ? "high" - : analysis.topArchitectureRisks.some((risk) => risk.severity === "medium") - ? "medium" - : "low"; - const content = `# Dev Architecture Analysis - -## Summary - -${analysis.moduleCount} modules were analyzed with dependency-cruiser, ts-prune, and ESLint. The dependency graph has ${analysis.dependencyGraph.nodes} local nodes and ${analysis.dependencyGraph.edges} local edges, with a coupling index of ${analysis.dependencyGraph.couplingIndex}. - -## Structural Metrics - -- Number of modules: ${analysis.moduleCount} -- Dependency graph: ${analysis.dependencyGraph.nodes} local nodes / ${analysis.dependencyGraph.edges} local edges -- Coupling index: ${analysis.dependencyGraph.couplingIndex} -- Circular dependencies: ${analysis.dependencyGraph.circularDependencies.length} -- Unused exports: ${analysis.unusedExports.length} -- Largest modules over 500 lines: ${analysis.eslintSummary.oversizedFiles.length} - -## Top 10 Architecture Risks - -${renderRiskList(analysis.topArchitectureRisks)} - -## Refactoring Suggestions - -${renderList(recommendations)} - -## Modules With Highest Complexity - -${renderMetricList(analysis.complexityHotspots)} - -## Modules Recommended For Isolation - -${renderMetricList(analysis.isolationCandidates)} - -## Architectural Observations - -${renderList(analysis.architectureObservations)} - -## Static Analysis Snapshot - -- dependency-cruiser: ${analysis.notes[0]} -- ts-prune: ${analysis.notes[1]} -- ESLint: ${analysis.notes[2]} -- Circular dependency paths: ${ - analysis.dependencyGraph.circularDependencies.length > 0 - ? analysis.dependencyGraph.circularDependencies.map((path) => path.join(" -> ")).join("; ") - : "None" - } -- Largest modules: ${analysis.largestModules.map((metric) => `${metric.filePath} (${metric.lineCount} lines)`).join(", ") || "None"} -- Highest change hotspots: ${analysis.changeHotspots.map((metric) => `${metric.filePath} (${metric.changeFrequency})`).join(", ") || "None"} -- Missing logging candidates: ${analysis.missingLogging.map((metric) => metric.filePath).join(", ") || "None"} -- Missing error handling candidates: ${analysis.missingErrorHandling.map((metric) => metric.filePath).join(", ") || "None"} -- Unused exports: ${ - analysis.unusedExports.length > 0 - ? analysis.unusedExports.map((entry) => `${entry.filePath} -> ${entry.symbol}`).join(", ") - : "None" - } -`; - - return { - title: "Dev Architecture Analysis", - summary: "DevAgent evaluated architectural hotspots, static-analysis findings, and code-level maintainability risks.", - findings, - recommendations, - riskLevel, - content - }; - } -} diff --git a/agents/documentation_agent/index.ts b/agents/documentation_agent/index.ts deleted file mode 100644 index 6b48361..0000000 --- a/agents/documentation_agent/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -import path from "node:path"; - -import { BaseAgent, renderAgentReport } from "../base-agent"; -import { writeFileEnsured } from "../../shared/fs-utils"; -import type { AgentEvaluation, AgentReport, ProjectContext } from "../../shared/types"; - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function buildArchitectureDoc(context: ProjectContext): string { - const { discovery } = context; - return `# Architecture - -## Overview - -- Repository: ${context.repoName} -- Project type: ${discovery.frameworks.join(", ") || discovery.languages.join(", ") || "Unknown"} -- Languages: ${discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${discovery.frameworks.join(", ") || "Unknown"} -- Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"} - -## Structure - -${renderList(discovery.structure.topLevelDirectories)} -`; -} - -function buildApiDoc(context: ProjectContext): string { - const { discovery } = context; - return `# API - -## Detected styles - -${renderList(discovery.apis)} - -## Related files - -${renderList(discovery.apiFiles)} -`; -} - -function buildRunbook(context: ProjectContext): string { - const { discovery } = context; - return `# Runbook - -## Operating notes - -- CI/CD: ${discovery.ci.providers.join(", ") || "Not detected"} -- Logging: ${discovery.logging.frameworks.join(", ") || "Not detected"} -- Metrics: ${discovery.metrics.tools.join(", ") || "Not detected"} -- Alerts: ${discovery.metrics.alertsConfigured ? "Detected" : "Not detected"} - -## Suggested operational actions - -${renderList(discovery.recommendations)} -`; -} - -export class DocumentationAgent extends BaseAgent { - constructor() { - super("documentation-agent", "documentation_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const deterministicFindings: string[] = []; - const recommendations: string[] = []; - - if (!context.discovery.files.some((file) => file.startsWith("docs/"))) { - deterministicFindings.push("No documentation directory was detected in the target repository."); - recommendations.push("Maintain generated docs alongside hand-written operating knowledge."); - } - - const aiResponse = await this.requestStructuredAI(context, { - task: "documentation-review", - systemPromptFile: "documentation.system.md", - analysisPrompt: [ - `Review the repository documentation posture and operational readability.`, - `Docs detected: ${context.discovery.files.some((file) => file.startsWith("docs/")) ? "yes" : "no"}.`, - `API files: ${context.discovery.apiFiles.join(", ") || "None"}.`, - `CI providers: ${context.discovery.ci.providers.join(", ") || "Not detected"}.`, - `Observability: logging=${context.discovery.logging.frameworks.join(", ") || "none"}, metrics=${context.discovery.metrics.tools.join(", ") || "none"}.`, - `Deterministic findings: ${deterministicFindings.join(" | ") || "None"}.` - ].join("\n") - }); - - return this.buildAIEnhancedEvaluation( - { - title: "Documentation Report", - summary: "DocumentationAgent generated architecture, API, and runbook documents.", - deterministicFindings, - recommendations, - riskLevel: "low" - }, - aiResponse - ); - } - - async run(context: ProjectContext): Promise { - const evaluation = await this.evaluate(context); - - await writeFileEnsured(path.join(context.docsDir, "architecture.md"), buildArchitectureDoc(context)); - await writeFileEnsured(path.join(context.docsDir, "api.md"), buildApiDoc(context)); - await writeFileEnsured(path.join(context.docsDir, "runbook.md"), buildRunbook(context)); - - const outputPath = path.join(context.reportsDir, "documentation_report.md"); - await writeFileEnsured(outputPath, renderAgentReport(evaluation)); - - return { - agentId: this.agentId, - title: evaluation.title, - summary: evaluation.summary, - findings: evaluation.findings, - recommendations: evaluation.recommendations, - riskLevel: evaluation.riskLevel, - outputPath - }; - } -} diff --git a/agents/infra_agent/index.ts b/agents/infra_agent/index.ts deleted file mode 100644 index 2889229..0000000 --- a/agents/infra_agent/index.ts +++ /dev/null @@ -1,171 +0,0 @@ -import path from "node:path"; - -import { BaseAgent } from "../base-agent"; -import { readTextSafe } from "../../shared/fs-utils"; - -import type { - AgentEvaluation, - ProjectContext, - SecurityCoverageStatus, - SecurityFinding -} from "../../shared/types"; - -const DOCKERFILE_PATTERN = /(^|\/)Dockerfile$/i; -const COMPOSE_PATTERN = /(^|\/)docker-compose[^/]*\.ya?ml$/i; -const HEADER_SIGNAL_PATTERN = /(^|\/)(middleware|next\.config|server|helmet|headers?)\.(ts|tsx|js|jsx|mjs|cjs)$/i; - -function pushFinding( - target: SecurityFinding[], - finding: SecurityFinding, - findings: string[], - recommendations: string[] -): void { - target.push(finding); - findings.push(`[${finding.severity.toUpperCase()}] ${finding.title}: ${finding.impact}`); - recommendations.push(finding.fix); -} - -export class InfraAgent extends BaseAgent { - constructor() { - super("infra-agent", "infra_security_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const securityFindings: SecurityFinding[] = []; - const coverage: SecurityCoverageStatus[] = []; - const dockerfiles = context.discovery.files.filter((filePath) => DOCKERFILE_PATTERN.test(filePath)); - const composeFiles = context.discovery.files.filter((filePath) => COMPOSE_PATTERN.test(filePath)); - const headerSignals = context.discovery.files.filter((filePath) => HEADER_SIGNAL_PATTERN.test(filePath)); - - for (const filePath of dockerfiles) { - const content = await readTextSafe(path.join(context.targetPath, filePath)); - if (!/^\s*USER\s+/m.test(content)) { - pushFinding( - securityFindings, - { - area: "infra_config", - severity: "medium", - title: "Contenedor sin usuario no-root explícito", - location: filePath, - evidence: "No se detectó instrucción `USER` en el Dockerfile inspeccionado.", - attackVector: [ - "Una vulnerabilidad en la app o dependencia permite ejecución dentro del contenedor.", - "El proceso corre como root por defecto.", - "El atacante amplía el impacto del compromiso dentro del runtime o volúmenes montados." - ], - impact: "Mayor severidad de compromisos dentro del contenedor y menor hardening por defecto en producción.", - fix: "Agregar un usuario dedicado y cambiar a él antes del entrypoint, por ejemplo: `RUN addgroup -S app && adduser -S app -G app` seguido de `USER app`.", - references: ["CWE-250", "OWASP A05:2021", "ASVS 14.4.1"], - effort: "low", - problemType: "configuration", - agentId: this.agentId - }, - findings, - recommendations - ); - } - } - - for (const filePath of composeFiles) { - const content = await readTextSafe(path.join(context.targetPath, filePath)); - if (/privileged\s*:\s*true/i.test(content)) { - pushFinding( - securityFindings, - { - area: "infra_config", - severity: "high", - title: "Servicio Docker Compose con modo privilegiado", - location: filePath, - evidence: "Se detectó `privileged: true` en la configuración de Compose.", - attackVector: [ - "Un atacante compromete el proceso dentro del contenedor privilegiado.", - "Obtiene capacidades ampliadas sobre el host.", - "Escala el compromiso fuera del contenedor." - ], - impact: "Aumento fuerte del blast radius entre contenedor y host.", - fix: "Eliminar `privileged: true` y otorgar solo capacidades mínimas imprescindibles mediante `cap_add` específico o rediseño del servicio.", - references: ["CWE-250", "OWASP A05:2021", "ASVS 14.4.1"], - effort: "medium", - problemType: "configuration", - agentId: this.agentId - }, - findings, - recommendations - ); - } - - if (/\/var\/run\/docker\.sock/i.test(content)) { - pushFinding( - securityFindings, - { - area: "infra_config", - severity: "high", - title: "Montaje de Docker socket dentro del contenedor", - location: filePath, - evidence: "Se detectó referencia a `/var/run/docker.sock` en la configuración revisada.", - attackVector: [ - "El atacante compromete el contenedor con acceso al socket.", - "Controla el daemon Docker del host.", - "Crea o manipula contenedores adicionales con privilegios del host." - ], - impact: "Control casi total del host Docker desde un contenedor comprometido.", - fix: "Eliminar el montaje del socket y reemplazarlo por un servicio intermedio o API con permisos mínimos específicos.", - references: ["CWE-732", "OWASP A05:2021", "ASVS 14.4.1"], - effort: "medium", - problemType: "configuration", - agentId: this.agentId - }, - findings, - recommendations - ); - } - } - - coverage.push({ - area: "http_headers", - status: headerSignals.length > 0 ? "ok" : "not-reviewed", - note: - headerSignals.length > 0 - ? `Se detectaron puntos de configuración potencial para headers en: ${headerSignals.slice(0, 6).join(", ")}` - : "No se confirmó una capa explícita de configuración de security headers; verificar middleware, reverse proxy o plataforma de despliegue.", - agentId: this.agentId - }); - - coverage.push({ - area: "infra_config", - status: securityFindings.some((entry) => entry.area === "infra_config") - ? "finding" - : dockerfiles.length > 0 || composeFiles.length > 0 || context.discovery.infrastructure.length > 0 - ? "ok" - : "not-reviewed", - note: - dockerfiles.length > 0 || composeFiles.length > 0 || context.discovery.infrastructure.length > 0 - ? `Superficie de infraestructura revisada: ${[...dockerfiles, ...composeFiles, ...context.discovery.infraFiles].slice(0, 8).join(", ") || context.discovery.infrastructure.join(", ")}` - : "No se detectó infraestructura local suficiente para revisar hardening de contenedores o despliegue.", - agentId: this.agentId - }); - - coverage.push({ - area: "web_attacks", - status: "not-reviewed", - note: "InfraAgent no confirmó mitigaciones de SSRF, CSRF o XSS desde proxy o plataforma; revisar backend, middleware y frontend por separado.", - agentId: this.agentId - }); - - return { - title: "Infra Security Report", - summary: "InfraAgent revisó hardening de contenedores, Compose y señales de configuración de headers para detectar riesgos de despliegue.", - findings, - recommendations, - riskLevel: securityFindings.some((entry) => entry.severity === "high") - ? "high" - : securityFindings.some((entry) => entry.severity === "medium") - ? "medium" - : "low", - securityFindings, - coverage - }; - } -} diff --git a/agents/legal_agent/index.ts b/agents/legal_agent/index.ts deleted file mode 100644 index 69aa8c4..0000000 --- a/agents/legal_agent/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext } from "../../shared/types"; - -export class LegalAgent extends BaseAgent { - constructor() { - super("legal-agent", "legal_updates.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const { discovery } = context; - const lowerFiles = discovery.files.map((file) => file.toLowerCase()); - - if (!lowerFiles.includes("license") && !lowerFiles.includes("license.md")) { - findings.push("No repository license file was detected."); - recommendations.push("Add an explicit project license and document any distribution constraints."); - } - - if (!lowerFiles.some((file) => file.includes("notice") || file.includes("third_party"))) { - findings.push("No third-party notice or attribution artifact was detected."); - recommendations.push("Generate a dependency attribution file for compliance and due diligence."); - } - - if (discovery.dependencies.length > 0) { - recommendations.push("Run a dependency license audit as part of the release workflow."); - } - - return { - title: "Legal Updates", - summary: "LegalAgent reviewed licensing and attribution hygiene signals.", - findings, - recommendations, - riskLevel: findings.length >= 2 ? "medium" : findings.length === 1 ? "low" : "low" - }; - } -} diff --git a/agents/observability_agent/index.ts b/agents/observability_agent/index.ts deleted file mode 100644 index 13ad4ac..0000000 --- a/agents/observability_agent/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext, SecurityCoverageStatus } from "../../shared/types"; - -export class ObservabilityAgent extends BaseAgent { - constructor() { - super("observability-agent", "observability_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const coverage: SecurityCoverageStatus[] = []; - const { discovery } = context; - - if (discovery.logging.frameworks.length === 0) { - findings.push("No dedicated logging framework was detected."); - recommendations.push("Adopt structured application logging with correlation-friendly fields."); - } - - if (discovery.metrics.tools.length === 0) { - findings.push("No metrics or tracing integration was detected."); - recommendations.push("Instrument core request, job, and database paths with metrics or traces."); - } - - if (!discovery.metrics.alertsConfigured) { - findings.push("No alerting configuration was detected."); - recommendations.push("Define alert thresholds for latency, error rate, and infrastructure saturation."); - } - - coverage.push({ - area: "observability", - status: findings.length > 0 ? "finding" : "ok", - note: - findings.length > 0 - ? `Hallazgos observability: ${findings.join(" | ")}` - : "Se confirmaron señales razonables de logging, métricas y alerting para investigación operativa.", - agentId: this.agentId - }); - - return { - title: "Observability Report", - summary: "ObservabilityAgent checked logging, metrics, and alert readiness.", - findings, - recommendations, - riskLevel: findings.length >= 2 ? "medium" : findings.length === 1 ? "low" : "low", - coverage - }; - } -} diff --git a/agents/optimization_agent/index.ts b/agents/optimization_agent/index.ts deleted file mode 100644 index 4c2aaba..0000000 --- a/agents/optimization_agent/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext } from "../../shared/types"; - -export class OptimizationAgent extends BaseAgent { - constructor() { - super("optimization-agent", "optimization_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const deterministicFindings: string[] = []; - const recommendations: string[] = []; - const { discovery } = context; - const dependencyCount = new Set( - discovery.dependencies.flatMap((manifest) => manifest.dependencies.map((dependency) => dependency.toLowerCase())) - ).size; - - if (dependencyCount > 80) { - deterministicFindings.push(`High dependency surface detected (${dependencyCount} unique packages).`); - recommendations.push("Review unused packages and separate production dependencies from tooling dependencies."); - } - - if (discovery.infrastructure.includes("Dockerfile") && discovery.dockerStageCount === 1) { - deterministicFindings.push("Docker builds appear single-stage, which often increases image size and attack surface."); - recommendations.push("Adopt a multi-stage Docker build to reduce runtime footprint."); - } - - if (discovery.structure.sourceFileCount > 250 && discovery.ci.providers.length === 0) { - deterministicFindings.push("Large codebase detected without CI acceleration or caching signals."); - recommendations.push("Add build caching, parallel quality gates, and dependency pruning to keep cycle time stable."); - } - - const aiResponse = await this.requestStructuredAI(context, { - task: "performance-analysis", - systemPromptFile: "optimization.system.md", - analysisPrompt: [ - `Review the repository for low-risk optimization opportunities.`, - `Unique dependencies: ${dependencyCount}.`, - `Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"}.`, - `Docker stages: ${discovery.dockerStageCount}.`, - `Source files: ${discovery.structure.sourceFileCount}.`, - `CI providers: ${discovery.ci.providers.join(", ") || "Not detected"}.`, - `Deterministic findings: ${deterministicFindings.join(" | ") || "None"}.` - ].join("\n") - }); - - return this.buildAIEnhancedEvaluation( - { - title: "Optimization Report", - summary: "OptimizationAgent evaluated dependency weight and deployment efficiency signals.", - deterministicFindings, - recommendations, - riskLevel: deterministicFindings.length >= 2 ? "medium" : deterministicFindings.length === 1 ? "low" : "low" - }, - aiResponse - ); - } -} diff --git a/agents/product_agent/index.ts b/agents/product_agent/index.ts deleted file mode 100644 index 2009003..0000000 --- a/agents/product_agent/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext } from "../../shared/types"; - -export class ProductAgent extends BaseAgent { - constructor() { - super("product-agent", "product_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const { discovery } = context; - - if (!discovery.files.some((file) => file.toLowerCase() === "readme.md")) { - findings.push("Repository onboarding starts without a root README."); - recommendations.push("Create a concise README with setup, architecture, and operating flows."); - } - - if (discovery.ci.providers.length === 0) { - findings.push("No CI/CD pipeline was detected, which increases operational friction."); - recommendations.push("Introduce a CI baseline that runs quality gates on every change."); - } - - if (discovery.apis.includes("REST") && !discovery.apis.includes("OpenAPI")) { - findings.push("REST surfaces exist without a visible API contract."); - recommendations.push("Publish and version an OpenAPI contract for developer and product alignment."); - } - - if (discovery.structure.subrepos.length > 1 && !discovery.files.some((file) => file.startsWith("docs/"))) { - findings.push("The repository appears multi-package but lacks navigational product documentation."); - recommendations.push("Document bounded contexts, ownership, and cross-package workflows."); - } - - return { - title: "Improvement Proposals", - summary: `Identified ${findings.length || 1} product and delivery opportunities across the repository.`, - findings, - recommendations, - riskLevel: findings.length >= 3 ? "medium" : "low" - }; - } -} diff --git a/agents/product_owner_agent/index.ts b/agents/product_owner_agent/index.ts deleted file mode 100644 index 08ef12d..0000000 --- a/agents/product_owner_agent/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext } from "../../shared/types"; - -export class ProductOwnerAgent extends BaseAgent { - constructor() { - super("product-owner-agent", "product_owner_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const { discovery } = context; - - if (!discovery.files.some((file) => file.toLowerCase() === "readme.md")) { - findings.push("Repository onboarding starts without a root README."); - recommendations.push("Create a concise README with setup, architecture, and operating flows."); - } - - if (discovery.ci.providers.length === 0) { - findings.push("No CI/CD pipeline was detected, which increases operational friction."); - recommendations.push("Introduce a CI baseline that runs quality gates on every change."); - } - - if (discovery.apis.includes("REST") && !discovery.apis.includes("OpenAPI")) { - findings.push("REST surfaces exist without a visible API contract."); - recommendations.push("Publish and version an OpenAPI contract for developer and product alignment."); - } - - if (discovery.structure.subrepos.length > 1 && !discovery.files.some((file) => file.startsWith("docs/"))) { - findings.push("The repository appears multi-package but lacks navigational product documentation."); - recommendations.push("Document bounded contexts, ownership, and cross-package workflows."); - } - - return { - title: "Improvement Proposals", - summary: `Identified ${findings.length || 1} product and delivery opportunities across the repository.`, - findings, - recommendations, - riskLevel: findings.length >= 3 ? "medium" : "low" - }; - } -} diff --git a/agents/prompts/architect.system.md b/agents/prompts/architect.system.md deleted file mode 100644 index 132c4ed..0000000 --- a/agents/prompts/architect.system.md +++ /dev/null @@ -1,26 +0,0 @@ -You are a senior software architect specialized in large engineering systems. - -Goal: -Identify structural architecture risks, boundary issues, and redesign opportunities. - -Focus on: -- module boundaries -- ownership clarity -- integration complexity -- deployment architecture drift -- documentation gaps that block safe evolution - -Output format: - -{ - "issues": [ - { "severity": "high", "description": "..." } - ], - "proposed_improvements": [ - { "type": "architecture", "proposal": "..." } - ] -} - -Do not generate code. -Only produce structured analysis. -Respond with JSON only. diff --git a/agents/prompts/coder.system.md b/agents/prompts/coder.system.md deleted file mode 100644 index 0a0d189..0000000 --- a/agents/prompts/coder.system.md +++ /dev/null @@ -1,26 +0,0 @@ -You are a senior engineering analysis agent specialized in maintainability and refactoring strategy. - -Goal: -Review engineering structure and identify safe improvement directions. - -Focus on: -- code organization -- maintainability risks -- naming clarity -- duplication -- low-risk improvement opportunities - -Output format: - -{ - "issues": [ - { "severity": "medium", "description": "..." } - ], - "proposed_improvements": [ - { "type": "refactor", "proposal": "..." } - ] -} - -Do not generate code. -Only produce structured analysis. -Respond with JSON only. diff --git a/agents/prompts/documentation.system.md b/agents/prompts/documentation.system.md deleted file mode 100644 index 00d3d45..0000000 --- a/agents/prompts/documentation.system.md +++ /dev/null @@ -1,26 +0,0 @@ -You are a senior technical documentation strategist. - -Goal: -Identify documentation gaps that slow understanding, onboarding, and operations. - -Focus on: -- architecture clarity -- API discoverability -- runbook completeness -- operational readability -- developer onboarding friction - -Output format: - -{ - "issues": [ - { "severity": "medium", "description": "..." } - ], - "proposed_improvements": [ - { "type": "documentation", "proposal": "..." } - ] -} - -Do not generate code. -Only produce structured analysis. -Respond with JSON only. diff --git a/agents/prompts/optimization.system.md b/agents/prompts/optimization.system.md deleted file mode 100644 index 09e2e72..0000000 --- a/agents/prompts/optimization.system.md +++ /dev/null @@ -1,26 +0,0 @@ -You are a senior optimization architect focused on performance, delivery efficiency, and runtime cost. - -Goal: -Detect optimization risks and low-risk performance improvements. - -Focus on: -- dependency weight -- build efficiency -- runtime footprint -- delivery friction -- operational waste - -Output format: - -{ - "issues": [ - { "severity": "medium", "description": "..." } - ], - "proposed_improvements": [ - { "type": "performance", "proposal": "..." } - ] -} - -Do not generate code. -Only produce structured analysis. -Respond with JSON only. diff --git a/agents/prompts/qa.system.md b/agents/prompts/qa.system.md deleted file mode 100644 index 9723dda..0000000 --- a/agents/prompts/qa.system.md +++ /dev/null @@ -1,26 +0,0 @@ -You are a senior QA architect specialized in engineering quality and regression prevention. - -Goal: -Detect testing gaps, validation blind spots, and release-safety risks. - -Focus on: -- missing automated tests -- weak coverage signals -- API validation gaps -- regression exposure -- testability of critical flows - -Output format: - -{ - "issues": [ - { "severity": "high", "description": "..." } - ], - "proposed_improvements": [ - { "type": "testing", "proposal": "..." } - ] -} - -Do not generate code. -Only produce structured analysis. -Respond with JSON only. diff --git a/agents/prompts/ux-improvement.system.md b/agents/prompts/ux-improvement.system.md deleted file mode 100644 index 15b41d8..0000000 --- a/agents/prompts/ux-improvement.system.md +++ /dev/null @@ -1,50 +0,0 @@ -You are a senior ERP UX improvement architect. - -Goal: -Convert operational UX findings into implementation-ready frontend improvement guidance for non-technical administrative staff. - -Primary user: -- government administrative staff -- non-technical -- repetitive form-based work -- needs minimal steps and clear language - -Critical rule: -- prioritize functional usability and workflow clarity over visual design - -Ignore completely: -- README files -- onboarding guides -- installation instructions -- developer documentation -- backend changes -- OpenAPI changes -- Prisma changes - -Focus only on: -- component-level UI friction -- navigation simplification -- sidebar grouping -- dashboard clarity -- form simplification -- terminology clarity -- table usability -- search and filter usability -- workflow visibility -- error clarity -- dropdown/select usage instead of raw inputs - -Output format: - -{ - "issues": [ - { "severity": "high", "description": "..." } - ], - "proposed_improvements": [ - { "type": "navigation", "proposal": "..." } - ] -} - -Do not generate code. -Do not propose backend or database changes. -Only produce structured analysis for frontend improvements. diff --git a/agents/prompts/ux.system.md b/agents/prompts/ux.system.md deleted file mode 100644 index a053641..0000000 --- a/agents/prompts/ux.system.md +++ /dev/null @@ -1,47 +0,0 @@ -You are a senior ERP UX architect specialized in software used by non-technical government administrative staff. - -Goal: -Detect operational interface usability problems in ERP frontends. - -Primary user: -- government administrative staff -- non-technical -- repetitive form-based work -- needs minimal steps and clear language - -Critical rule: -- prioritize functional usability and workflow clarity over visual design - -Ignore completely: -- README files -- onboarding guides -- installation instructions -- developer documentation -- contributor notes - -Focus only on: -- navigation -- sidebar menu structure -- dashboard clarity -- form complexity -- label and terminology clarity -- table usability -- search and filtering -- workflow visibility -- error clarity -- dropdown/select usage instead of raw inputs - -Output format: - -{ - "issues": [ - { "severity": "high", "description": "..." } - ], - "proposed_improvements": [ - { "type": "navigation", "proposal": "..." } - ] -} - -Do not generate code. -Only produce structured analysis. -Respond with JSON only. diff --git a/agents/qa_agent/index.ts b/agents/qa_agent/index.ts deleted file mode 100644 index 5f0db22..0000000 --- a/agents/qa_agent/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { AgentEvaluation, ProjectContext, SecurityCoverageStatus } from "../../shared/types"; - -export class QAAgent extends BaseAgent { - constructor() { - super("qa-agent", "qa_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const deterministicFindings: string[] = []; - const recommendations: string[] = []; - const coverage: SecurityCoverageStatus[] = []; - const { discovery } = context; - const testFiles = discovery.structure.testFileCount; - const sourceFiles = discovery.structure.sourceFileCount; - - if (discovery.testing.length === 0) { - deterministicFindings.push("No automated test framework was detected."); - recommendations.push("Adopt a baseline automated test framework aligned with the primary runtime."); - } - - if (sourceFiles > 20 && testFiles === 0) { - deterministicFindings.push("The repository has source-heavy areas without any test files."); - recommendations.push("Start with smoke tests around the highest-change modules and API entry points."); - } - - if (sourceFiles > 0 && testFiles > 0 && sourceFiles / testFiles > 8) { - deterministicFindings.push("The test-to-source ratio suggests thin coverage on critical paths."); - recommendations.push("Expand coverage on authentication, data access, and integration seams first."); - } - - if ((discovery.apis.includes("REST") || discovery.apis.includes("GraphQL")) && testFiles === 0) { - deterministicFindings.push("Exposed API surfaces appear to be untested."); - recommendations.push("Add route-level or schema-level contract tests for the public API."); - } - - const aiResponse = await this.requestStructuredAI(context, { - task: "qa-analysis", - systemPromptFile: "qa.system.md", - analysisPrompt: [ - `Review the repository for testing and release-safety risks.`, - `Source files: ${sourceFiles}.`, - `Test files: ${testFiles}.`, - `Testing frameworks: ${discovery.testing.join(", ") || "Not detected"}.`, - `APIs: ${discovery.apis.join(", ") || "Not detected"}.`, - `Deterministic findings: ${deterministicFindings.join(" | ") || "None"}.` - ].join("\n") - }); - - coverage.push({ - area: "abuse_protection", - status: testFiles === 0 ? "not-reviewed" : "ok", - note: - testFiles === 0 - ? "No se confirmaron tests suficientes para validar rate limiting, brute force o anti-abuso en rutas sensibles." - : `Se detectaron ${testFiles} archivos de test, pero la cobertura específica contra abuso debe confirmarse por suite o naming.`, - agentId: this.agentId - }); - - return this.buildAIEnhancedEvaluation( - { - title: "QA Report", - summary: `QAAgent reviewed ${sourceFiles} source files and ${testFiles} test files.`, - deterministicFindings, - recommendations, - riskLevel: deterministicFindings.length >= 2 ? "high" : deterministicFindings.length === 1 ? "medium" : "low", - coverage - }, - aiResponse - ); - } -} diff --git a/agents/security_agent/index.ts b/agents/security_agent/index.ts deleted file mode 100644 index 213e3b3..0000000 --- a/agents/security_agent/index.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { BaseAgent } from "../base-agent"; - -import type { - AgentEvaluation, - ProjectContext, - SecurityCoverageStatus, - SecurityFinding -} from "../../shared/types"; - -const LOCKFILES = [ - "package-lock.json", - "pnpm-lock.yaml", - "yarn.lock", - "poetry.lock", - "Pipfile.lock", - "go.sum", - "Cargo.lock" -]; - -function pushFinding( - target: SecurityFinding[], - finding: SecurityFinding, - findings: string[], - recommendations: string[] -): void { - target.push(finding); - findings.push(`[${finding.severity.toUpperCase()}] ${finding.title}: ${finding.impact}`); - recommendations.push(finding.fix); -} - -export class SecurityAgent extends BaseAgent { - constructor() { - super("security-agent", "security_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const findings: string[] = []; - const recommendations: string[] = []; - const securityFindings: SecurityFinding[] = []; - const coverage: SecurityCoverageStatus[] = []; - const { discovery } = context; - - const riskyFiles = discovery.files.filter( - (file) => - /(^|\/)\.env($|[^/])/.test(file) || - /\.(pem|key)$/i.test(file) || - /id_rsa|credentials|secret/i.test(file) - ).filter((file) => !/\.example$|\.sample$|\.template$/i.test(file)); - - if (riskyFiles.length > 0) { - pushFinding( - securityFindings, - { - area: "sensitive_data", - severity: "high", - title: "Secretos o material sensible versionado en el repositorio", - location: riskyFiles.slice(0, 5).join(", "), - evidence: `Se detectaron archivos con patrón sensible: ${riskyFiles.slice(0, 5).join(", ")}.`, - attackVector: [ - "Un atacante obtiene acceso al repositorio o a un artefacto que lo replique.", - "Lee el archivo sensible comprometido.", - "Reutiliza secretos, claves o credenciales contra entornos reales." - ], - impact: "Exposición de credenciales, secretos operativos o llaves privadas reutilizables.", - fix: "Mover los secretos a un vault o variables de entorno del despliegue, rotarlos y agregar reglas de ignore para impedir nuevos commits sensibles.", - references: ["CWE-798", "OWASP A05:2021", "ASVS 8.1.1"], - effort: "medium", - problemType: "configuration", - agentId: this.agentId - }, - findings, - recommendations - ); - coverage.push({ - area: "sensitive_data", - status: "finding", - note: `Archivos sensibles confirmados: ${riskyFiles.slice(0, 5).join(", ")}`, - agentId: this.agentId - }); - } else { - coverage.push({ - area: "sensitive_data", - status: "ok", - note: "No se confirmaron archivos de secretos versionados con patrones directos en el escaneo del repositorio.", - agentId: this.agentId - }); - } - - if (discovery.dependencies.length > 0 && !LOCKFILES.some((lockfile) => discovery.files.includes(lockfile))) { - pushFinding( - securityFindings, - { - area: "infra_config", - severity: "medium", - title: "Dependencias sin lockfile versionado", - location: discovery.manifests.slice(0, 5).join(", ") || "package manifests", - evidence: `Se detectaron manifests de dependencias sin lockfile asociado. Manifests: ${discovery.manifests.join(", ") || "desconocidos"}.`, - attackVector: [ - "Una instalación futura resuelve versiones diferentes a las evaluadas.", - "Se incorpora una versión vulnerable o maliciosa.", - "El entorno ejecuta código distinto al revisado." - ], - impact: "Menor reproducibilidad y mayor riesgo de supply-chain drift en build o runtime.", - fix: "Generar y versionar el lockfile correspondiente (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, etc.) para fijar resoluciones reproducibles.", - references: ["CWE-1104", "OWASP A06:2021", "ASVS 14.2.1"], - effort: "low", - problemType: "configuration", - agentId: this.agentId - }, - findings, - recommendations - ); - } - - if (discovery.infrastructure.includes("Dockerfile") && !discovery.files.includes(".dockerignore")) { - pushFinding( - securityFindings, - { - area: "infra_config", - severity: "low", - title: "Dockerfile sin `.dockerignore` defensivo", - location: "Dockerfile", - evidence: "Se detectó Dockerfile pero no `.dockerignore` en la raíz del repositorio.", - attackVector: [ - "El build context incluye archivos innecesarios o sensibles.", - "La imagen copia contenido no destinado a runtime.", - "Datos internos terminan dentro del artefacto desplegado." - ], - impact: "Mayor riesgo de filtración de secretos, metadata del repo o artefactos internos dentro de imágenes.", - fix: "Agregar `.dockerignore` para excluir secretos, `.git`, `node_modules`, salidas de build y archivos locales no destinados a imagen.", - references: ["CWE-668", "OWASP A05:2021", "ASVS 14.4.3"], - effort: "low", - problemType: "configuration", - agentId: this.agentId - }, - findings, - recommendations - ); - } - - if (!coverage.some((entry) => entry.area === "infra_config")) { - coverage.push({ - area: "infra_config", - status: securityFindings.some((entry) => entry.area === "infra_config") ? "finding" : "ok", - note: securityFindings.some((entry) => entry.area === "infra_config") - ? "Se detectaron riesgos concretos de configuración o supply chain." - : "No se confirmaron hallazgos directos de configuración en secretos, lockfiles o hygiene básica de contenedores.", - agentId: this.agentId - }); - } - - return { - title: "Security Report", - summary: "SecurityAgent evaluó exposición de secretos, higiene de dependencias y señales básicas de hardening de contenedores.", - findings, - recommendations, - riskLevel: securityFindings.some((entry) => entry.severity === "high") - ? "high" - : securityFindings.some((entry) => entry.severity === "medium") - ? "medium" - : "low", - securityFindings, - coverage - }; - } -} diff --git a/agents/ux_agent/index.ts b/agents/ux_agent/index.ts deleted file mode 100644 index 29831f9..0000000 --- a/agents/ux_agent/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { BaseAgent } from "../base-agent"; -import { normalizeAIImprovement, normalizeAIInsight } from "../ai-support"; -import { - analyzeFrontendUsability, - filterOperationalUXItems, - formatFrontendUsabilityAnalysis -} from "../../analysis/ux_task_generator"; - -import type { AgentEvaluation, ProjectContext } from "../../shared/types"; - -export class UXAgent extends BaseAgent { - constructor() { - super("ux-agent", "ux_report.md"); - } - - protected async evaluate(context: ProjectContext): Promise { - const analysis = await analyzeFrontendUsability(context.targetPath); - const deterministicFindings = filterOperationalUXItems(analysis.findings); - const recommendations = filterOperationalUXItems(analysis.recommendations); - - if (!analysis.frontendDetected) { - return { - title: "UX Report", - summary: "UXAgent skipped operational UX analysis because no frontend UI surface was detected under the expected frontend source roots.", - findings: ["No frontend UI surface was detected under src/components, src/app, src/layouts, src/pages, or src/features."], - recommendations: [], - riskLevel: "low" - }; - } - - const aiResponse = await this.requestStructuredAI(context, { - task: "ux-audit", - systemPromptFile: "ux.system.md", - analysisPrompt: [ - "Evaluate the repository only for operational ERP usability in the existing UI.", - "Primary users: non-technical government administrative staff performing repetitive form-based work.", - "Critical rule: prioritize functional usability and workflow clarity over visual design.", - "Ignore README files, onboarding guides, installation instructions, and developer documentation.", - "Focus only on navigation, sidebar structure, dashboards, forms, terminology, tables, search/filtering, workflow visibility, error clarity, and guided selectors.", - formatFrontendUsabilityAnalysis(analysis), - `Deterministic findings: ${deterministicFindings.join(" | ") || "None"}.` - ].join("\n") - }); - const filteredAIResponse = aiResponse - ? { - issues: aiResponse.issues.filter((issue) => filterOperationalUXItems([normalizeAIInsight(issue)]).length > 0), - proposed_improvements: aiResponse.proposed_improvements.filter( - (improvement) => filterOperationalUXItems([normalizeAIImprovement(improvement)]).length > 0 - ) - } - : undefined; - - return this.buildAIEnhancedEvaluation( - { - title: "UX Report", - summary: - "UXAgent evaluated operational interface usability for non-technical administrative staff, focusing on navigation, forms, workflow clarity, tables, and search/filter surfaces.", - deterministicFindings, - recommendations, - riskLevel: deterministicFindings.length >= 4 ? "high" : deterministicFindings.length >= 2 ? "medium" : "low" - }, - filteredAIResponse - ); - } -} diff --git a/agents/ux_improvement_agent/index.ts b/agents/ux_improvement_agent/index.ts deleted file mode 100644 index 8b64ce8..0000000 --- a/agents/ux_improvement_agent/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { BaseAgent } from "../base-agent"; -import { - analyzeFrontendUsability, - filterOperationalUXItems, - formatFrontendUsabilityAnalysis, - formatComponentCatalog, - generateUXImprovementArtifacts, - loadUXImprovementInputs -} from "../../analysis/ux_task_generator"; -import { - combineRecommendations, - mergeRiskLevel, - normalizeAIImprovement, - normalizeAIInsight -} from "../ai-support"; - -import type { AgentEvaluation, AgentReport, ProjectContext } from "../../shared/types"; - -interface UXImprovementPlan { - evaluation: AgentEvaluation; - findings: string[]; - recommendations: string[]; - inputFiles: string[]; - emptyMessage?: string; -} - -function summarizeItems(items: string[], fallback: string): string { - return items.length > 0 ? items.join(" | ") : fallback; -} - -export class UXImprovementAgent extends BaseAgent { - constructor() { - super("ux-improvement-agent", "UX_IMPLEMENTATION_TASKS.md"); - } - - private async buildPlan(context: ProjectContext): Promise { - const inputs = await loadUXImprovementInputs(context); - const analysis = await analyzeFrontendUsability(context.targetPath); - - if (!inputs.frontendDetected) { - return { - evaluation: { - title: "UX Improvement Tasks", - summary: - "UXImprovementAgent skipped task generation because no frontend source surface was detected under the expected UI roots.", - findings: ["No frontend component surface was detected under src/components, src/app, src/layouts, src/pages, or src/features."], - recommendations: [], - riskLevel: "low" - }, - findings: [], - recommendations: [], - inputFiles: inputs.inputFiles, - emptyMessage: "No frontend component surface was detected under src, so no UI implementation tasks were generated." - }; - } - - if (inputs.inputFiles.length === 0) { - return { - evaluation: { - title: "UX Improvement Tasks", - summary: "UXImprovementAgent skipped task generation because no UX source reports were available for this repository output.", - findings: [ - "No UX source reports were found in reports/ux_report.md, reports/usability_findings.md, or reports/workflow_analysis.md." - ], - recommendations: [], - riskLevel: "low" - }, - findings: [], - recommendations: [], - inputFiles: inputs.inputFiles, - emptyMessage: "No UX source reports were found, so no implementation tasks could be generated. Run UXAgent first." - }; - } - - const aiResponse = await this.requestStructuredAI(context, { - task: "ux-improvement", - systemPromptFile: "ux-improvement.system.md", - analysisPrompt: [ - "Convert operational UX findings into implementation-ready frontend improvements for ERP users.", - "Primary users: non-technical government administrative staff performing repetitive form-based work.", - "Critical rule: prioritize functional usability and workflow clarity over visual design.", - "Ignore README files, onboarding guides, installation instructions, and developer documentation.", - "Do not propose backend, OpenAPI, Prisma, or server-side changes.", - `Source reports: ${inputs.inputFiles.join(", ")}.`, - formatFrontendUsabilityAnalysis(analysis), - formatComponentCatalog(inputs), - `Findings: ${summarizeItems(inputs.findings, "None")}.`, - `Recommendations: ${summarizeItems(inputs.recommendations, "None")}.` - ].join("\n") - }); - - const mergedFindings = filterOperationalUXItems( - combineRecommendations(inputs.findings, analysis.findings, aiResponse?.issues.map(normalizeAIInsight) ?? []) - ); - const mergedRecommendations = filterOperationalUXItems( - combineRecommendations( - inputs.recommendations, - analysis.recommendations, - aiResponse?.proposed_improvements.map(normalizeAIImprovement) ?? [] - ) - ); - - const summary = - mergedFindings.length > 0 - ? `UXImprovementAgent translated ${inputs.inputFiles.length} UX source reports into component-level frontend tasks.` - : "UXImprovementAgent reviewed the available UX source reports but did not find actionable frontend implementation tasks."; - - return { - evaluation: { - title: "UX Improvement Tasks", - summary, - findings: mergedFindings, - recommendations: mergedRecommendations, - riskLevel: mergeRiskLevel( - mergedFindings.some((finding) => /workflow|navigation|form|table|dashboard|search|error clarity/i.test(finding)) - ? "medium" - : "low", - (aiResponse?.issues ?? []).filter((issue) => filterOperationalUXItems([normalizeAIInsight(issue)]).length > 0) - ) - }, - findings: mergedFindings, - recommendations: mergedRecommendations, - inputFiles: inputs.inputFiles, - emptyMessage: - mergedFindings.length === 0 && mergedRecommendations.length === 0 - ? "No actionable UX issues were derived from the current UX source reports." - : undefined - }; - } - - async run(context: ProjectContext): Promise { - this.logger.info("Agent analysis started", { - action: "analysis_start", - repoName: context.repoName, - outputPath: context.outputPath - }); - - const plan = await this.buildPlan(context); - const artifacts = await generateUXImprovementArtifacts(context, { - findings: plan.findings, - recommendations: plan.recommendations, - inputFiles: plan.inputFiles, - emptyMessage: plan.emptyMessage - }); - - this.logger.info("Agent analysis completed", { - action: "analysis_complete", - repoName: context.repoName, - findings: plan.evaluation.findings.length, - recommendations: plan.evaluation.recommendations.length, - reportPath: artifacts.implementationTasksPath, - navigationPath: artifacts.navigationRestructurePath, - formPath: artifacts.formSimplificationTasksPath, - workspacePath: artifacts.workspaceImprovementsPath - }); - - return { - agentId: this.agentId, - title: plan.evaluation.title, - summary: plan.evaluation.summary, - findings: plan.evaluation.findings, - recommendations: plan.evaluation.recommendations, - riskLevel: plan.evaluation.riskLevel, - outputPath: artifacts.implementationTasksPath - }; - } - - protected async evaluate(context: ProjectContext): Promise { - const plan = await this.buildPlan(context); - return plan.evaluation; - } -} diff --git a/analysis/api_scanner/index.ts b/analysis/api_scanner/index.ts deleted file mode 100644 index 57acbe7..0000000 --- a/analysis/api_scanner/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { findOpenApiFiles } from "../../tools/openapi_tools"; -import { uniqueSorted } from "../../shared/fs-utils"; -import type { ApiScanResult, DependencyManifest } from "../../shared/types"; - -function flattenDependencies(manifests: DependencyManifest[]): string[] { - return manifests.flatMap((manifest) => manifest.dependencies.map((dependency) => dependency.toLowerCase())); -} - -export function scanApis( - files: string[], - dependencies: DependencyManifest[], - frameworks: string[] -): ApiScanResult { - const apis = new Set(); - const apiFiles = new Set(); - const flatDependencies = flattenDependencies(dependencies); - const openApiFiles = findOpenApiFiles(files); - const graphQlFiles = files.filter((file) => /\.(graphql|gql)$/i.test(file) || /graphql/i.test(file)); - - for (const file of openApiFiles) { - apiFiles.add(file); - apis.add("OpenAPI"); - if (/swagger/i.test(file)) { - apis.add("Swagger"); - } - } - - for (const file of graphQlFiles) { - apiFiles.add(file); - apis.add("GraphQL"); - } - - if (flatDependencies.some((dependency) => dependency.includes("swagger"))) { - apis.add("Swagger"); - } - - if (flatDependencies.some((dependency) => dependency.includes("openapi"))) { - apis.add("OpenAPI"); - } - - if (flatDependencies.some((dependency) => dependency.includes("graphql") || dependency.includes("apollo"))) { - apis.add("GraphQL"); - } - - if ( - frameworks.some((framework) => - ["NestJS", "Express", "Django", "Flask", "FastAPI", "Spring", "Rails"].includes(framework) - ) || - files.some((file) => /(^|\/)(routes|controllers|api)\//i.test(file)) - ) { - apis.add("REST"); - } - - return { - apis: uniqueSorted([...apis]), - apiFiles: uniqueSorted([...apiFiles]) - }; -} diff --git a/analysis/code_graph_v2/index.ts b/analysis/code_graph_v2/index.ts deleted file mode 100644 index fbbb521..0000000 --- a/analysis/code_graph_v2/index.ts +++ /dev/null @@ -1,611 +0,0 @@ -import { createHash } from "node:crypto"; -import { promises as fs } from "node:fs"; -import path from "node:path"; - -import ts from "typescript"; - -import { readJsonSafe, toPosixPath, uniqueSorted, walkDirectory, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - CodeGraphBuildResult, - CodeGraphDocument, - CodeGraphEdge, - CodeGraphEdgeKind, - CodeGraphFileRecord, - CodeGraphSymbol, - ProjectContext -} from "../../shared/types"; - -const CODE_GRAPH_V2_FILE = "code_graph_v2.json"; -const GRAPH_SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]; - -function isGraphSource(filePath: string): boolean { - return GRAPH_SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension)); -} - -export function supportsCodeGraphV2(filePath: string): boolean { - return isGraphSource(filePath); -} - -function isTestFile(filePath: string): boolean { - return /(^|\/)(__tests__|tests?|spec)(\/|\.|$)/i.test(filePath) || /\.(test|spec)\.[^.]+$/i.test(filePath); -} - -function fileLanguage(filePath: string): string { - if (filePath.endsWith(".tsx")) { - return "tsx"; - } - if (filePath.endsWith(".ts")) { - return "typescript"; - } - if (filePath.endsWith(".jsx")) { - return "jsx"; - } - if (filePath.endsWith(".mjs")) { - return "mjs"; - } - if (filePath.endsWith(".cjs")) { - return "cjs"; - } - return "javascript"; -} - -function sortSymbols(symbols: CodeGraphSymbol[]): CodeGraphSymbol[] { - return [...symbols].sort( - (left, right) => - left.filePath.localeCompare(right.filePath) || - left.lineStart - right.lineStart || - left.id.localeCompare(right.id) - ); -} - -function edgeKey(edge: CodeGraphEdge): string { - return `${edge.kind}:::${edge.from}:::${edge.to}:::${edge.filePath}:::${edge.line}`; -} - -function sortEdges(edges: CodeGraphEdge[]): CodeGraphEdge[] { - return [...edges].sort( - (left, right) => - left.filePath.localeCompare(right.filePath) || - left.kind.localeCompare(right.kind) || - left.from.localeCompare(right.from) || - left.to.localeCompare(right.to) || - left.line - right.line - ); -} - -function defaultCompilerOptions(): ts.CompilerOptions { - return { - allowJs: true, - checkJs: false, - jsx: ts.JsxEmit.Preserve, - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - skipLibCheck: true, - noEmit: true - }; -} - -function loadCompilerOptions(targetPath: string): ts.CompilerOptions { - const defaults = defaultCompilerOptions(); - const configPath = ts.findConfigFile(targetPath, ts.sys.fileExists, "tsconfig.json"); - - if (!configPath) { - return defaults; - } - - try { - const configFile = ts.readConfigFile(configPath, ts.sys.readFile); - if (configFile.error) { - return defaults; - } - - const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath)); - return { - ...defaults, - ...parsed.options, - allowJs: true, - checkJs: false, - skipLibCheck: true, - noEmit: true - }; - } catch { - return defaults; - } -} - -function lineNumber(sourceFile: ts.SourceFile, node: ts.Node): number { - return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; -} - -function lineEndNumber(sourceFile: ts.SourceFile, node: ts.Node): number { - return sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1; -} - -function hasExportModifier(node: ts.Node): boolean { - return Boolean(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export); -} - -function maybeDefaultName(node: ts.Node): string | undefined { - return Boolean(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Default) ? "default" : undefined; -} - -function symbolId(filePath: string, name: string, parentSymbolId?: string): string { - return parentSymbolId ? `${parentSymbolId}.${name}` : `${filePath}#${name}`; -} - -function isFunctionLikeInitializer(node: ts.Expression | undefined): node is ts.ArrowFunction | ts.FunctionExpression { - return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node))); -} - -function propertyNameText(name: ts.PropertyName | ts.BindingName | undefined): string | undefined { - if (!name) { - return undefined; - } - if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { - return name.text; - } - return undefined; -} - -async function fileHash(filePath: string): Promise { - const content = await fs.readFile(filePath); - return createHash("sha1").update(content).digest("hex"); -} - -function graphPathFor(context: ProjectContext): string { - return path.join(context.runtimeMemoryDir, "code_graph", CODE_GRAPH_V2_FILE); -} - -function emptyGraph(targetPath: string): CodeGraphDocument { - return { - version: 2, - generatedAt: new Date().toISOString(), - targetPath, - nodes: [], - edges: [], - files: [], - symbols: [], - build: { - mode: "full", - updatedFiles: [], - removedFiles: [], - unchangedFiles: 0 - }, - stats: { - files: 0, - symbols: 0, - nodes: 0, - edges: 0, - edgeKinds: {} - } - }; -} - -function resolveLocalImport( - targetPath: string, - fromFile: string, - specifier: string, - compilerOptions: ts.CompilerOptions, - knownFiles: Set -): string | undefined { - const fromAbsolute = path.join(targetPath, fromFile); - const resolution = ts.resolveModuleName(specifier, fromAbsolute, compilerOptions, ts.sys).resolvedModule; - - if (!resolution?.resolvedFileName) { - return undefined; - } - - const rawRelative = toPosixPath(path.relative(targetPath, resolution.resolvedFileName)); - const directMatch = rawRelative.replace(/^\.\/+/, ""); - if (knownFiles.has(directMatch)) { - return directMatch; - } - - if (directMatch.endsWith(".d.ts")) { - const tsCandidate = directMatch.replace(/\.d\.ts$/, ".ts"); - const tsxCandidate = directMatch.replace(/\.d\.ts$/, ".tsx"); - const jsCandidate = directMatch.replace(/\.d\.ts$/, ".js"); - const jsxCandidate = directMatch.replace(/\.d\.ts$/, ".jsx"); - - for (const candidate of [tsCandidate, tsxCandidate, jsCandidate, jsxCandidate]) { - if (knownFiles.has(candidate)) { - return candidate; - } - } - } - - return undefined; -} - -function aggregateDocument( - targetPath: string, - fileRecords: CodeGraphFileRecord[], - mode: "full" | "incremental", - updatedFiles: string[], - removedFiles: string[], - unchangedFiles: number -): CodeGraphDocument { - const files = [...fileRecords].sort((left, right) => left.filePath.localeCompare(right.filePath)); - const symbolMap = new Map(); - const edgeMap = new Map(); - - for (const file of files) { - for (const symbol of file.symbols) { - symbolMap.set(symbol.id, symbol); - } - for (const edge of file.edges) { - edgeMap.set(edgeKey(edge), edge); - } - } - - const symbols = sortSymbols([...symbolMap.values()]); - const edges = sortEdges([...edgeMap.values()]); - const nodes = uniqueSorted([...files.map((file) => file.filePath), ...symbols.map((symbol) => symbol.id)]); - const edgeKinds: Partial> = {}; - - for (const edge of edges) { - edgeKinds[edge.kind] = (edgeKinds[edge.kind] ?? 0) + 1; - } - - return { - version: 2, - generatedAt: new Date().toISOString(), - targetPath, - nodes, - edges, - files, - symbols, - build: { - mode, - updatedFiles, - removedFiles, - unchangedFiles - }, - stats: { - files: files.length, - symbols: symbols.length, - nodes: nodes.length, - edges: edges.length, - edgeKinds - } - }; -} - -function parseGraphFile( - program: ts.Program, - compilerOptions: ts.CompilerOptions, - targetPath: string, - filePath: string, - hash: string, - knownFiles: Set -): CodeGraphFileRecord { - const absolutePath = path.join(targetPath, filePath); - const sourceFile = program.getSourceFile(absolutePath); - - if (!sourceFile) { - return { - filePath, - hash, - language: fileLanguage(filePath), - isTest: isTestFile(filePath), - imports: [], - symbols: [], - edges: [] - }; - } - - const imports = new Set(); - const symbols: CodeGraphSymbol[] = []; - const edges: CodeGraphEdge[] = []; - const declarationIds = new WeakMap(); - const topLevelSymbols = new Map(); - const importedBindings = new Map(); - const testFile = isTestFile(filePath); - - const addEdge = (kind: CodeGraphEdgeKind, from: string, to: string, node: ts.Node): void => { - edges.push({ - kind, - from, - to, - filePath, - line: lineNumber(sourceFile, node) - }); - }; - - const addSymbol = ( - name: string, - kind: CodeGraphSymbol["kind"], - node: ts.Node, - exported: boolean, - parentSymbolId?: string - ): string => { - const id = symbolId(filePath, name, parentSymbolId); - symbols.push({ - id, - name, - qualifiedName: id, - kind, - filePath, - exported, - lineStart: lineNumber(sourceFile, node), - lineEnd: lineEndNumber(sourceFile, node), - parentSymbolId - }); - addEdge("contains", parentSymbolId ?? filePath, id, node); - declarationIds.set(node, id); - - if (!parentSymbolId) { - topLevelSymbols.set(name, id); - } - - return id; - }; - - const recordImport = (node: ts.ImportDeclaration): void => { - if (!ts.isStringLiteral(node.moduleSpecifier)) { - return; - } - - const resolvedFile = resolveLocalImport(targetPath, filePath, node.moduleSpecifier.text, compilerOptions, knownFiles); - if (!resolvedFile) { - return; - } - - imports.add(resolvedFile); - addEdge("imports", filePath, resolvedFile, node.moduleSpecifier); - - const importClause = node.importClause; - if (!importClause) { - return; - } - - if (importClause.name) { - importedBindings.set(importClause.name.text, { - targetFile: resolvedFile, - importedName: "default" - }); - } - - if (!importClause.namedBindings) { - return; - } - - if (ts.isNamespaceImport(importClause.namedBindings)) { - importedBindings.set(importClause.namedBindings.name.text, { - targetFile: resolvedFile, - importedName: "*" - }); - return; - } - - for (const element of importClause.namedBindings.elements) { - importedBindings.set(element.name.text, { - targetFile: resolvedFile, - importedName: element.propertyName?.text ?? element.name.text - }); - } - }; - - const visitTopLevelStatement = (node: ts.Statement): void => { - if (ts.isImportDeclaration(node)) { - recordImport(node); - return; - } - - if (ts.isFunctionDeclaration(node)) { - if (!node.body) { - return; - } - - const name = node.name?.text ?? maybeDefaultName(node); - if (!name) { - return; - } - - addSymbol(name, testFile && /^test|smoke/i.test(name) ? "test" : "function", node, hasExportModifier(node)); - return; - } - - if (ts.isClassDeclaration(node)) { - const name = node.name?.text ?? maybeDefaultName(node); - if (!name) { - return; - } - - const classId = addSymbol(name, "class", node, hasExportModifier(node)); - - for (const member of node.members) { - if (!ts.isMethodDeclaration(member) && !ts.isConstructorDeclaration(member)) { - continue; - } - if ("body" in member && !member.body) { - continue; - } - - const memberName = ts.isConstructorDeclaration(member) ? "constructor" : propertyNameText(member.name); - if (!memberName) { - continue; - } - - addSymbol(memberName, "method", member, false, classId); - } - return; - } - - if (ts.isVariableStatement(node)) { - const exported = hasExportModifier(node); - for (const declaration of node.declarationList.declarations) { - const name = propertyNameText(declaration.name); - if (!name) { - continue; - } - - const kind = testFile && /^test|smoke/i.test(name) - ? "test" - : isFunctionLikeInitializer(declaration.initializer) - ? "function" - : "variable"; - const id = addSymbol(name, kind, declaration, exported); - - if (isFunctionLikeInitializer(declaration.initializer)) { - declarationIds.set(declaration.initializer, id); - } - } - return; - } - - if (ts.isInterfaceDeclaration(node)) { - const name = node.name?.text; - if (name) { - addSymbol(name, "interface", node, hasExportModifier(node)); - } - return; - } - - if (ts.isTypeAliasDeclaration(node)) { - addSymbol(node.name.text, "type", node, hasExportModifier(node)); - return; - } - - if (ts.isEnumDeclaration(node)) { - addSymbol(node.name.text, "enum", node, hasExportModifier(node)); - } - }; - - for (const statement of sourceFile.statements) { - visitTopLevelStatement(statement); - } - - const resolveCallTarget = (expression: ts.LeftHandSideExpression): string | undefined => { - if (ts.isIdentifier(expression)) { - const local = topLevelSymbols.get(expression.text); - if (local) { - return local; - } - - const imported = importedBindings.get(expression.text); - if (!imported) { - return undefined; - } - - if (imported.importedName === "default") { - return `${imported.targetFile}#default`; - } - if (imported.importedName === "*") { - return imported.targetFile; - } - return `${imported.targetFile}#${imported.importedName}`; - } - - if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) { - const imported = importedBindings.get(expression.expression.text); - if (!imported) { - return undefined; - } - return `${imported.targetFile}#${expression.name.text}`; - } - - return undefined; - }; - - const walkCalls = (node: ts.Node, currentSymbolId?: string): void => { - const nextSymbolId = declarationIds.get(node) ?? currentSymbolId; - - if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && nextSymbolId) { - const target = resolveCallTarget(node.expression); - if (target) { - addEdge("calls", nextSymbolId, target, node); - } - } - - ts.forEachChild(node, (child) => walkCalls(child, nextSymbolId)); - }; - - walkCalls(sourceFile); - - return { - filePath, - hash, - language: fileLanguage(filePath), - isTest: testFile, - imports: uniqueSorted([...imports]), - symbols: sortSymbols(symbols), - edges: sortEdges(edges) - }; -} - -export async function buildOrUpdateCodeGraphV2(context: ProjectContext): Promise { - const graphPath = graphPathFor(context); - const previous = await readJsonSafe(graphPath); - const relativeOutput = toPosixPath(path.relative(context.targetPath, context.outputPath)); - const excludePaths = - !relativeOutput || relativeOutput === "." || relativeOutput.startsWith("../") - ? [] - : [relativeOutput]; - const discoveredFiles = (await walkDirectory(context.targetPath, 12000, excludePaths)).filter(isGraphSource); - const currentFiles = uniqueSorted(discoveredFiles.map((filePath) => filePath.replace(/^\.\/+/, ""))); - - if (currentFiles.length === 0) { - const graph = emptyGraph(context.targetPath); - await writeJsonEnsured(graphPath, graph); - return { graphPath, graph }; - } - - const currentHashes = new Map( - await Promise.all( - currentFiles.map(async (filePath) => [filePath, await fileHash(path.join(context.targetPath, filePath))] as const) - ) - ); - const previousFiles = new Map((previous?.files ?? []).map((record) => [record.filePath, record])); - const removedFiles = previous - ? uniqueSorted([...previousFiles.keys()].filter((filePath) => !currentHashes.has(filePath))) - : []; - const changedFiles = previous - ? uniqueSorted( - currentFiles.filter((filePath) => previousFiles.get(filePath)?.hash !== currentHashes.get(filePath)) - ) - : currentFiles; - const filesToParse = previous && removedFiles.length === 0 ? changedFiles : currentFiles; - const mode: "full" | "incremental" = previous ? "incremental" : "full"; - const nextFiles = new Map(previousFiles); - - for (const removedFile of removedFiles) { - nextFiles.delete(removedFile); - } - - if (filesToParse.length > 0) { - const compilerOptions = loadCompilerOptions(context.targetPath); - const program = ts.createProgram({ - rootNames: currentFiles.map((filePath) => path.join(context.targetPath, filePath)), - options: compilerOptions - }); - const knownFiles = new Set(currentFiles); - - for (const filePath of filesToParse) { - const hash = currentHashes.get(filePath); - if (!hash) { - continue; - } - nextFiles.set( - filePath, - parseGraphFile(program, compilerOptions, context.targetPath, filePath, hash, knownFiles) - ); - } - } - - const graph = aggregateDocument( - context.targetPath, - [...nextFiles.values()], - mode, - filesToParse, - removedFiles, - Math.max(currentFiles.length - filesToParse.length, 0) - ); - await writeJsonEnsured(graphPath, graph); - - return { - graphPath, - graph - }; -} diff --git a/analysis/dependency_scanner/index.ts b/analysis/dependency_scanner/index.ts deleted file mode 100644 index 9d299ed..0000000 --- a/analysis/dependency_scanner/index.ts +++ /dev/null @@ -1,198 +0,0 @@ -import path from "node:path"; - -import { readJsonSafe, readTextSafe, uniqueSorted } from "../../shared/fs-utils"; -import type { DependencyManifest, DependencyScanResult } from "../../shared/types"; - -const FRAMEWORK_MAP = new Map([ - ["@nestjs/core", "NestJS"], - ["express", "Express"], - ["next", "NextJS"], - ["react", "React"], - ["django", "Django"], - ["flask", "Flask"], - ["fastapi", "FastAPI"], - ["spring-boot-starter", "Spring"], - ["springframework", "Spring"], - ["rails", "Rails"] -]); - -const TESTING_MAP = new Map([ - ["jest", "Jest"], - ["pytest", "Pytest"], - ["vitest", "Vitest"], - ["mocha", "Mocha"], - ["cypress", "Cypress"] -]); - -interface PackageJsonShape { - dependencies?: Record; - devDependencies?: Record; - peerDependencies?: Record; - optionalDependencies?: Record; -} - -function detectSignals( - dependencies: string[], - frameworks: Set, - testing: Set -): void { - for (const dependency of dependencies) { - const lower = dependency.toLowerCase(); - - for (const [needle, framework] of FRAMEWORK_MAP.entries()) { - if (lower.includes(needle)) { - frameworks.add(framework); - } - } - - for (const [needle, testFramework] of TESTING_MAP.entries()) { - if (lower.includes(needle)) { - testing.add(testFramework); - } - } - } -} - -function parseRequirements(content: string): string[] { - return uniqueSorted( - content - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#") && !line.startsWith("-r")) - .map((line) => line.match(/^([A-Za-z0-9_.-]+)/)?.[1] ?? "") - .filter(Boolean) - ); -} - -function parseGoMod(content: string): string[] { - return uniqueSorted( - [...content.matchAll(/^\s*([A-Za-z0-9_.\-\/]+)\s+v[\w.+-]+/gm)] - .map((match) => match[1]) - .filter(Boolean) as string[] - ); -} - -function parsePom(content: string): string[] { - return uniqueSorted( - [...content.matchAll(/([^<]+)<\/artifactId>/g)] - .map((match) => match[1]) - .filter((dependency) => dependency !== "project") - ); -} - -function parseCargo(content: string): string[] { - const dependencies: string[] = []; - let inDependencyBlock = false; - - for (const line of content.split(/\r?\n/)) { - const trimmed = line.trim(); - - if (trimmed.startsWith("[") && trimmed.endsWith("]")) { - inDependencyBlock = trimmed === "[dependencies]" || trimmed === "[dev-dependencies]"; - continue; - } - - if (!inDependencyBlock || !trimmed || trimmed.startsWith("#")) { - continue; - } - - const match = trimmed.match(/^([A-Za-z0-9_-]+)\s*=/); - if (match?.[1]) { - dependencies.push(match[1]); - } - } - - return uniqueSorted(dependencies); -} - -function parseGemfile(content: string): string[] { - return uniqueSorted( - [...content.matchAll(/^\s*gem\s+["']([^"']+)["']/gm)].map((match) => match[1]).filter(Boolean) as string[] - ); -} - -async function parseManifest(rootPath: string, manifest: string): Promise { - const absolutePath = path.join(rootPath, manifest); - const base = path.posix.basename(manifest); - - if (base === "package.json") { - const parsed = (await readJsonSafe(absolutePath)) ?? {}; - const dependencies = uniqueSorted([ - ...Object.keys(parsed.dependencies ?? {}), - ...Object.keys(parsed.devDependencies ?? {}), - ...Object.keys(parsed.peerDependencies ?? {}), - ...Object.keys(parsed.optionalDependencies ?? {}) - ]); - return { path: manifest, ecosystem: "node", dependencies }; - } - - const content = await readTextSafe(absolutePath); - - if (base === "requirements.txt") { - return { path: manifest, ecosystem: "python", dependencies: parseRequirements(content) }; - } - - if (base === "go.mod") { - return { path: manifest, ecosystem: "go", dependencies: parseGoMod(content) }; - } - - if (base === "pom.xml") { - return { path: manifest, ecosystem: "java", dependencies: parsePom(content) }; - } - - if (base === "Cargo.toml") { - return { path: manifest, ecosystem: "rust", dependencies: parseCargo(content) }; - } - - if (base === "Gemfile") { - return { path: manifest, ecosystem: "ruby", dependencies: parseGemfile(content) }; - } - - if (base === "composer.json") { - const parsed = (await readJsonSafe<{ require?: Record }>(absolutePath)) ?? {}; - return { path: manifest, ecosystem: "php", dependencies: uniqueSorted(Object.keys(parsed.require ?? {})) }; - } - - if (base.endsWith(".csproj")) { - return { - path: manifest, - ecosystem: "dotnet", - dependencies: uniqueSorted( - [...content.matchAll(/PackageReference\s+Include="([^"]+)"/g)].map((match) => match[1]).filter(Boolean) as string[] - ) - }; - } - - return { path: manifest, ecosystem: "unknown", dependencies: [] }; -} - -export async function scanDependencies(rootPath: string, files: string[]): Promise { - const manifests = files.filter((file) => { - const base = path.posix.basename(file); - return ( - [ - "package.json", - "requirements.txt", - "go.mod", - "pom.xml", - "Cargo.toml", - "Gemfile", - "composer.json" - ].includes(base) || base.endsWith(".csproj") - ); - }); - const dependencies = await Promise.all(manifests.map((manifest) => parseManifest(rootPath, manifest))); - const frameworks = new Set(); - const testing = new Set(); - - for (const manifest of dependencies) { - detectSignals(manifest.dependencies, frameworks, testing); - } - - return { - manifests, - dependencies, - frameworks: uniqueSorted([...frameworks]), - testing: uniqueSorted([...testing]) - }; -} diff --git a/analysis/impact_radius/index.ts b/analysis/impact_radius/index.ts deleted file mode 100644 index 2407f1c..0000000 --- a/analysis/impact_radius/index.ts +++ /dev/null @@ -1,490 +0,0 @@ -import path from "node:path"; - -import { buildOrUpdateCodeGraphV2, supportsCodeGraphV2 } from "../code_graph_v2"; -import { readTextSafe, toPosixPath, uniqueSorted, writeFileEnsured, writeJsonEnsured, walkDirectory } from "../../shared/fs-utils"; -import type { CodeGraphDocument, ImpactAnalysisResult, ProjectContext } from "../../shared/types"; -import { listChangedFiles } from "../../tools/git_tools"; - -interface LegacyImportGraphDocument { - generatedAt: string; - targetPath: string; - nodes: string[]; - edges: Array<{ - from: string; - to: string; - }>; - unresolvedImports: Array<{ - file: string; - specifier: string; - }>; -} - -const LEGACY_SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"]; - -function isLegacySource(filePath: string): boolean { - return LEGACY_SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension)); -} - -function isTestFile(filePath: string): boolean { - return /(^|\/)(__tests__|tests?|spec)(\/|\.|$)/i.test(filePath) || /\.(test|spec)\.[^.]+$/i.test(filePath); -} - -function normalizeFiles(files: string[]): string[] { - return uniqueSorted(files.map((filePath) => toPosixPath(filePath).replace(/^\.\/+/, ""))); -} - -function parseJavaScriptImports(content: string): string[] { - const matches = [ - ...content.matchAll(/\bimport\s+[^"']*?from\s+["']([^"']+)["']/g), - ...content.matchAll(/\bexport\s+[^"']*?from\s+["']([^"']+)["']/g), - ...content.matchAll(/\brequire\(\s*["']([^"']+)["']\s*\)/g), - ...content.matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g) - ]; - - return uniqueSorted( - matches - .map((match) => match[1]?.trim()) - .filter(Boolean) as string[] - ); -} - -function parsePythonImports(content: string): string[] { - const matches = [ - ...content.matchAll(/^\s*from\s+([A-Za-z0-9_\.]+)\s+import\s+/gm), - ...content.matchAll(/^\s*from\s+(\.+[A-Za-z0-9_\.]*)\s+import\s+/gm), - ...content.matchAll(/^\s*import\s+([A-Za-z0-9_\.]+)/gm) - ]; - - return uniqueSorted( - matches - .map((match) => match[1]?.trim()) - .filter(Boolean) as string[] - ); -} - -function resolveRelativeImport( - fromFile: string, - specifier: string, - knownFiles: Set -): string | undefined { - if (!specifier.startsWith(".")) { - return undefined; - } - - const baseDir = path.posix.dirname(fromFile); - const resolvedBase = path.posix.normalize(path.posix.join(baseDir, specifier)); - const candidates = new Set([resolvedBase]); - - for (const extension of LEGACY_SOURCE_EXTENSIONS) { - candidates.add(`${resolvedBase}${extension}`); - candidates.add(path.posix.join(resolvedBase, `index${extension}`)); - } - - for (const candidate of candidates) { - const normalized = toPosixPath(candidate); - if (knownFiles.has(normalized)) { - return normalized; - } - } - - return undefined; -} - -function resolvePythonImport(specifier: string, fromFile: string, knownFiles: Set): string | undefined { - if (specifier.startsWith(".")) { - const dots = specifier.match(/^\.+/)?.[0].length ?? 0; - const modulePath = specifier.slice(dots).replace(/\./g, "/"); - let baseDir = path.posix.dirname(fromFile); - - for (let index = 1; index < dots; index += 1) { - baseDir = path.posix.dirname(baseDir); - } - - const resolved = path.posix.join(baseDir, modulePath); - const candidates = [`${resolved}.py`, path.posix.join(resolved, "__init__.py")]; - - return candidates.find((candidate) => knownFiles.has(candidate)); - } - - const absolutePath = specifier.replace(/\./g, "/"); - const candidates = [`${absolutePath}.py`, path.posix.join(absolutePath, "__init__.py")]; - return candidates.find((candidate) => knownFiles.has(candidate)); -} - -function resolveImport( - filePath: string, - specifier: string, - knownFiles: Set -): string | undefined { - if (filePath.endsWith(".py")) { - return resolvePythonImport(specifier, filePath, knownFiles); - } - - return resolveRelativeImport(filePath, specifier, knownFiles); -} - -async function buildLegacyImportGraph( - targetPath: string, - excludePaths: string[] -): Promise<{ - importsByFile: Map; - reverseDependencies: Map; - unresolvedImports: Array<{ file: string; specifier: string }>; -}> { - const files = (await walkDirectory(targetPath, 12000, excludePaths)).filter(isLegacySource); - const normalizedFiles = normalizeFiles(files); - const knownFiles = new Set(normalizedFiles); - const importsByFile = new Map(); - const reverseDependencies = new Map>(); - const unresolvedImports: Array<{ file: string; specifier: string }> = []; - - for (const filePath of normalizedFiles) { - const content = await readTextSafe(path.join(targetPath, filePath)); - const rawImports = filePath.endsWith(".py") ? parsePythonImports(content) : parseJavaScriptImports(content); - const resolvedImports = uniqueSorted( - rawImports - .map((specifier) => { - const resolved = resolveImport(filePath, specifier, knownFiles); - if (!resolved && (specifier.startsWith(".") || filePath.endsWith(".py"))) { - unresolvedImports.push({ file: filePath, specifier }); - } - return resolved; - }) - .filter(Boolean) as string[] - ); - - importsByFile.set(filePath, resolvedImports); - - for (const importedFile of resolvedImports) { - const current = reverseDependencies.get(importedFile) ?? new Set(); - current.add(filePath); - reverseDependencies.set(importedFile, current); - } - } - - return { - importsByFile, - reverseDependencies: new Map( - [...reverseDependencies.entries()].map(([filePath, dependents]) => [filePath, uniqueSorted([...dependents])]) - ), - unresolvedImports - }; -} - -function relatedTestFilesFromImports( - importsByFile: Map, - affectedFiles: Set -): string[] { - return uniqueSorted( - [...importsByFile.entries()] - .filter(([filePath, imports]) => isTestFile(filePath) && imports.some((importedFile) => affectedFiles.has(importedFile))) - .map(([filePath]) => filePath) - ); -} - -function reviewSet( - changedFiles: string[], - directDependents: string[], - transitiveDependents: string[], - impactedTests: string[] -): string[] { - return uniqueSorted([...changedFiles, ...directDependents, ...transitiveDependents, ...impactedTests]); -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function buildReverseDependenciesFromCodeGraph(graph: CodeGraphDocument): Map { - const reverse = new Map>(); - - for (const file of graph.files) { - for (const importedFile of file.imports) { - const current = reverse.get(importedFile) ?? new Set(); - current.add(file.filePath); - reverse.set(importedFile, current); - } - } - - return new Map([...reverse.entries()].map(([filePath, dependents]) => [filePath, uniqueSorted([...dependents])])); -} - -function relatedTestFilesFromCodeGraph(graph: CodeGraphDocument, affectedFiles: Set): string[] { - return uniqueSorted( - graph.files - .filter((file) => file.isTest && file.imports.some((importedFile) => affectedFiles.has(importedFile))) - .map((file) => file.filePath) - ); -} - -function computeDependents( - changedFiles: string[], - reverseDependencies: Map -): { - directDependents: string[]; - transitiveDependents: string[]; -} { - const directDependents = uniqueSorted( - changedFiles - .flatMap((filePath) => reverseDependencies.get(filePath) ?? []) - .filter((filePath) => !isTestFile(filePath)) - ); - const visited = new Set(changedFiles); - const queue = [...directDependents]; - const transitiveDependents: string[] = []; - - for (const filePath of directDependents) { - visited.add(filePath); - } - - while (queue.length > 0) { - const current = queue.shift()!; - const dependents = reverseDependencies.get(current) ?? []; - - for (const dependent of dependents) { - if (visited.has(dependent) || isTestFile(dependent)) { - continue; - } - - visited.add(dependent); - transitiveDependents.push(dependent); - queue.push(dependent); - } - } - - return { - directDependents, - transitiveDependents: uniqueSorted(transitiveDependents) - }; -} - -function renderImpactReport(input: { - context: ProjectContext; - graphMode: "legacy-imports" | "code-graph-v2"; - graphBuildMode: "full" | "incremental"; - graphNodes: number; - graphEdges: number; - graphFiles: number; - graphSymbols: number; - changedFiles: string[]; - analyzableChangedFiles: string[]; - directDependents: string[]; - transitiveDependents: string[]; - impactedTests: string[]; - reviewFiles: string[]; - unresolvedImports: string[]; -}): string { - return `# Impact Radius - -## Scope - -- Repository: ${input.context.repoName} -- Graph engine: ${input.graphMode} -- Graph build mode: ${input.graphBuildMode} -- Changed files: ${input.changedFiles.length} -- Changed files analyzed structurally: ${input.analyzableChangedFiles.length} -- Graph files: ${input.graphFiles} -- Graph symbols: ${input.graphSymbols} -- Graph nodes: ${input.graphNodes} -- Graph edges: ${input.graphEdges} - -## Changed Files - -${renderList(input.changedFiles)} - -## Structurally Analyzed Files - -${renderList(input.analyzableChangedFiles)} - -## Direct Dependents - -${renderList(input.directDependents)} - -## Transitive Dependents - -${renderList(input.transitiveDependents)} - -## Related Tests - -${renderList(input.impactedTests)} - -## Minimal Review Set - -${renderList(input.reviewFiles)} - -## Unresolved Imports - -${renderList(input.unresolvedImports)} -`; -} - -async function analyzeWithLegacyGraph( - context: ProjectContext, - changedFiles: string[] -): Promise { - const relativeOutput = toPosixPath(path.relative(context.targetPath, context.outputPath)); - const excludePaths = - !relativeOutput || relativeOutput === "." || relativeOutput.startsWith("../") - ? [] - : [relativeOutput]; - const graph = await buildLegacyImportGraph(context.targetPath, excludePaths); - const analyzableChangedFiles = changedFiles.filter((filePath) => graph.importsByFile.has(filePath)); - const dependentInfo = computeDependents(analyzableChangedFiles, graph.reverseDependencies); - const affectedFiles = new Set([ - ...analyzableChangedFiles, - ...dependentInfo.directDependents, - ...dependentInfo.transitiveDependents - ]); - const impactedTests = uniqueSorted([ - ...changedFiles.filter(isTestFile), - ...[...affectedFiles].filter(isTestFile), - ...relatedTestFilesFromImports(graph.importsByFile, affectedFiles) - ]); - const reviewFiles = reviewSet(changedFiles, dependentInfo.directDependents, dependentInfo.transitiveDependents, impactedTests); - - const graphDir = path.join(context.runtimeMemoryDir, "code_graph"); - const reportPath = path.join(context.reportsDir, "impact_radius.md"); - const graphPath = path.join(graphDir, "import_graph.json"); - const graphDocument: LegacyImportGraphDocument = { - generatedAt: new Date().toISOString(), - targetPath: context.targetPath, - nodes: uniqueSorted([...graph.importsByFile.keys()]), - edges: uniqueSorted( - [...graph.importsByFile.entries()].flatMap(([from, imports]) => imports.map((to) => `${from}:::${to}`)) - ).map((edge) => { - const [from, to] = edge.split(":::"); - return { from, to }; - }), - unresolvedImports: graph.unresolvedImports - }; - const unresolvedImports = uniqueSorted(graph.unresolvedImports.map((entry) => `${entry.file} -> ${entry.specifier}`)); - const reportContent = renderImpactReport({ - context, - graphMode: "legacy-imports", - graphBuildMode: "full", - graphNodes: graphDocument.nodes.length, - graphEdges: graphDocument.edges.length, - graphFiles: graphDocument.nodes.length, - graphSymbols: 0, - changedFiles, - analyzableChangedFiles, - directDependents: dependentInfo.directDependents, - transitiveDependents: dependentInfo.transitiveDependents, - impactedTests, - reviewFiles, - unresolvedImports - }); - - await writeJsonEnsured(graphPath, graphDocument); - await writeFileEnsured(reportPath, reportContent); - - return { - targetPath: context.targetPath, - outputPath: context.outputPath, - changedFiles, - directDependents: dependentInfo.directDependents, - transitiveDependents: dependentInfo.transitiveDependents, - impactedTests, - reviewFiles, - unresolvedImports, - graphPath, - reportPath, - graphStats: { - nodes: graphDocument.nodes.length, - edges: graphDocument.edges.length, - files: graphDocument.nodes.length, - symbols: 0, - buildMode: "full", - updatedFiles: analyzableChangedFiles.length - } - }; -} - -async function analyzeWithCodeGraphV2( - context: ProjectContext, - changedFiles: string[] -): Promise { - const { graphPath, graph } = await buildOrUpdateCodeGraphV2(context); - const graphFiles = new Set(graph.files.map((file) => file.filePath)); - const analyzableChangedFiles = changedFiles.filter((filePath) => graphFiles.has(filePath)); - const reverseDependencies = buildReverseDependenciesFromCodeGraph(graph); - const dependentInfo = computeDependents(analyzableChangedFiles, reverseDependencies); - const affectedFiles = new Set([ - ...analyzableChangedFiles, - ...dependentInfo.directDependents, - ...dependentInfo.transitiveDependents - ]); - const impactedTests = uniqueSorted([ - ...changedFiles.filter((filePath) => isTestFile(filePath) || graph.files.find((file) => file.filePath === filePath)?.isTest), - ...relatedTestFilesFromCodeGraph(graph, affectedFiles) - ]); - const reviewFiles = reviewSet(changedFiles, dependentInfo.directDependents, dependentInfo.transitiveDependents, impactedTests); - const reportPath = path.join(context.reportsDir, "impact_radius.md"); - const reportContent = renderImpactReport({ - context, - graphMode: "code-graph-v2", - graphBuildMode: graph.build.mode, - graphNodes: graph.stats.nodes, - graphEdges: graph.stats.edges, - graphFiles: graph.stats.files, - graphSymbols: graph.stats.symbols, - changedFiles, - analyzableChangedFiles, - directDependents: dependentInfo.directDependents, - transitiveDependents: dependentInfo.transitiveDependents, - impactedTests, - reviewFiles, - unresolvedImports: [] - }); - await writeFileEnsured(reportPath, reportContent); - - return { - targetPath: context.targetPath, - outputPath: context.outputPath, - changedFiles, - directDependents: dependentInfo.directDependents, - transitiveDependents: dependentInfo.transitiveDependents, - impactedTests, - reviewFiles, - unresolvedImports: [], - graphPath, - reportPath, - graphStats: { - nodes: graph.stats.nodes, - edges: graph.stats.edges, - files: graph.stats.files, - symbols: graph.stats.symbols, - buildMode: graph.build.mode, - updatedFiles: graph.build.updatedFiles.length - } - }; -} - -export async function analyzeImpactRadius( - context: ProjectContext, - options?: { - files?: string[]; - baseRef?: string; - headRef?: string; - } -): Promise { - const changedFiles = - options?.files && options.files.length > 0 - ? normalizeFiles(options.files) - : normalizeFiles(listChangedFiles(context.targetPath, options?.baseRef, options?.headRef)); - - const shouldUseLegacy = - changedFiles.some((filePath) => !supportsCodeGraphV2(filePath) && isLegacySource(filePath)); - - if (shouldUseLegacy) { - return analyzeWithLegacyGraph(context, changedFiles); - } - - const v2Result = await analyzeWithCodeGraphV2(context, changedFiles); - - if (v2Result.graphStats.files === 0) { - return analyzeWithLegacyGraph(context, changedFiles); - } - - return v2Result; -} diff --git a/analysis/infra_scanner/index.ts b/analysis/infra_scanner/index.ts deleted file mode 100644 index 48ddb68..0000000 --- a/analysis/infra_scanner/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { countDockerStages, detectInfrastructure } from "../../tools/infra_tools"; -import type { InfraScanResult } from "../../shared/types"; - -export async function scanInfrastructure(rootPath: string, files: string[]): Promise { - const detection = detectInfrastructure(files); - - return { - infrastructure: detection.technologies, - infraFiles: detection.files, - dockerStageCount: await countDockerStages(rootPath, detection.files) - }; -} diff --git a/analysis/metrics/metrics_collector.ts b/analysis/metrics/metrics_collector.ts deleted file mode 100644 index d59c31c..0000000 --- a/analysis/metrics/metrics_collector.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { promises as fs } from "node:fs"; -import path from "node:path"; - -import { ensureDir, readJsonSafe, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import { StructuredLogger } from "../../shared/logger"; -import type { AgentReport, GovernanceSummary, ProjectContext } from "../../shared/types"; - -export interface CycleTelemetry { - cycleId: string; - repo: string; - cycleType: string; - duration: number; - cycleDuration: number; - agentsExecuted: number; - proposalsGenerated: number; - risksDetected: number; - timestamp: string; - agentIds: string[]; - riskTypes: string[]; - proposalStatuses: Record; -} - -export interface CycleSpan { - cycleId: string; - cycleType: string; - startedAt: number; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function uniqueRepositoryCount(records: CycleTelemetry[]): number { - return new Set(records.filter((record) => record.repo !== "ecosystem").map((record) => record.repo)).size; -} - -export class MetricsCollector { - private readonly logger = new StructuredLogger("metrics-collector"); - - private telemetryFileName(telemetry: CycleTelemetry): string { - const repoSlug = telemetry.repo - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") - .slice(0, 32); - return `cycle_${telemetry.timestamp.replace(/[:.]/g, "-")}_${repoSlug || "repo"}.json`; - } - - startCycle(cycleType: string, cycleId: string): CycleSpan { - return { - cycleId, - cycleType, - startedAt: Date.now() - }; - } - - completeCycle( - span: CycleSpan, - repo: string, - agentReports: AgentReport[], - summary: GovernanceSummary | undefined - ): CycleTelemetry { - const duration = Date.now() - span.startedAt; - const proposalStatuses = (summary?.proposals ?? []).reduce>((acc, proposal) => { - acc[proposal.status] = (acc[proposal.status] ?? 0) + 1; - return acc; - }, {}); - - return { - cycleId: span.cycleId, - repo, - cycleType: span.cycleType, - duration, - cycleDuration: duration, - agentsExecuted: summary?.executionRecords.filter((record) => record.status === "completed").length ?? agentReports.length, - proposalsGenerated: summary?.proposals.length ?? 0, - risksDetected: agentReports.reduce((count, report) => count + report.findings.length, 0), - timestamp: new Date().toISOString(), - agentIds: (summary?.tasks ?? []).map((task) => task.agentId), - riskTypes: agentReports - .filter((report) => report.findings.length > 0) - .map((report) => report.riskLevel), - proposalStatuses - }; - } - - async persistCycleTelemetry(context: ProjectContext, telemetry: CycleTelemetry): Promise { - return this.persistTelemetry(context.reportsDir, telemetry); - } - - async persistTelemetry(reportsDir: string, telemetry: CycleTelemetry): Promise { - const telemetryDir = path.join(reportsDir, "telemetry"); - const fileName = this.telemetryFileName(telemetry); - const filePath = path.join(telemetryDir, fileName); - - await ensureDir(telemetryDir); - await writeJsonEnsured(filePath, telemetry); - - this.logger.info("Persisted cycle telemetry", { - component: "telemetry", - action: "telemetry_persisted", - cycleId: telemetry.cycleId, - filePath - }); - - return filePath; - } - - async writeRuntimeObservabilityReport(reportsDir: string): Promise { - const telemetryDir = path.join(reportsDir, "telemetry"); - let telemetryFiles: string[] = []; - - try { - telemetryFiles = (await fs.readdir(telemetryDir)) - .filter((fileName) => fileName.startsWith("cycle_") && fileName.endsWith(".json")) - .sort((left, right) => left.localeCompare(right)); - } catch { - telemetryFiles = []; - } - - const telemetryRecords = ( - await Promise.all( - telemetryFiles.map((fileName) => readJsonSafe(path.join(telemetryDir, fileName))) - ) - ).filter(Boolean) as CycleTelemetry[]; - const granularRecords = telemetryRecords.filter((record) => record.repo !== "ecosystem"); - const reportSource = granularRecords.length > 0 ? granularRecords : telemetryRecords; - - const averageCycleDuration = - telemetryRecords.length > 0 - ? Math.round(telemetryRecords.reduce((sum, record) => sum + record.cycleDuration, 0) / telemetryRecords.length) - : 0; - const mostActiveAgents = [...reportSource.reduce>((acc, record) => { - for (const agentId of record.agentIds) { - acc.set(agentId, (acc.get(agentId) ?? 0) + 1); - } - return acc; - }, new Map())] - .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])) - .slice(0, 5) - .map(([agentId, count]) => `${agentId}: ${count}`); - const mostCommonRiskTypes = [...reportSource.reduce>((acc, record) => { - for (const riskType of record.riskTypes) { - acc.set(riskType, (acc.get(riskType) ?? 0) + 1); - } - return acc; - }, new Map())] - .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])) - .slice(0, 5) - .map(([riskType, count]) => `${riskType}: ${count}`); - const proposalFrequency = - reportSource.length > 0 - ? (reportSource.reduce((sum, record) => sum + record.proposalsGenerated, 0) / reportSource.length).toFixed(2) - : "0.00"; - const reportPath = path.join(reportsDir, "runtime_observability.md"); - const content = `# Runtime Observability - -## Summary - -- Average cycle duration: ${averageCycleDuration} ms -- Telemetry files tracked: ${telemetryRecords.length} -- Repositories observed: ${uniqueRepositoryCount(telemetryRecords)} -- Improvement proposal frequency: ${proposalFrequency} proposals per cycle - -## Most Active Agents - -${renderList(mostActiveAgents)} - -## Repository Activity - -${renderList( - [...telemetryRecords.reduce>((acc, record) => { - acc.set(record.repo, (acc.get(record.repo) ?? 0) + 1); - return acc; - }, new Map())] - .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])) - .slice(0, 10) - .map(([repo, count]) => `${repo}: ${count}`) - )} - -## Most Common Risk Types - -${renderList(mostCommonRiskTypes)} - -## Proposal Status Distribution - -${renderList( - [...telemetryRecords.reduce>((acc, record) => { - for (const [status, count] of Object.entries(record.proposalStatuses)) { - acc.set(status, (acc.get(status) ?? 0) + count); - } - return acc; - }, new Map())] - .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])) - .map(([status, count]) => `${status}: ${count}`) - )} -`; - - await writeFileEnsured(reportPath, content); - return reportPath; - } -} diff --git a/analysis/repo_scanner/index.ts b/analysis/repo_scanner/index.ts deleted file mode 100644 index f8b07ec..0000000 --- a/analysis/repo_scanner/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -import path from "node:path"; - -import { readTextSafe, uniqueSorted, walkDirectory } from "../../shared/fs-utils"; -import type { BasicRepoScan } from "../../shared/types"; - -const LANGUAGE_BY_EXTENSION = new Map([ - [".ts", "TypeScript"], - [".tsx", "TypeScript"], - [".py", "Python"], - [".go", "Go"], - [".java", "Java"], - [".rs", "Rust"], - [".cs", "C#"], - [".rb", "Ruby"], - [".php", "PHP"] -]); - -function isManifestFile(file: string): boolean { - const base = path.posix.basename(file); - return ( - [ - "package.json", - "requirements.txt", - "go.mod", - "pom.xml", - "Cargo.toml", - "Gemfile", - "composer.json" - ].includes(base) || base.endsWith(".csproj") - ); -} - -function isSourceFile(file: string): boolean { - return [...LANGUAGE_BY_EXTENSION.keys()].some((extension) => file.endsWith(extension)); -} - -function isTestFile(file: string): boolean { - return /(^|\/)(__tests__|tests?|spec)(\/|\.|$)/i.test(file); -} - -async function readSubmodules(targetPath: string): Promise { - const content = await readTextSafe(path.join(targetPath, ".gitmodules")); - return uniqueSorted( - [...content.matchAll(/path\s*=\s*(.+)/g)].map((match) => match[1]?.trim()).filter(Boolean) as string[] - ); -} - -export async function scanRepositoryStructure(targetPath: string, excludedPaths: string[] = []): Promise { - const files = await walkDirectory(targetPath, 8000, excludedPaths); - const languages = new Set(); - - for (const file of files) { - const extension = path.posix.extname(file); - const language = LANGUAGE_BY_EXTENSION.get(extension); - - if (language) { - languages.add(language); - } - - const base = path.posix.basename(file); - if (base === "package.json") { - languages.add("TypeScript"); - } - if (base === "requirements.txt") { - languages.add("Python"); - } - if (base === "go.mod") { - languages.add("Go"); - } - if (base === "pom.xml") { - languages.add("Java"); - } - if (base === "Cargo.toml") { - languages.add("Rust"); - } - } - - const manifests = files.filter(isManifestFile); - const subrepos = uniqueSorted(manifests.map((manifest) => path.posix.dirname(manifest)).filter((dir) => dir !== ".")); - const submodules = await readSubmodules(targetPath); - - return { - repoName: path.basename(targetPath), - targetPath, - scannedAt: new Date().toISOString(), - files, - languages: uniqueSorted([...languages]), - structure: { - topLevelDirectories: uniqueSorted( - files.map((file) => file.split("/")[0]).filter((entry) => entry && entry !== ".") - ), - sampleFiles: files.slice(0, 50), - subrepos, - submodules, - fileCount: files.length, - sourceFileCount: files.filter(isSourceFile).length, - testFileCount: files.filter(isTestFile).length - } - }; -} diff --git a/analysis/repository_fact_graph/index.ts b/analysis/repository_fact_graph/index.ts deleted file mode 100644 index 1b60fa8..0000000 --- a/analysis/repository_fact_graph/index.ts +++ /dev/null @@ -1,300 +0,0 @@ -import path from "node:path"; - -import { ensureDir, uniqueSorted, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - CodeGraphDocument, - ProjectContext, - RepositoryFactGraphDocument, - RepositoryFactGraphEdge, - RepositoryFactGraphEdgeKind, - RepositoryFactGraphNode, - RepositoryFactGraphNodeKind -} from "../../shared/types"; - -interface RepositoryFactGraphBuildResult { - graphPath: string; - reportPath: string; - graph: RepositoryFactGraphDocument; -} - -function makeNode( - id: string, - label: string, - kind: RepositoryFactGraphNodeKind, - attributes?: RepositoryFactGraphNode["attributes"] -): RepositoryFactGraphNode { - return { id, label, kind, attributes }; -} - -function makeEdge( - kind: RepositoryFactGraphEdgeKind, - from: string, - to: string, - evidencePath?: string, - line?: number -): RepositoryFactGraphEdge { - return { kind, from, to, evidencePath, line }; -} - -function edgeKey(edge: RepositoryFactGraphEdge): string { - return `${edge.kind}:::${edge.from}:::${edge.to}:::${edge.evidencePath ?? ""}:::${edge.line ?? 0}`; -} - -function topLevelDirectoryFor(filePath: string, topLevelDirectories: Set): string | undefined { - const [firstSegment] = filePath.split("/"); - if (!firstSegment || firstSegment === filePath || !topLevelDirectories.has(firstSegment)) { - return undefined; - } - return firstSegment; -} - -function countBy(values: T[]): Partial> { - return values.reduce>>((accumulator, value) => { - accumulator[value] = (accumulator[value] ?? 0) + 1; - return accumulator; - }, {}); -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderCountList(items: Record): string { - const entries = Object.entries(items).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])); - return entries.length > 0 ? entries.map(([label, count]) => `- ${label}: ${count}`).join("\n") : "- None"; -} - -function buildReport( - context: ProjectContext, - graphPath: string, - graph: RepositoryFactGraphDocument, - topConnectedFiles: Array<{ filePath: string; degree: number }>, - exportedSymbols: string[] -): string { - return `# Repository Fact Graph - -## Summary - -- Repository: ${context.repoName} -- Target path: ${context.targetPath} -- Generated at: ${graph.generatedAt} -- Code graph source files: ${graph.stats.codeGraphFiles} -- Code graph symbols: ${graph.stats.codeGraphSymbols} -- Fact graph nodes: ${graph.stats.nodes} -- Fact graph edges: ${graph.stats.edges} -- Fact graph JSON: ${graphPath} - -## Verified repository facts - -- Languages: ${context.discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${context.discovery.frameworks.join(", ") || "None detected"} -- Dependency manifests: ${context.discovery.manifests.join(", ") || "None detected"} -- API files: ${context.discovery.apiFiles.join(", ") || "None detected"} -- Infrastructure files: ${context.discovery.infraFiles.join(", ") || "None detected"} -- Top-level directories: ${context.discovery.structure.topLevelDirectories.join(", ") || "None detected"} - -## Graph inventory - -### Node kinds - -${renderCountList(graph.stats.nodeKinds)} - -### Edge kinds - -${renderCountList(graph.stats.edgeKinds)} - -## Most connected files - -${renderList(topConnectedFiles.map((entry) => `${entry.filePath} (degree=${entry.degree})`))} - -## Exported symbols snapshot - -${renderList(exportedSymbols)} - -## Notes - -- This artifact includes only verified structural relationships derived from repository discovery and the persisted code graph. -- No inferred or ambiguous edges are emitted in this report. -- File-level import, symbol containment, and call relationships depend on the current code_graph_v2 coverage. -`; -} - -export async function buildRepositoryFactGraph( - context: ProjectContext, - codeGraph: CodeGraphDocument -): Promise { - const graphDir = path.join(context.runtimeMemoryDir, "knowledge_graph"); - const graphPath = path.join(graphDir, "repository_fact_graph.json"); - const reportPath = path.join(context.reportsDir, "repository_fact_graph.md"); - - await ensureDir(graphDir); - await ensureDir(path.dirname(reportPath)); - - const nodeMap = new Map(); - const edgeMap = new Map(); - const topLevelDirectories = new Set(context.discovery.structure.topLevelDirectories); - const codeGraphFileIds = new Set(codeGraph.files.map((file) => file.filePath)); - const codeGraphSymbolIds = new Set(codeGraph.symbols.map((symbol) => symbol.id)); - const fileNodeId = (filePath: string) => `file:${filePath}`; - const symbolNodeId = (symbolId: string) => `symbol:${symbolId}`; - - const addNode = (node: RepositoryFactGraphNode): void => { - nodeMap.set(node.id, node); - }; - - const addEdge = (edge: RepositoryFactGraphEdge): void => { - edgeMap.set(edgeKey(edge), edge); - }; - - const repositoryId = `repository:${context.repoName}`; - addNode( - makeNode(repositoryId, context.repoName, "repository", { - targetPath: context.targetPath, - scannedAt: context.scannedAt - }) - ); - - for (const directory of context.discovery.structure.topLevelDirectories) { - const directoryId = `directory:${directory}`; - addNode(makeNode(directoryId, directory, "directory")); - addEdge(makeEdge("contains", repositoryId, directoryId)); - } - - for (const language of context.discovery.languages) { - const languageId = `language:${language}`; - addNode(makeNode(languageId, language, "language")); - addEdge(makeEdge("uses_language", repositoryId, languageId)); - } - - for (const framework of context.discovery.frameworks) { - const frameworkId = `framework:${framework}`; - addNode(makeNode(frameworkId, framework, "framework")); - addEdge(makeEdge("uses_framework", repositoryId, frameworkId)); - } - - for (const manifest of context.discovery.manifests) { - const manifestId = `manifest:${manifest}`; - addNode(makeNode(manifestId, manifest, "manifest", { filePath: manifest })); - addEdge(makeEdge("has_manifest", repositoryId, manifestId, manifest)); - } - - for (const apiFile of context.discovery.apiFiles) { - const apiId = `api:${apiFile}`; - addNode(makeNode(apiId, apiFile, "api_surface", { filePath: apiFile })); - addEdge(makeEdge("exposes_api", repositoryId, apiId, apiFile)); - } - - for (const infraFile of context.discovery.infraFiles) { - const infraId = `infra:${infraFile}`; - addNode(makeNode(infraId, infraFile, "infra_surface", { filePath: infraFile })); - addEdge(makeEdge("defines_infra", repositoryId, infraId, infraFile)); - } - - for (const file of codeGraph.files) { - const currentFileId = fileNodeId(file.filePath); - addNode( - makeNode(currentFileId, file.filePath, "file", { - filePath: file.filePath, - language: file.language, - isTest: file.isTest, - imports: file.imports.length - }) - ); - - const topLevelDirectory = topLevelDirectoryFor(file.filePath, topLevelDirectories); - if (topLevelDirectory) { - addEdge(makeEdge("contains", `directory:${topLevelDirectory}`, currentFileId, file.filePath)); - } else { - addEdge(makeEdge("contains", repositoryId, currentFileId, file.filePath)); - } - - for (const importedFile of uniqueSorted(file.imports)) { - if (!codeGraphFileIds.has(importedFile)) { - continue; - } - addEdge(makeEdge("imports", currentFileId, fileNodeId(importedFile), file.filePath)); - } - } - - for (const symbol of codeGraph.symbols) { - const currentSymbolId = symbolNodeId(symbol.id); - addNode( - makeNode(currentSymbolId, symbol.qualifiedName, "symbol", { - filePath: symbol.filePath, - kind: symbol.kind, - exported: symbol.exported, - lineStart: symbol.lineStart, - lineEnd: symbol.lineEnd - }) - ); - addEdge(makeEdge("declares", fileNodeId(symbol.filePath), currentSymbolId, symbol.filePath, symbol.lineStart)); - } - - for (const edge of codeGraph.edges) { - const from = - codeGraphFileIds.has(edge.from) ? fileNodeId(edge.from) : codeGraphSymbolIds.has(edge.from) ? symbolNodeId(edge.from) : undefined; - const to = codeGraphFileIds.has(edge.to) ? fileNodeId(edge.to) : codeGraphSymbolIds.has(edge.to) ? symbolNodeId(edge.to) : undefined; - - if (!from || !to) { - continue; - } - - addEdge(makeEdge(edge.kind, from, to, edge.filePath, edge.line)); - } - - const nodes = [...nodeMap.values()].sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)); - const edges = [...edgeMap.values()].sort( - (left, right) => - left.kind.localeCompare(right.kind) || - left.from.localeCompare(right.from) || - left.to.localeCompare(right.to) || - (left.evidencePath ?? "").localeCompare(right.evidencePath ?? "") || - (left.line ?? 0) - (right.line ?? 0) - ); - - const degreeByNode = new Map(); - for (const edge of edges) { - degreeByNode.set(edge.from, (degreeByNode.get(edge.from) ?? 0) + 1); - degreeByNode.set(edge.to, (degreeByNode.get(edge.to) ?? 0) + 1); - } - - const topConnectedFiles = nodes - .filter((node) => node.kind === "file") - .map((node) => ({ - filePath: node.label, - degree: degreeByNode.get(node.id) ?? 0 - })) - .sort((left, right) => right.degree - left.degree || left.filePath.localeCompare(right.filePath)) - .slice(0, 10); - - const exportedSymbols = codeGraph.symbols - .filter((symbol) => symbol.exported) - .map((symbol) => `${symbol.qualifiedName} (${symbol.filePath}:${symbol.lineStart})`) - .slice(0, 15); - - const graph: RepositoryFactGraphDocument = { - version: 1, - generatedAt: new Date().toISOString(), - targetPath: context.targetPath, - repoName: context.repoName, - nodes, - edges, - stats: { - nodes: nodes.length, - edges: edges.length, - codeGraphFiles: codeGraph.stats.files, - codeGraphSymbols: codeGraph.stats.symbols, - nodeKinds: countBy(nodes.map((node) => node.kind)), - edgeKinds: countBy(edges.map((edge) => edge.kind)) - } - }; - - await writeJsonEnsured(graphPath, graph); - await writeFileEnsured(reportPath, buildReport(context, graphPath, graph, topConnectedFiles, exportedSymbols)); - - return { - graphPath, - reportPath, - graph - }; -} diff --git a/analysis/ux_task_generator/index.ts b/analysis/ux_task_generator/index.ts deleted file mode 100644 index affdecc..0000000 --- a/analysis/ux_task_generator/index.ts +++ /dev/null @@ -1,1433 +0,0 @@ -import { existsSync } from "node:fs"; -import path from "node:path"; - -import { fileExists, readTextSafe, uniqueSorted, walkDirectory, writeFileEnsured } from "../../shared/fs-utils"; -import type { ProjectContext, RiskLevel } from "../../shared/types"; - -export type SupportedComponent = - | "Dashboard" - | "Sidebar" - | "AdminConsoleNav" - | "Forms" - | "Workspace" - | "Tables" - | "Search" - | "Dropdowns"; - -interface ComponentRule { - name: SupportedComponent; - issueKeywords: RegExp[]; - fileKeywords: RegExp[]; - defaultImpact: string; - defaultFix: string; -} - -interface FrontendCatalog { - sourceRoot: string; - scannedDirectories: string[]; - files: string[]; - byComponent: Record; -} - -export interface FrontendUsabilityAnalysis { - frontendDetected: boolean; - sourceRoot?: string; - scannedDirectories: string[]; - pageCount: number; - layoutCount: number; - componentFiles: Record; - findings: string[]; - recommendations: string[]; - suggestedTasks: UXTask[]; -} - -export interface UXTask { - component: SupportedComponent; - file: string; - problem: string; - userImpact: string; - proposedChange: string; - risk: RiskLevel; - effort: "Low" | "Medium" | "High"; -} - -export interface UXImprovementInputs { - inputFiles: string[]; - findings: string[]; - recommendations: string[]; - frontendDetected: boolean; - scannedSourceRoot?: string; - scannedDirectories: string[]; - componentFiles: Record; -} - -export interface UXImprovementArtifacts { - implementationTasksPath: string; - navigationRestructurePath: string; - formSimplificationTasksPath: string; - workspaceImprovementsPath: string; - tasks: UXTask[]; - navigationTasks: UXTask[]; - formTasks: UXTask[]; - inputFiles: string[]; - frontendDetected: boolean; -} - -interface ReportInsights { - findings: string[]; - recommendations: string[]; -} - -const REPORT_FILES = ["ux_report.md", "usability_findings.md", "workflow_analysis.md"] as const; -const PRIORITY_UI_DIRECTORIES = ["app", "domains", "shared/ui"] as const; -const FALLBACK_UI_DIRECTORIES = ["components", "layouts", "pages", "features"] as const; -const ERP_FRONTEND_PRIORITY_FILES = { - Sidebar: ["shared/ui/layout/Sidebar.tsx"], - AdminConsoleNav: ["domains/admin-console/components/AdminConsoleNav.tsx"], - Workspace: [ - "domains/expediente-workspace/components/ExpedienteWorkspace.tsx", - "domains/procedimiento-wizard/components/ProcedimientoWizard.tsx", - "domains/procedimiento-wizard/components/NextStepCard.tsx" - ], - Forms: [ - "domains/necesidades/components/NecesidadForm.tsx", - "domains/inventario-write/components/InventoryAjustesPanel.tsx", - "domains/finanzas/components/FinanzasPanel.tsx" - ], - Dashboard: [ - "domains/dashboard-institucional/components/InstitutionalDashboard.tsx", - "domains/dashboard-operativo/components/OperativeDashboard.tsx" - ], - Tables: [ - "domains/dashboard-operativo/components/ExpedientesRiesgoTable.tsx", - "domains/dashboard-operativo/components/OperacionesFueraSecuenciaTable.tsx", - "domains/dashboard-operativo/components/ProveedoresAlertadosTable.tsx" - ], - Search: [], - Dropdowns: [ - "domains/dashboard-institucional/components/InstitutionalDashboard.tsx", - "domains/inventario-write/components/InventoryAjustesPanel.tsx" - ] -} satisfies Record; -const OPERATIONAL_UX_PATTERNS = [ - /navigation/i, - /sidebar/i, - /menu/i, - /dashboard/i, - /form/i, - /field/i, - /input/i, - /label/i, - /terminology/i, - /table/i, - /search/i, - /filter/i, - /workflow/i, - /workspace/i, - /step/i, - /error/i, - /validation/i, - /dropdown/i, - /select/i, - /combobox/i, - /searchable/i, - /plain[- ]language/i, - /click/i, - /record/i, - /task/i, - /operator/i -] as const; -const NON_OPERATIONAL_UX_PATTERNS = [ - /readme/i, - /onboarding/i, - /developer documentation/i, - /documentation/i, - /installation/i, - /install/i, - /setup/i, - /contributor/i, - /api contract/i, - /openapi/i, - /prisma/i, - /server logic/i, - /backend/i, - /developer/i -] as const; - -const COMPONENT_RULES: ComponentRule[] = [ - { - name: "Dashboard", - issueKeywords: [/dashboard/i, /overview/i, /metrics/i, /widget/i, /summary/i], - fileKeywords: [/dashboard/i, /overview/i, /metrics/i, /widget/i, /home/i], - defaultImpact: "Users cannot understand system status or priorities quickly.", - defaultFix: "Simplify dashboard hierarchy, prioritize the primary actions, and remove low-value visual noise." - }, - { - name: "Sidebar", - issueKeywords: [/sidebar/i, /navigation/i, /menu/i, /nav/i, /orientation/i], - fileKeywords: [/sidebar/i, /nav/i, /menu/i, /layout/i, /shell/i], - defaultImpact: "Users lose orientation and need extra clicks to reach core workflows.", - defaultFix: "Reduce navigation depth, group items by task, and make the current location obvious." - }, - { - name: "AdminConsoleNav", - issueKeywords: [/admin/i, /catalog/i, /configuration/i, /observability/i, /administrative navigation/i], - fileKeywords: [/adminconsolenav/i, /admin-console/i, /catalog/i, /configuracion/i], - defaultImpact: "Administrative users must interpret technical categories before they can complete a basic task.", - defaultFix: "Rename technical labels in the admin menu, group related settings, and surface the highest-frequency options first." - }, - { - name: "Forms", - issueKeywords: [/form/i, /field/i, /input/i, /validation/i, /label/i, /terminology/i], - fileKeywords: [/form/i, /input/i, /field/i, /modal/i, /dialog/i], - defaultImpact: "Users take longer to complete tasks and are more likely to submit incorrect data.", - defaultFix: "Split long forms, clarify labels, add helper text, and surface inline validation near the affected field." - }, - { - name: "Workspace", - issueKeywords: [/workflow/i, /workspace/i, /step/i, /journey/i, /handoff/i], - fileKeywords: [/workspace/i, /shell/i, /layout/i, /page/i, /wizard/i], - defaultImpact: "Users must jump across screens and lose context while completing a single flow.", - defaultFix: "Reorganize the workspace around the main step sequence and keep related actions in the same surface." - }, - { - name: "Tables", - issueKeywords: [/table/i, /grid/i, /row/i, /column/i, /list/i], - fileKeywords: [/table/i, /grid/i, /list/i, /row/i], - defaultImpact: "Users struggle to scan records, compare values, and act on data quickly.", - defaultFix: "Improve table hierarchy, prioritize the most relevant columns, and simplify row-level actions." - }, - { - name: "Search", - issueKeywords: [/search/i, /filter/i, /find/i, /lookup/i, /query/i], - fileKeywords: [/search/i, /filter/i, /lookup/i, /autocomplete/i], - defaultImpact: "Users spend too much time locating records or filtering large datasets.", - defaultFix: "Make search and filters more prominent, support faster refinement, and improve empty-state guidance." - }, - { - name: "Dropdowns", - issueKeywords: [/dropdown/i, /select/i, /combobox/i, /picker/i, /option/i], - fileKeywords: [/dropdown/i, /select/i, /combo/i, /picker/i], - defaultImpact: "Users struggle to choose the right option and may select incorrect values.", - defaultFix: "Reduce option overload, group related values, and use searchable selects when the list is long." - } -]; - -const FINDING_SECTION_PATTERNS = [ - /human deterministic findings/i, - /ai insights/i, - /main usability problems/i, - /workflow/i, - /findings/i, - /pain points/i -]; - -const RECOMMENDATION_SECTION_PATTERNS = [ - /combined recommendations/i, - /recommendations/i, - /task list/i, - /proposed improvements/i, - /action items/i -]; - -const STOP_WORDS = new Set([ - "the", - "and", - "with", - "that", - "this", - "from", - "into", - "without", - "have", - "will", - "more", - "less", - "than", - "they", - "them", - "their", - "while", - "where", - "which", - "when", - "what", - "your", - "users", - "user", - "system", - "application", - "frontend", - "repository" -]); - -const IGNORED_FINDINGS = [/ai insights were unavailable for this cycle/i, /^none$/i]; -const NAVIGATION_COMPONENTS = new Set(["Sidebar", "AdminConsoleNav", "Workspace"]); -const FORM_COMPONENTS = new Set(["Forms", "Dropdowns"]); - -function normalizeMarkdownText(value: string): string { - return value - .replace(/`([^`]+)`/g, "$1") - .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") - .replace(/\*\*([^*]+)\*\*/g, "$1") - .replace(/\*([^*]+)\*/g, "$1") - .replace(/^[-*]\s+/, "") - .replace(/^\d+\.\s+/, "") - .replace(/\s+/g, " ") - .trim(); -} - -function parseMarkdownSections(content: string): Array<{ title: string; lines: string[] }> { - const sections: Array<{ title: string; lines: string[] }> = []; - let currentTitle = "root"; - let currentLines: string[] = []; - - for (const rawLine of content.split(/\r?\n/)) { - const headerMatch = rawLine.match(/^#{1,6}\s+(.+?)\s*$/); - if (headerMatch) { - sections.push({ title: currentTitle, lines: currentLines }); - currentTitle = headerMatch[1] ?? "root"; - currentLines = []; - continue; - } - - currentLines.push(rawLine); - } - - sections.push({ title: currentTitle, lines: currentLines }); - return sections.filter((section) => section.lines.length > 0); -} - -function extractListItems(lines: string[]): string[] { - return lines - .map((line) => line.trim()) - .filter((line) => /^[-*]\s+/.test(line) || /^\d+\.\s+/.test(line)) - .map((line) => normalizeMarkdownText(line)) - .filter(Boolean); -} - -function canonicalizeUXText(value: string): string { - return normalizeMarkdownText(value) - .replace(/^\[(high|medium|low)\]\s*/i, "") - .replace(/^(the|a|an)\s+/i, "") - .toLowerCase(); -} - -function dedupeItems(items: string[]): string[] { - const seen = new Set(); - const deduped: string[] = []; - - for (const item of items) { - if (IGNORED_FINDINGS.some((pattern) => pattern.test(item))) { - continue; - } - - const normalized = canonicalizeUXText(item); - if (seen.has(normalized)) { - continue; - } - - seen.add(normalized); - deduped.push(item); - } - - return deduped; -} - -function extractInsights(content: string): ReportInsights { - const sections = parseMarkdownSections(content); - const findings: string[] = []; - const recommendations: string[] = []; - - for (const section of sections) { - const items = extractListItems(section.lines); - if (items.length === 0) { - continue; - } - - if (FINDING_SECTION_PATTERNS.some((pattern) => pattern.test(section.title))) { - findings.push(...items); - continue; - } - - if (RECOMMENDATION_SECTION_PATTERNS.some((pattern) => pattern.test(section.title))) { - recommendations.push(...items); - } - } - - if (findings.length === 0 && recommendations.length === 0) { - const items = extractListItems(content.split(/\r?\n/)); - findings.push(...items); - } - - return { - findings: dedupeItems(findings), - recommendations: dedupeItems(recommendations) - }; -} - -function tokenize(value: string): string[] { - return normalizeMarkdownText(value) - .toLowerCase() - .split(/[^a-z0-9]+/) - .filter((token) => token.length > 3 && !STOP_WORDS.has(token)); -} - -function isOperationalUXText(value: string): boolean { - const normalized = normalizeMarkdownText(value); - if (!normalized) { - return false; - } - - if (NON_OPERATIONAL_UX_PATTERNS.some((pattern) => pattern.test(normalized))) { - return false; - } - - return OPERATIONAL_UX_PATTERNS.some((pattern) => pattern.test(normalized)); -} - -export function filterOperationalUXItems(items: string[]): string[] { - return dedupeItems(items.filter((item) => isOperationalUXText(item))); -} - -function scoreRecommendation(issue: string, recommendation: string, component: SupportedComponent): number { - let score = 0; - const issueTokens = new Set(tokenize(issue)); - const recommendationTokens = tokenize(recommendation); - - for (const token of recommendationTokens) { - if (issueTokens.has(token)) { - score += 2; - } - } - - if (recommendation.toLowerCase().includes(component.toLowerCase())) { - score += 2; - } - - return score; -} - -function selectSourceRoot(targetPath: string): string | undefined { - const candidates = [ - path.join(targetPath, "src"), - path.join(targetPath, "app", "src"), - path.join(targetPath, "frontend", "src"), - path.join(targetPath, "web", "src"), - path.join(targetPath, "apps", "web", "src"), - path.join(targetPath, "packages", "web", "src") - ]; - - return candidates.find((candidate) => existsSync(candidate)); -} - -async function scanFrontendComponents(targetPath: string): Promise { - const sourceRoot = selectSourceRoot(targetPath); - if (!sourceRoot) { - return undefined; - } - - const preferredDirectories = PRIORITY_UI_DIRECTORIES.map((directory) => path.join(sourceRoot, directory)).filter((directory) => - existsSync(directory) - ); - const fallbackDirectories = FALLBACK_UI_DIRECTORIES.map((directory) => path.join(sourceRoot, directory)).filter((directory) => - existsSync(directory) - ); - const directoriesToScan = [...preferredDirectories, ...fallbackDirectories]; - const scanRoots = directoriesToScan.length > 0 ? directoriesToScan : [sourceRoot]; - const files = uniqueSorted( - ( - await Promise.all( - scanRoots.map(async (scanRoot) => - (await walkDirectory(scanRoot)) - .filter((file) => /\.(tsx?|jsx?)$/i.test(file)) - .map((file) => path.relative(sourceRoot, path.join(scanRoot, file.replace(/^\.\/+/, "")))) - ) - ) - ) - .flat() - .map((file) => file.replace(/\\/g, "/")) - .filter((file) => !file.startsWith("..")) - ).sort((left, right) => left.localeCompare(right)); - - if (files.length === 0) { - return undefined; - } - - const byComponent = Object.fromEntries( - COMPONENT_RULES.map((rule) => [ - rule.name, - files.filter((file) => rule.fileKeywords.some((pattern) => pattern.test(file))) - ]) - ) as Record; - - return { - sourceRoot, - scannedDirectories: scanRoots.map((directory) => path.relative(targetPath, directory) || "."), - files, - byComponent - }; -} - -function selectComponent(issue: string, catalog: FrontendCatalog): ComponentRule { - const scoredRules = COMPONENT_RULES.map((rule) => { - let score = 0; - - for (const keyword of rule.issueKeywords) { - if (keyword.test(issue)) { - score += 3; - } - } - - if ((catalog.byComponent[rule.name] ?? []).length > 0) { - score += 1; - } - - return { rule, score }; - }).sort((left, right) => right.score - left.score); - - return scoredRules[0]?.rule ?? COMPONENT_RULES[0]!; -} - -function selectComponentFile(rule: ComponentRule, catalog: FrontendCatalog): string { - const priorityFiles = ERP_FRONTEND_PRIORITY_FILES[rule.name] as string[]; - const directMatch = preferredFilesForComponent(rule.name, catalog).sort((left, right) => { - const leftExactPriority = priorityFiles.includes(left) ? 0 : 1; - const rightExactPriority = priorityFiles.includes(right) ? 0 : 1; - if (leftExactPriority !== rightExactPriority) { - return leftExactPriority - rightExactPriority; - } - - const leftSurfacePriority = /^(app|domains|shared\/ui|components|features|layouts|pages)\//.test(left) ? 0 : 1; - const rightSurfacePriority = /^(app|domains|shared\/ui|components|features|layouts|pages)\//.test(right) ? 0 : 1; - if (leftSurfacePriority !== rightSurfacePriority) { - return leftSurfacePriority - rightSurfacePriority; - } - - return left.localeCompare(right); - })[0]; - if (directMatch) { - return `src/${directMatch}`; - } - - const shellFallback = - catalog.files.find((file) => /(app|layout|shell|page)\.(tsx?|jsx?)$/i.test(file)) ?? - catalog.files[0]; - - return shellFallback ? `src/${shellFallback}` : "src/"; -} - -function inferUserImpact(problem: string, rule: ComponentRule): string { - const normalized = problem.toLowerCase(); - - if (/cognitive load|terminology|label|validation|form/.test(normalized)) { - return COMPONENT_RULES.find((candidate) => candidate.name === "Forms")?.defaultImpact ?? rule.defaultImpact; - } - - if (/navigation|sidebar|menu|orientation/.test(normalized)) { - return COMPONENT_RULES.find((candidate) => candidate.name === "Sidebar")?.defaultImpact ?? rule.defaultImpact; - } - - if (/workflow|workspace|step|journey/.test(normalized)) { - return COMPONENT_RULES.find((candidate) => candidate.name === "Workspace")?.defaultImpact ?? rule.defaultImpact; - } - - if (/search|filter|lookup/.test(normalized)) { - return COMPONENT_RULES.find((candidate) => candidate.name === "Search")?.defaultImpact ?? rule.defaultImpact; - } - - if (/table|grid|column|row/.test(normalized)) { - return COMPONENT_RULES.find((candidate) => candidate.name === "Tables")?.defaultImpact ?? rule.defaultImpact; - } - - return rule.defaultImpact; -} - -function inferRisk(problem: string, effort: UXTask["effort"]): RiskLevel { - if (/\[high\]|critical|blocking|impossible|overly complex/i.test(problem) || effort === "High") { - return "high"; - } - - if (/\[medium\]|ambigu|confus|cognitive load|workflow/i.test(problem) || effort === "Medium") { - return "medium"; - } - - return "low"; -} - -function inferEffort(component: SupportedComponent, file: string, problem: string): UXTask["effort"] { - let score = 1; - - if (["Dashboard", "Sidebar", "Workspace"].includes(component)) { - score += 1; - } - - if (/(layout|shell|page|workspace|wizard)/i.test(file)) { - score += 1; - } - - if (/workflow|navigation|cross-screen|multiple|overly complex/i.test(problem)) { - score += 1; - } - - if (score >= 4) { - return "High"; - } - - if (score >= 3) { - return "Medium"; - } - - return "Low"; -} - -function selectRecommendation(issue: string, rule: ComponentRule, recommendations: string[]): string { - const ranked = recommendations - .map((recommendation) => ({ - recommendation, - score: scoreRecommendation(issue, recommendation, rule.name) - })) - .sort((left, right) => right.score - left.score); - - return ranked[0]?.score ? ranked[0].recommendation : rule.defaultFix; -} - -function renderTaskReport(task: UXTask): string { - return [ - "### Task", - `Component: ${task.component}`, - `File: ${task.file}`, - `Problem: ${task.problem}`, - `User impact: ${task.userImpact}`, - `Proposed change: ${task.proposedChange}`, - `Risk: ${task.risk}`, - `Effort: ${task.effort}`, - "" - ].join("\n"); -} - -function renderTaskReportFile(tasks: UXTask[], inputFiles: string[]): string { - const fileList = inputFiles.length > 0 ? inputFiles.map((file) => `- ${file}`).join("\n") : "- reports/ux_report.md"; - const body = tasks.map((task) => renderTaskReport(task)).join("\n"); - - return `# UX Implementation Tasks - -Generated from: -${fileList} - -${body} -`; -} - -function renderEmptyReport(title: string, inputFiles: string[], message: string): string { - const fileList = inputFiles.length > 0 ? inputFiles.map((file) => `- ${file}`).join("\n") : "- None"; - - return `# ${title} - -Generated from: -${fileList} - -${message} -`; -} - -function renderBulletList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function filterNavigationTasks(tasks: UXTask[]): UXTask[] { - return tasks.filter( - (task) => - NAVIGATION_COMPONENTS.has(task.component) || - /navigation|sidebar|menu|admin menu|workflow visibility|next action|wizard/i.test( - `${task.problem} ${task.proposedChange}` - ) - ); -} - -function filterFormTasks(tasks: UXTask[]): UXTask[] { - return tasks.filter( - (task) => - FORM_COMPONENTS.has(task.component) || - /form|field|validation|label|dropdown|select|helper text|technical labels|uuid|identifier/i.test( - `${task.problem} ${task.proposedChange}` - ) - ); -} - -function renderNavigationRestructure(tasks: UXTask[], inputFiles: string[]): string { - if (tasks.length === 0) { - return renderEmptyReport( - "Navigation Restructure", - inputFiles, - "No navigation simplification tasks were derived from the current UX inputs." - ); - } - - const frictionPoints = dedupeItems(tasks.map((task) => task.problem)); - const simplifications = dedupeItems(tasks.map((task) => task.proposedChange)); - - return `# Navigation Restructure - -Generated from: -${inputFiles.map((file) => `- ${file}`).join("\n")} - -## Friction Points - -${renderBulletList(frictionPoints)} - -## Proposed Simplifications - -${renderBulletList(simplifications)} - -## Tasks - -${tasks.map((task) => renderTaskReport(task)).join("\n")} -`; -} - -function renderFormSimplificationTasks(tasks: UXTask[], inputFiles: string[]): string { - if (tasks.length === 0) { - return renderEmptyReport( - "Form Simplification Tasks", - inputFiles, - "No form simplification tasks were derived from the current UX inputs." - ); - } - - const painPoints = dedupeItems(tasks.map((task) => task.problem)); - - return `# Form Simplification Tasks - -Generated from: -${inputFiles.map((file) => `- ${file}`).join("\n")} - -## Form Friction Points - -${renderBulletList(painPoints)} - -## Tasks - -${tasks.map((task) => renderTaskReport(task)).join("\n")} -`; -} - -function renderWorkspaceImprovements( - tasks: UXTask[], - inputFiles: string[], - analysis: FrontendUsabilityAnalysis -): string { - if (!analysis.frontendDetected) { - return renderEmptyReport( - "Workspace Improvements", - inputFiles, - "No frontend UI surface was detected, so no workspace-level usability improvements were generated." - ); - } - - const priorityTasks = tasks.filter((task) => - ["Workspace", "Dashboard", "Sidebar", "Tables", "Search", "Dropdowns"].includes(task.component) - ); - const frictionPoints = dedupeItems([ - ...analysis.findings, - ...priorityTasks.map((task) => task.problem) - ]); - const actionPlan = dedupeItems([ - ...analysis.recommendations, - ...priorityTasks.map((task) => task.proposedChange) - ]); - - return `# Workspace Improvements - -Generated from: -${inputFiles.map((file) => `- ${file}`).join("\n")} - -## User Persona - -- Government administrative staff -- Non-technical -- Repetitive form-based work -- Needs minimal steps and clear language - -## Operating Rule - -- Prioritize functional usability over visual design. - -## UI Surfaces Reviewed - -- Scanned directories: ${analysis.scannedDirectories.join(", ") || "none detected"} -- Routed pages: ${analysis.pageCount} -- Layout shells: ${analysis.layoutCount} -- Sidebar files: ${analysis.componentFiles.Sidebar.length} -- Dashboard files: ${analysis.componentFiles.Dashboard.length} -- Workspace files: ${analysis.componentFiles.Workspace.length} -- Form files: ${analysis.componentFiles.Forms.length} -- Table files: ${analysis.componentFiles.Tables.length} -- Search/filter files: ${analysis.componentFiles.Search.length} - -## Priority Friction Points - -${renderBulletList(frictionPoints)} - -## Improvement Directions - -${renderBulletList(actionPlan)} - -## Component-Level Tasks - -${priorityTasks.length > 0 ? priorityTasks.map((task) => renderTaskReport(task)).join("\n") : "No workspace-level tasks were derived from the current UX inputs.\n"} -`; -} - -function buildTasks(findings: string[], recommendations: string[], catalog: FrontendCatalog): UXTask[] { - const dedupedTasks: UXTask[] = []; - const seen = new Set(); - - for (const finding of findings) { - const rule = selectComponent(finding, catalog); - const file = selectComponentFile(rule, catalog); - const effort = inferEffort(rule.name, file, finding); - const task: UXTask = { - component: rule.name, - file, - problem: finding, - userImpact: inferUserImpact(finding, rule), - proposedChange: selectRecommendation(finding, rule, recommendations), - risk: inferRisk(finding, effort), - effort - }; - - const dedupeKey = `${task.component}::${canonicalizeUXText(task.problem)}::${canonicalizeUXText(task.proposedChange)}`; - if (seen.has(dedupeKey)) { - continue; - } - - seen.add(dedupeKey); - dedupedTasks.push(task); - } - - return dedupedTasks; -} - -function emptyComponentCatalog(): Record { - return COMPONENT_RULES.reduce>((catalog, rule) => { - catalog[rule.name] = []; - return catalog; - }, {} as Record); -} - -export function formatComponentCatalog(inputs: UXImprovementInputs): string { - const lines = Object.entries(inputs.componentFiles).map(([component, files]) => { - const sample = files.length > 0 ? files.slice(0, 3).join(", ") : "none detected"; - return `- ${component}: ${sample}`; - }); - - return [ - `Frontend detected: ${inputs.frontendDetected ? "yes" : "no"}`, - `Source root: ${inputs.scannedSourceRoot ?? "not detected"}`, - `Scanned directories: ${inputs.scannedDirectories.join(", ") || "none detected"}`, - "Component file hints:", - ...lines - ].join("\n"); -} - -function countMatches(value: string, pattern: RegExp): number { - const matches = value.match(pattern); - return matches?.length ?? 0; -} - -interface FrontendFileEntry { - file: string; - content: string; -} - -function preferredFilesForComponent(component: SupportedComponent, catalog: FrontendCatalog): string[] { - const preferred = ERP_FRONTEND_PRIORITY_FILES[component].filter((candidate) => catalog.files.includes(candidate)); - const fallback = (catalog.byComponent[component] ?? []).filter((candidate) => !preferred.includes(candidate)); - return [...preferred, ...fallback]; -} - -function getFileEntry(entries: FrontendFileEntry[], file: string): FrontendFileEntry | undefined { - return entries.find((entry) => entry.file === file); -} - -function createDeterministicTask( - component: SupportedComponent, - file: string, - problem: string, - userImpact: string, - proposedChange: string, - risk: RiskLevel, - effort: UXTask["effort"] -): UXTask { - return { - component, - file: `src/${file}`, - problem, - userImpact, - proposedChange, - risk, - effort - }; -} - -function dedupeTasks(tasks: UXTask[]): UXTask[] { - const seen = new Set(); - const deduped: UXTask[] = []; - - for (const task of tasks) { - const key = [ - task.component, - task.file, - canonicalizeUXText(task.problem), - canonicalizeUXText(task.proposedChange) - ].join("::"); - - if (seen.has(key)) { - continue; - } - - seen.add(key); - deduped.push(task); - } - - return deduped; -} - -function buildERPFrontendTasks(catalog: FrontendCatalog, contents: FrontendFileEntry[]): UXTask[] { - const tasks: UXTask[] = []; - const pageCount = contents.filter( - (entry) => /(^|\/)app\/.+\/page\.(tsx?|jsx?)$/i.test(entry.file) || /(^|\/)pages\/.+\.(tsx?|jsx?)$/i.test(entry.file) - ).length; - - const sidebarFile = preferredFilesForComponent("Sidebar", catalog)[0]; - if (sidebarFile) { - const sidebarEntry = getFileEntry(contents, sidebarFile); - const navItems = sidebarEntry ? countMatches(sidebarEntry.content, /href:\s*['"]/g) : 0; - const sections = sidebarEntry ? countMatches(sidebarEntry.content, /title:\s*['"]/g) : 0; - if (navItems >= 18 || pageCount >= 40) { - tasks.push( - createDeterministicTask( - "Sidebar", - sidebarFile, - `The navigation is spread across ${pageCount} routed screens, ${sections} sidebar sections, and ${navItems} menu options, which is too dense for non-technical administrative users.`, - "Users lose orientation and need extra clicks before they reach the procurement step they use every day.", - "Group menu entries by procurement workflow, surface the top 5-7 daily actions first, and rename ambiguous labels in plain administrative language.", - "high", - "High" - ) - ); - } - } - - const adminConsoleNavFile = preferredFilesForComponent("AdminConsoleNav", catalog)[0]; - if (adminConsoleNavFile) { - const adminEntry = getFileEntry(contents, adminConsoleNavFile); - const adminItems = adminEntry ? countMatches(adminEntry.content, /href:\s*['"]/g) : 0; - if (adminItems >= 5) { - tasks.push( - createDeterministicTask( - "AdminConsoleNav", - adminConsoleNavFile, - `The administrative navigation exposes ${adminItems} peer options with technical labels such as observability and importaciones, which forces staff to interpret system categories instead of business tasks.`, - "Administrative users spend more time deciding where to click and are more likely to choose the wrong configuration area.", - "Rename technical options in user language, move the most frequent actions first, and reserve advanced technical tools for a secondary group.", - "medium", - "Low" - ) - ); - } - } - - const workspaceFile = preferredFilesForComponent("Workspace", catalog)[0]; - if (workspaceFile) { - const workspaceEntry = getFileEntry(contents, workspaceFile); - const linkedPanels = workspaceEntry ? countMatches(workspaceEntry.content, /Panel\b/g) : 0; - if (linkedPanels >= 5) { - tasks.push( - createDeterministicTask( - "Workspace", - workspaceFile, - "The main workspace combines timeline, risks, financial tracking, inventory status, and the procedure wizard in one dense surface, which weakens the visibility of the current step and next action.", - "Staff have to infer what to do next and may leave the workspace without completing the required procurement step.", - "Make the current step and next action persistent at the top of the workspace and move secondary monitoring panels below the primary workflow area.", - "high", - "High" - ) - ); - } - } - - const wizardFile = preferredFilesForComponent("Workspace", catalog).find((file) => /ProcedimientoWizard\.tsx$/i.test(file)); - if (wizardFile) { - const wizardEntry = getFileEntry(contents, wizardFile); - const quickActions = wizardEntry ? countMatches(wizardEntry.content, /Acciones rápidas|QuickAction|getWizardQuickActionTarget/g) : 0; - if (quickActions > 0) { - tasks.push( - createDeterministicTask( - "Workspace", - wizardFile, - "The procedure wizard exposes multiple quick actions and stage switches before explaining the current step in plain language.", - "Users can jump to the wrong procurement stage or miss the legally required order of actions.", - "Show a short explanation of the current stage, keep one primary next action visible, and demote secondary quick actions behind clearer labels.", - "medium", - "Medium" - ) - ); - } - } - - const nextStepCardFile = preferredFilesForComponent("Workspace", catalog).find((file) => /NextStepCard\.tsx$/i.test(file)); - if (nextStepCardFile) { - tasks.push( - createDeterministicTask( - "Workspace", - nextStepCardFile, - "The next-step card uses generic text like 'Siguiente paso recomendado' and 'Ir al paso', which does not explain the concrete action that the operator must complete.", - "Users do not understand why the next step matters or what they should complete before leaving the screen.", - "Rewrite the heading and button copy in action-oriented language and add a one-line explanation of the pending administrative task.", - "low", - "Low" - ) - ); - } - - const necesidadFormFile = preferredFilesForComponent("Forms", catalog).find((file) => /NecesidadForm\.tsx$/i.test(file)); - if (necesidadFormFile) { - const necesidadEntry = getFileEntry(contents, necesidadFormFile); - const technicalLabels = necesidadEntry - ? [ - "expedienteId", - "area_id", - "clasificacion_bien", - "justificacion" - ].filter((label) => necesidadEntry.content.includes(label)) - : []; - if (technicalLabels.length >= 3) { - tasks.push( - createDeterministicTask( - "Forms", - necesidadFormFile, - `NecesidadForm still shows technical labels (${technicalLabels.join(", ")}) instead of plain-language procurement terms.`, - "Users have to translate internal field names before they can capture the request correctly.", - "Replace technical labels with administrative language, keep helper text next to complex fields, and convert area or classification capture to guided selection where possible.", - "medium", - "Low" - ) - ); - } - } - - const inventoryAjustesFile = preferredFilesForComponent("Forms", catalog).find((file) => /InventoryAjustesPanel\.tsx$/i.test(file)); - if (inventoryAjustesFile) { - const inventoryEntry = getFileEntry(contents, inventoryAjustesFile); - const technicalSignals = inventoryEntry - ? [ - "inventarioId", - "productoId", - "expedienteId", - "correlationId", - "Endpoints contractuales" - ].filter((signal) => inventoryEntry.content.includes(signal)) - : []; - if (technicalSignals.length >= 4) { - tasks.push( - createDeterministicTask( - "Forms", - inventoryAjustesFile, - "InventoryAjustesPanel exposes UUID-driven fields and endpoint terminology in the main form, which is too technical for day-to-day inventory adjustments.", - "Operators need technical identifiers before they can register an adjustment, increasing delays and data-entry mistakes.", - "Rename technical fields in user language, remove endpoint references from the main panel, and guide users through inventory, product, and expediente selection with clearer prompts.", - "high", - "Medium" - ) - ); - } - } - - const finanzasPanelFile = preferredFilesForComponent("Forms", catalog).find((file) => /FinanzasPanel\.tsx$/i.test(file)); - if (finanzasPanelFile) { - const finanzasEntry = getFileEntry(contents, finanzasPanelFile); - const idPrompts = finanzasEntry - ? ["contratoId", "ordenCompraId", "Capture contratoId"].filter((token) => finanzasEntry.content.includes(token)) - : []; - if (idPrompts.length >= 2) { - tasks.push( - createDeterministicTask( - "Forms", - finanzasPanelFile, - "FinanzasPanel starts the financial flow by asking for contratoId and ordenCompraId, which assumes technical knowledge instead of business context.", - "Administrative staff cannot continue unless they already know internal identifiers for the contract and purchase order.", - "Rename identifiers in plain language, explain the required context, and guide the user to select the contract or order before loading the financial flow.", - "high", - "Medium" - ) - ); - } - } - - const institutionalDashboardFile = preferredFilesForComponent("Dashboard", catalog).find((file) => - /InstitutionalDashboard\.tsx$/i.test(file) - ); - if (institutionalDashboardFile) { - const institutionalEntry = getFileEntry(contents, institutionalDashboardFile); - const viewCount = institutionalEntry ? countMatches(institutionalEntry.content, /key:\s*['"][a-z-]+['"]/g) : 0; - if (viewCount >= 4) { - tasks.push( - createDeterministicTask( - "Dashboard", - institutionalDashboardFile, - `InstitutionalDashboard mixes ${viewCount} dashboard views before showing the most urgent public procurement actions.`, - "Users must choose between several monitoring perspectives before they understand what is pending today.", - "Prioritize pending actions and alerts in the first view, simplify tab labels, and use plain-language summaries that explain what requires attention now.", - "medium", - "Medium" - ) - ); - } - } - - const operativeDashboardFile = preferredFilesForComponent("Dashboard", catalog).find((file) => /OperativeDashboard\.tsx$/i.test(file)); - if (operativeDashboardFile) { - const operativeEntry = getFileEntry(contents, operativeDashboardFile); - const tableCount = operativeEntry ? countMatches(operativeEntry.content, /Table\b/g) : 0; - const searchSignals = operativeEntry ? countMatches(operativeEntry.content, /\bsearch\b|\bfilter\b/i) : 0; - if (tableCount >= 2 && searchSignals === 0) { - tasks.push( - createDeterministicTask( - "Dashboard", - operativeDashboardFile, - "The operative dashboard shows multiple tables and alert lists without an obvious filtering or narrowing mechanism for high-priority records.", - "Users must scan every row manually to find the expediente or supplier that needs attention.", - "Introduce clearer prioritization labels and a visible filter/search entry point for the highest-volume dashboard lists.", - "medium", - "Medium" - ) - ); - } - } - - return dedupeTasks(tasks); -} - -function buildGenericOperationalTasks( - catalog: FrontendCatalog, - counts: { - pageCount: number; - layoutCount: number; - rawInputSignals: number; - guidedSelectionSignals: number; - searchSignals: number; - filterSignals: number; - errorSignals: number; - } -): UXTask[] { - const tasks: UXTask[] = []; - const sidebarFile = preferredFilesForComponent("Sidebar", catalog)[0]; - const formFile = preferredFilesForComponent("Forms", catalog)[0]; - const workspaceFile = preferredFilesForComponent("Workspace", catalog)[0]; - const tableFile = preferredFilesForComponent("Tables", catalog)[0]; - - if (sidebarFile && (counts.pageCount >= 3 || counts.layoutCount >= 1)) { - tasks.push( - createDeterministicTask( - "Sidebar", - sidebarFile, - `Navigation is spread across ${counts.pageCount} routed screens and shared layout shells, which is difficult for non-technical operators to scan quickly.`, - "Users need extra clicks before they can reach the screen required for their daily task.", - "Reduce menu depth, group the most common actions together, and keep workflow labels explicit.", - "medium", - "Medium" - ) - ); - } - - if (formFile && counts.rawInputSignals > counts.guidedSelectionSignals) { - tasks.push( - createDeterministicTask( - "Forms", - formFile, - "Form workflows appear to rely more on raw text inputs than guided selectors, increasing typing and classification errors.", - "Users take longer to complete repetitive forms and are more likely to enter inconsistent data.", - "Replace raw IDs or category inputs with dropdowns, comboboxes, or clearer field labels where valid values are known.", - "medium", - "Low" - ) - ); - } - - if (workspaceFile) { - tasks.push( - createDeterministicTask( - "Workspace", - workspaceFile, - "Workspace views need clearer workflow visibility so users can see the current step, next action, and completion state.", - "Users lose context while moving through a multi-step operational flow.", - "Keep the current stage and next action visible in the same workspace and demote secondary information.", - "medium", - "Medium" - ) - ); - } - - if (formFile && counts.errorSignals < counts.rawInputSignals) { - tasks.push( - createDeterministicTask( - "Forms", - formFile, - "Error guidance appears weaker than the amount of data entry required in the interface.", - "Users do not know how to fix mistakes when a form fails validation.", - "Show inline validation and plain-language error messages next to the affected field.", - "medium", - "Low" - ) - ); - } - - if (tableFile && counts.searchSignals + counts.filterSignals === 0) { - tasks.push( - createDeterministicTask( - "Tables", - tableFile, - "Record-heavy views do not expose an obvious search or filter entry point.", - "Users must inspect rows manually to locate the correct record.", - "Add a visible search/filter control near the main table surface and support lookup by business terms.", - "medium", - "Medium" - ) - ); - } - - return dedupeTasks(tasks); -} - -function buildOperationalUsabilityFindings( - catalog: FrontendCatalog, - counts: { - pageCount: number; - layoutCount: number; - rawInputSignals: number; - guidedSelectionSignals: number; - searchSignals: number; - filterSignals: number; - errorSignals: number; - }, - suggestedTasks: UXTask[] -): FrontendUsabilityAnalysis { - return { - frontendDetected: true, - sourceRoot: catalog.sourceRoot, - scannedDirectories: catalog.scannedDirectories, - pageCount: counts.pageCount, - layoutCount: counts.layoutCount, - componentFiles: catalog.byComponent, - findings: filterOperationalUXItems(suggestedTasks.map((task) => task.problem)), - recommendations: filterOperationalUXItems(suggestedTasks.map((task) => task.proposedChange)), - suggestedTasks - }; -} - -export async function analyzeFrontendUsability(targetPath: string): Promise { - const catalog = await scanFrontendComponents(targetPath); - - if (!catalog) { - return { - frontendDetected: false, - scannedDirectories: [], - pageCount: 0, - layoutCount: 0, - componentFiles: emptyComponentCatalog(), - findings: [], - recommendations: [], - suggestedTasks: [] - }; - } - - const contents = await Promise.all( - catalog.files.map(async (file) => ({ - file, - content: await readTextSafe(path.join(catalog.sourceRoot, file)) - })) - ); - - const counts = contents.reduce( - (summary, entry) => { - if (/(^|\/)app\/.+\/page\.(tsx?|jsx?)$/i.test(entry.file) || /(^|\/)pages\/.+\.(tsx?|jsx?)$/i.test(entry.file)) { - summary.pageCount += 1; - } - - if (/(^|\/)layout\.(tsx?|jsx?)$/i.test(entry.file)) { - summary.layoutCount += 1; - } - - summary.rawInputSignals += countMatches(entry.content, / 0 ? erpTasks : buildGenericOperationalTasks(catalog, counts); - return buildOperationalUsabilityFindings(catalog, counts, suggestedTasks); -} - -export function formatFrontendUsabilityAnalysis(analysis: FrontendUsabilityAnalysis): string { - const componentLines = Object.entries(analysis.componentFiles).map(([component, files]) => { - const sample = files.length > 0 ? files.slice(0, 3).join(", ") : "none detected"; - return `- ${component}: ${sample}`; - }); - - return [ - `Frontend detected: ${analysis.frontendDetected ? "yes" : "no"}`, - `Source root: ${analysis.sourceRoot ?? "not detected"}`, - `Scanned directories: ${analysis.scannedDirectories.join(", ") || "none detected"}`, - `Routed pages: ${analysis.pageCount}`, - `Layouts: ${analysis.layoutCount}`, - `Deterministic UX tasks: ${analysis.suggestedTasks.length}`, - "Priority UI surfaces:", - ...componentLines, - `Operational findings: ${analysis.findings.join(" | ") || "None"}`, - `Operational recommendations: ${analysis.recommendations.join(" | ") || "None"}` - ].join("\n"); -} - -export async function loadUXImprovementInputs(context: ProjectContext): Promise { - const catalog = await scanFrontendComponents(context.targetPath); - const existingInputs: string[] = []; - const findings: string[] = []; - const recommendations: string[] = []; - - for (const fileName of REPORT_FILES) { - const reportPath = path.join(context.reportsDir, fileName); - if (!(await fileExists(reportPath))) { - continue; - } - - existingInputs.push(`reports/${fileName}`); - const content = await readTextSafe(reportPath); - const extracted = extractInsights(content); - findings.push(...extracted.findings); - recommendations.push(...extracted.recommendations); - } - - return { - inputFiles: existingInputs, - findings: filterOperationalUXItems(findings), - recommendations: filterOperationalUXItems(recommendations), - frontendDetected: Boolean(catalog), - scannedSourceRoot: catalog?.sourceRoot, - scannedDirectories: catalog?.scannedDirectories ?? [], - componentFiles: catalog?.byComponent ?? emptyComponentCatalog() - }; -} - -interface UXImprovementArtifactOptions { - findings?: string[]; - recommendations?: string[]; - inputFiles?: string[]; - emptyMessage?: string; -} - -export async function generateUXImprovementArtifacts( - context: ProjectContext, - options: UXImprovementArtifactOptions = {} -): Promise { - const inputs = await loadUXImprovementInputs(context); - const catalog = await scanFrontendComponents(context.targetPath); - const usabilityAnalysis = await analyzeFrontendUsability(context.targetPath); - const inputFiles = options.inputFiles ?? inputs.inputFiles; - const findings = filterOperationalUXItems([ - ...(options.findings ?? inputs.findings), - ...usabilityAnalysis.findings - ]); - const recommendations = filterOperationalUXItems([ - ...(options.recommendations ?? inputs.recommendations), - ...usabilityAnalysis.recommendations - ]); - - const implementationTasksPath = path.join(context.outputPath, "UX_IMPLEMENTATION_TASKS.md"); - const navigationRestructurePath = path.join(context.outputPath, "NAVIGATION_RESTRUCTURE.md"); - const formSimplificationTasksPath = path.join(context.outputPath, "FORM_SIMPLIFICATION_TASKS.md"); - const workspaceImprovementsPath = path.join(context.outputPath, "WORKSPACE_IMPROVEMENTS.md"); - - if (!catalog) { - const message = options.emptyMessage ?? "No frontend component surface was detected under src, so no UI implementation tasks were generated."; - await Promise.all([ - writeFileEnsured(implementationTasksPath, renderEmptyReport("UX Implementation Tasks", inputFiles, message)), - writeFileEnsured(navigationRestructurePath, renderEmptyReport("Navigation Restructure", inputFiles, message)), - writeFileEnsured(formSimplificationTasksPath, renderEmptyReport("Form Simplification Tasks", inputFiles, message)), - writeFileEnsured(workspaceImprovementsPath, renderEmptyReport("Workspace Improvements", inputFiles, message)) - ]); - - return { - implementationTasksPath, - navigationRestructurePath, - formSimplificationTasksPath, - workspaceImprovementsPath, - tasks: [], - navigationTasks: [], - formTasks: [], - inputFiles, - frontendDetected: false - }; - } - - const taskSeeds = findings.length > 0 ? findings : recommendations; - const coveredProblems = new Set(usabilityAnalysis.suggestedTasks.map((task) => canonicalizeUXText(task.problem))); - const inferredTasks = buildTasks(taskSeeds, recommendations, catalog).filter( - (task) => !coveredProblems.has(canonicalizeUXText(task.problem)) - ); - const tasks = dedupeTasks([...usabilityAnalysis.suggestedTasks, ...inferredTasks]); - const navigationTasks = filterNavigationTasks(tasks); - const formTasks = filterFormTasks(tasks); - - if (tasks.length === 0) { - const message = options.emptyMessage ?? "No actionable UX findings were available to convert into frontend tasks."; - await Promise.all([ - writeFileEnsured(implementationTasksPath, renderEmptyReport("UX Implementation Tasks", inputFiles, message)), - writeFileEnsured(navigationRestructurePath, renderEmptyReport("Navigation Restructure", inputFiles, message)), - writeFileEnsured(formSimplificationTasksPath, renderEmptyReport("Form Simplification Tasks", inputFiles, message)), - writeFileEnsured(workspaceImprovementsPath, renderWorkspaceImprovements(tasks, inputFiles, usabilityAnalysis)) - ]); - } else { - await Promise.all([ - writeFileEnsured(implementationTasksPath, renderTaskReportFile(tasks, inputFiles)), - writeFileEnsured(navigationRestructurePath, renderNavigationRestructure(navigationTasks, inputFiles)), - writeFileEnsured(formSimplificationTasksPath, renderFormSimplificationTasks(formTasks, inputFiles)), - writeFileEnsured(workspaceImprovementsPath, renderWorkspaceImprovements(tasks, inputFiles, usabilityAnalysis)) - ]); - } - - return { - implementationTasksPath, - navigationRestructurePath, - formSimplificationTasksPath, - workspaceImprovementsPath, - tasks, - navigationTasks, - formTasks, - inputFiles, - frontendDetected: true - }; -} - -export async function generateUXImplementationTasks(context: ProjectContext): Promise { - const artifacts = await generateUXImprovementArtifacts(context); - return artifacts.tasks.length > 0 ? artifacts.implementationTasksPath : undefined; -} diff --git a/analysis/workspace_discovery/index.ts b/analysis/workspace_discovery/index.ts deleted file mode 100644 index 737bb42..0000000 --- a/analysis/workspace_discovery/index.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { promises as fs } from "node:fs"; -import type { Dirent } from "node:fs"; -import path from "node:path"; - -import { fileExists, relativeTo, toPosixPath, uniqueSorted } from "../../shared/fs-utils"; -import type { RepositoryTarget } from "../../shared/types"; - -const REPO_MARKERS = new Set([ - ".git", - "package.json", - "requirements.txt", - "go.mod", - "pom.xml", - "Cargo.toml", - "Gemfile", - "composer.json" -]); - -const IGNORED_WORKSPACE_DIRECTORIES = new Set([ - ".git", - "node_modules", - "dist", - "coverage", - "build", - ".next", - ".nuxt", - ".turbo", - ".idea", - ".vscode", - "sample-output", - "AI_CONTEXT", - "reports", - "docs", - "memory", - "tasks", - "BRAIN", - "ecosystem" -]); - -async function hasCsprojFile(targetPath: string): Promise { - try { - const entries = await fs.readdir(targetPath); - return entries.some((entry) => entry.endsWith(".csproj")); - } catch { - return false; - } -} - -async function isRepositoryRoot(targetPath: string): Promise { - const markerChecks = await Promise.all( - [...REPO_MARKERS].map(async (marker) => fileExists(path.join(targetPath, marker))) - ); - - if (markerChecks.some(Boolean)) { - return true; - } - - return hasCsprojFile(targetPath); -} - -function outputExclusionName(rootPath: string, outputPath: string): string | undefined { - const relativeOutput = relativeTo(rootPath, outputPath); - - if (!relativeOutput || relativeOutput === "." || relativeOutput.startsWith("../")) { - return undefined; - } - - return relativeOutput.split("/")[0]; -} - -export async function discoverRepositoryTargets( - rootPath: string, - outputPath = rootPath -): Promise<{ - mode: "single" | "workspace"; - repositories: RepositoryTarget[]; -}> { - if (await isRepositoryRoot(rootPath)) { - return { - mode: "single", - repositories: [ - { - repoName: path.basename(rootPath), - targetPath: rootPath, - relativePath: "." - } - ] - }; - } - - let entries: Dirent[] = []; - - try { - entries = await fs.readdir(rootPath, { withFileTypes: true }); - } catch { - return { - mode: "single", - repositories: [ - { - repoName: path.basename(rootPath), - targetPath: rootPath, - relativePath: "." - } - ] - }; - } - - const excludedName = outputExclusionName(rootPath, outputPath); - const repositoryTargets = ( - await Promise.all( - entries - .filter((entry) => entry.isDirectory()) - .filter((entry) => !IGNORED_WORKSPACE_DIRECTORIES.has(entry.name)) - .filter((entry) => entry.name !== excludedName) - .map(async (entry) => { - const targetPath = path.join(rootPath, entry.name); - - if (!(await isRepositoryRoot(targetPath))) { - return undefined; - } - - return { - repoName: entry.name, - targetPath, - relativePath: toPosixPath(entry.name) - } satisfies RepositoryTarget; - }) - ) - ).filter(Boolean) as RepositoryTarget[]; - - if (repositoryTargets.length === 0) { - return { - mode: "single", - repositories: [ - { - repoName: path.basename(rootPath), - targetPath: rootPath, - relativePath: "." - } - ] - }; - } - - if (repositoryTargets.length === 1) { - return { - mode: "workspace", - repositories: repositoryTargets - }; - } - - return { - mode: "workspace", - repositories: repositoryTargets.sort((left, right) => left.repoName.localeCompare(right.repoName)) - }; -} - -export function uniqueRepositoryNames(repositories: RepositoryTarget[]): string[] { - return uniqueSorted(repositories.map((repository) => repository.repoName)); -} diff --git a/bin/brain.mjs b/bin/brain.mjs new file mode 100755 index 0000000..39de3bd --- /dev/null +++ b/bin/brain.mjs @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { runCli } from "../src/cli.mjs"; + +process.exitCode = await runCli(); diff --git a/cli/project-brain.ts b/cli/project-brain.ts deleted file mode 100644 index 43f99bc..0000000 --- a/cli/project-brain.ts +++ /dev/null @@ -1,1456 +0,0 @@ -#!/usr/bin/env node -import { readFileSync } from "node:fs"; -import path from "node:path"; -import { createInterface, type Interface } from "node:readline/promises"; - -import { Command } from "commander"; - -import { AIRouter } from "../core/ai_router/router"; -import { ProjectBrainOrchestrator } from "../core/orchestrator/main"; -import { setLoggerOptions, StructuredLogger } from "../shared/logger"; -import type { - CodebaseMapResult, - ContextTrustLevel, - DoctorSetupItem, - EcosystemAnalysisResult, - EcosystemCodebaseMapResult, - GovernanceTrigger, - LearningOutcome, - OrchestrationResult, - ProjectSeedArchetype, - ProjectSeedInput, - ProjectSeedPriority, - SwarmEngine -} from "../shared/types"; -import { createDefaultTerminalSession, launchTerminalConsole } from "./terminal-console"; - -const program = new Command(); -const orchestrator = new ProjectBrainOrchestrator(); -const aiRouter = new AIRouter(); -const logger = new StructuredLogger("cli"); - -function readPackageVersion(): string { - const candidates = [ - path.resolve(__dirname, "..", "package.json"), - path.resolve(__dirname, "..", "..", "package.json") - ]; - - for (const candidate of candidates) { - try { - const parsed = JSON.parse(readFileSync(candidate, "utf8")) as { version?: unknown }; - if (typeof parsed.version === "string" && parsed.version.trim().length > 0) { - return parsed.version; - } - } catch { - // Try the next layout: source runs from cli/, built runs from dist/cli/. - } - } - - return "0.0.0"; -} - -function commandName(): string { - const invokedName = path.basename(process.argv[1] ?? "project-brain").replace(/\.(?:cjs|js|mjs)$/i, ""); - return invokedName === "project-brain" || invokedName === "brain" ? invokedName : "project-brain"; -} - -function parseTimeoutMs(value: string): number { - const timeoutMs = Number(value); - if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { - throw new Error(`Invalid Ollama timeout: ${value}. Expected a positive integer in milliseconds.`); - } - - return Math.trunc(timeoutMs); -} - -function parsePositiveInteger(value: string, label: string): number { - const numeric = Number(value); - if (!Number.isFinite(numeric) || numeric <= 0) { - throw new Error(`Invalid ${label}: ${value}. Expected a positive integer.`); - } - - return Math.trunc(numeric); -} - -function parseNonNegativeInteger(value: string, label: string): number { - const numeric = Number(value); - if (!Number.isFinite(numeric) || numeric < 0) { - throw new Error(`Invalid ${label}: ${value}. Expected zero or a positive integer.`); - } - - return Math.trunc(numeric); -} - -function parseSwarmEngine(value: string): SwarmEngine { - const normalized = value.trim().toLowerCase(); - if (normalized === "bounded" || normalized === "deepagents") { - return normalized; - } - - throw new Error(`Invalid swarm engine: ${value}. Expected bounded or deepagents.`); -} - -type SwarmPreset = "cheap" | "balanced" | "thorough"; -type ProjectSeedOptions = { - name?: string; - problem?: string; - audience?: string; - type?: string; - template?: string; - stack?: string; - features?: string; - auth?: string; - roles?: string; - data?: string; - integrations?: string; - priority?: string; - language?: string; - notes?: string; - force?: boolean; - yes?: boolean; -}; - -const PROJECT_ARCHETYPE_CHOICES: Array<{ value: ProjectSeedArchetype; label: string }> = [ - { value: "saas-webapp", label: "SaaS / web app" }, - { value: "marketing-site", label: "Marketing site" }, - { value: "mobile-app", label: "Mobile app" }, - { value: "api-backend", label: "API / backend" }, - { value: "internal-tool", label: "Internal tool" }, - { value: "content-platform", label: "Content platform" }, - { value: "custom", label: "Custom" } -]; - -const PROJECT_PRIORITY_CHOICES: Array<{ value: ProjectSeedPriority; label: string }> = [ - { value: "mvp-fast", label: "MVP rapido" }, - { value: "solid-architecture", label: "Arquitectura solida" }, - { value: "low-cost", label: "Costo bajo" }, - { value: "security-first", label: "Seguridad alta" } -]; - -function parseSwarmPreset(value?: string): SwarmPreset | undefined { - if (!value) { - return undefined; - } - const normalized = value.trim().toLowerCase(); - if (normalized === "cheap" || normalized === "balanced" || normalized === "thorough") { - return normalized; - } - - throw new Error(`Invalid swarm preset: ${value}. Expected cheap, balanced, or thorough.`); -} - -function parseCsv(value?: string): string[] { - return (value ?? "") - .split(",") - .map((item) => item.trim()) - .filter(Boolean); -} - -function parseProjectArchetype(value?: string): ProjectSeedArchetype | undefined { - if (!value) { - return undefined; - } - const normalized = value.trim().toLowerCase(); - const match = PROJECT_ARCHETYPE_CHOICES.find((choice) => choice.value === normalized); - if (!match) { - throw new Error(`Invalid project type: ${value}. Expected one of ${PROJECT_ARCHETYPE_CHOICES.map((choice) => choice.value).join(", ")}.`); - } - return match.value; -} - -function parseProjectPriority(value?: string): ProjectSeedPriority | undefined { - if (!value) { - return undefined; - } - const normalized = value.trim().toLowerCase(); - const match = PROJECT_PRIORITY_CHOICES.find((choice) => choice.value === normalized); - if (!match) { - throw new Error(`Invalid project priority: ${value}. Expected one of ${PROJECT_PRIORITY_CHOICES.map((choice) => choice.value).join(", ")}.`); - } - return match.value; -} - -function parseOptionalBoolean(value?: string): boolean | undefined { - if (!value) { - return undefined; - } - const normalized = value.trim().toLowerCase(); - if (["true", "yes", "y", "si", "s", "1"].includes(normalized)) { - return true; - } - if (["false", "no", "n", "0"].includes(normalized)) { - return false; - } - throw new Error(`Invalid boolean value: ${value}. Expected yes or no.`); -} - -async function cliPromptLine(rl: Interface, label: string, defaultValue = ""): Promise { - const prompt = defaultValue.length > 0 ? `${label} [${defaultValue}]: ` : `${label}: `; - const answer = (await rl.question(prompt)).trim(); - return answer.length > 0 ? answer : defaultValue; -} - -async function cliPromptYesNo(rl: Interface, label: string, defaultValue: boolean): Promise { - while (true) { - const answer = (await rl.question(`${label} [${defaultValue ? "Y/n" : "y/N"}]: `)).trim().toLowerCase(); - if (answer.length === 0) { - return defaultValue; - } - if (["y", "yes", "s", "si"].includes(answer)) { - return true; - } - if (["n", "no"].includes(answer)) { - return false; - } - console.log("Responde y/n."); - } -} - -async function cliPromptChoice( - rl: Interface, - label: string, - options: Array<{ value: T; label: string }>, - defaultValue: T -): Promise { - while (true) { - console.log(label); - options.forEach((option, index) => { - const suffix = option.value === defaultValue ? " (default)" : ""; - console.log(` ${index + 1}. ${option.label}${suffix}`); - }); - const answer = (await rl.question("> ")).trim().toLowerCase(); - if (answer.length === 0) { - return defaultValue; - } - const numeric = Number.parseInt(answer, 10); - if (Number.isFinite(numeric) && numeric >= 1 && numeric <= options.length) { - return options[numeric - 1].value; - } - const direct = options.find((option) => option.value === answer); - if (direct) { - return direct.value; - } - console.log("Seleccion invalida."); - } -} - -async function collectProjectSeedInput(targetPath: string, options: ProjectSeedOptions): Promise { - const canPrompt = !options.yes && process.stdin.isTTY && process.stdout.isTTY; - const inferredName = path.basename(targetPath); - const providedArchetype = parseProjectArchetype(options.type ?? options.template); - const providedPriority = parseProjectPriority(options.priority); - const providedAuth = parseOptionalBoolean(options.auth); - - if (!canPrompt) { - return { - projectName: options.name ?? inferredName, - problem: options.problem ?? "Pending problem statement.", - audience: options.audience ?? "Pending audience definition.", - archetype: providedArchetype ?? "custom", - stackPreference: options.stack ?? "", - features: parseCsv(options.features), - authRequired: providedAuth ?? false, - roles: parseCsv(options.roles), - dataEntities: parseCsv(options.data), - integrations: parseCsv(options.integrations), - priority: providedPriority ?? "solid-architecture", - language: options.language ?? "es", - notes: parseCsv(options.notes), - contextOnly: true, - overwrite: Boolean(options.force) - }; - } - - const rl = createInterface({ input: process.stdin, output: process.stdout }); - try { - const projectName = options.name ?? (await cliPromptLine(rl, "Nombre del proyecto", inferredName)); - const problem = options.problem ?? (await cliPromptLine(rl, "Que problema resuelve")); - const audience = options.audience ?? (await cliPromptLine(rl, "Para quien es")); - const archetype = providedArchetype ?? (await cliPromptChoice(rl, "Tipo de proyecto", PROJECT_ARCHETYPE_CHOICES, "saas-webapp")); - const stackPreference = options.stack ?? (await cliPromptLine(rl, "Stack preferido (opcional)", "recomiendame uno")); - const features = options.features - ? parseCsv(options.features) - : parseCsv(await cliPromptLine(rl, "Features iniciales (CSV)", "onboarding,dashboard,admin settings")); - const authRequired = providedAuth ?? (await cliPromptYesNo(rl, "Necesita autenticacion", true)); - const roles = options.roles - ? parseCsv(options.roles) - : authRequired - ? parseCsv(await cliPromptLine(rl, "Roles (CSV)", "owner,admin,member")) - : []; - const dataEntities = options.data - ? parseCsv(options.data) - : parseCsv(await cliPromptLine(rl, "Entidades principales (CSV)", "User,Project,ActivityLog")); - const integrations = options.integrations - ? parseCsv(options.integrations) - : parseCsv(await cliPromptLine(rl, "Integraciones (CSV)", "email,storage,analytics")); - const priority = providedPriority ?? (await cliPromptChoice(rl, "Prioridad", PROJECT_PRIORITY_CHOICES, "solid-architecture")); - const language = options.language ?? (await cliPromptLine(rl, "Idioma", "es")); - const notes = options.notes ? parseCsv(options.notes) : parseCsv(await cliPromptLine(rl, "Notas adicionales (CSV)")); - - return { - projectName, - problem, - audience, - archetype, - stackPreference, - features, - authRequired, - roles, - dataEntities, - integrations, - priority, - language, - notes, - contextOnly: true, - overwrite: Boolean(options.force) - }; - } finally { - rl.close(); - } -} - -function swarmPresetOptions(preset?: SwarmPreset): { - parallelism?: number; - chunkSize?: number; - taskTimeoutMs?: number; - plannerTimeoutMs?: number; - synthesisTimeoutMs?: number; - runTimeoutMs?: number; - maxQueuedTasks?: number; - maxRetries?: number; -} { - switch (preset) { - case "cheap": - return { - parallelism: 2, - chunkSize: 1, - taskTimeoutMs: 90_000, - plannerTimeoutMs: 60_000, - synthesisTimeoutMs: 60_000, - runTimeoutMs: 120_000, - maxQueuedTasks: 4, - maxRetries: 0 - }; - case "balanced": - return { - chunkSize: 1, - taskTimeoutMs: 120_000, - plannerTimeoutMs: 80_000, - synthesisTimeoutMs: 90_000, - runTimeoutMs: 180_000, - maxQueuedTasks: 6, - maxRetries: 1 - }; - case "thorough": - return { - parallelism: 4, - chunkSize: 2, - taskTimeoutMs: 180_000, - plannerTimeoutMs: 120_000, - synthesisTimeoutMs: 120_000, - runTimeoutMs: 360_000, - maxQueuedTasks: 12, - maxRetries: 1 - }; - default: - return {}; - } -} - -function printSuggestions( - suggestions: Array<{ - label: string; - command: string; - rationale: string; - priority: string; - }> -): void { - if (suggestions.length === 0) { - return; - } - - console.log("Suggested actions:"); - for (const suggestion of suggestions) { - console.log(`- [${suggestion.priority.toUpperCase()}] ${suggestion.label}`); - console.log(` Command: ${suggestion.command}`); - console.log(` Why: ${suggestion.rationale}`); - } -} - -function printDoctorSetup(setupItems: DoctorSetupItem[]): void { - if (setupItems.length === 0) { - return; - } - - const groups: Array<{ title: string; items: DoctorSetupItem[] }> = [ - { title: "Required local runtime", items: setupItems.filter((item) => item.tier === "required") }, - { title: "Recommended for this target", items: setupItems.filter((item) => item.tier === "recommended") }, - { title: "Optional open-source expansion", items: setupItems.filter((item) => item.tier === "optional") } - ]; - - console.log("Runtime setup:"); - for (const group of groups) { - if (group.items.length === 0) { - continue; - } - console.log(`- ${group.title}:`); - for (const item of group.items) { - console.log(` - ${item.label}: ${item.status.toUpperCase()} - ${item.summary}`); - console.log(` Install / enable: ${item.installHint}`); - } - } -} - -function resolveTarget(target: string): string { - return path.resolve(process.cwd(), target); -} - -const DEFAULT_OUTPUT_DIR_NAME = "BRAIN"; - -function resolveOutput(targetPath: string, output?: string): string { - return output ? resolveTarget(output) : path.join(targetPath, DEFAULT_OUTPUT_DIR_NAME); -} - -function resolveTrigger(trigger?: string): GovernanceTrigger { - const aliases: Record = { - manual: "manual", - "repository-change": "repository-change", - "weekly-review": "weekly-review", - "security-audit": "security-audit", - "security-advisory": "security-advisory", - "architecture-review": "architecture-review", - "incident-detection": "incident-detection", - "dependency-update": "dependency-update" - }; - - if (trigger && aliases[trigger]) { - return aliases[trigger]; - } - - return "manual"; -} - -function resolveTrustLevel(trust?: string): ContextTrustLevel | undefined { - if (!trust) { - return undefined; - } - - const normalized = trust.trim().toLowerCase(); - if (normalized === "official" || normalized === "maintainer" || normalized === "community") { - return normalized; - } - - throw new Error(`Invalid trust level: ${trust}. Expected official, maintainer, or community.`); -} - -function isEcosystemResult( - result: OrchestrationResult | EcosystemAnalysisResult -): result is EcosystemAnalysisResult { - return "repositories" in result && "knowledgeGraphPath" in result; -} - -function isEcosystemCodebaseMapResult( - result: CodebaseMapResult | EcosystemCodebaseMapResult -): result is EcosystemCodebaseMapResult { - return "repositories" in result && "rootPath" in result && !("context" in result); -} - -program - .name(commandName()) - .description("Analyze repositories, build project context, run specialist agents, and generate reports.") - .version(readPackageVersion()); - -program - .command("console") - .alias("terminal") - .option("--target ", "Initial repository or workspace target", ".") - .option("-o, --output ", "Initial output directory") - .option("--engine ", "Initial swarm engine: bounded or deepagents") - .option("--parallel ", "Initial max parallel workers for swarm") - .option("--chunk-size ", "Initial swarm chunk size") - .option("--task-timeout-ms ", "Initial per-worker timeout budget in milliseconds") - .option("--planner-timeout-ms ", "Initial planner timeout budget in milliseconds") - .option("--synthesis-timeout-ms ", "Initial synthesis timeout budget in milliseconds") - .option("--run-timeout-ms ", "Initial global timeout budget in milliseconds") - .option("--max-queued-tasks ", "Initial cap for queued worker tasks") - .option("--max-retries ", "Initial max retry count for worker chunks") - .option("-t, --trigger ", "Default governance trigger", "manual") - .option("--ollama-timeout ", "Default Ollama inference timeout in milliseconds for console runs") - .option("--verbose", "Enable verbose runtime logs for supported console actions") - .description("Launch an interactive terminal console for configuring and running project-brain workflows.") - .action( - async (options: { - target?: string; - output?: string; - engine?: string; - parallel?: string; - chunkSize?: string; - taskTimeoutMs?: string; - plannerTimeoutMs?: string; - synthesisTimeoutMs?: string; - runTimeoutMs?: string; - maxQueuedTasks?: string; - maxRetries?: string; - trigger?: string; - ollamaTimeout?: string; - verbose?: boolean; - }) => { - const targetPath = resolveTarget(options.target ?? "."); - const outputPath = resolveOutput(targetPath, options.output); - const session = createDefaultTerminalSession(process.cwd()); - session.targetPath = targetPath; - session.outputPath = outputPath; - session.trigger = resolveTrigger(options.trigger); - session.verbose = Boolean(options.verbose); - session.swarmEngine = options.engine ? parseSwarmEngine(options.engine) : session.swarmEngine; - session.parallelism = options.parallel - ? parsePositiveInteger(options.parallel, "parallel worker count") - : session.parallelism; - session.chunkSize = options.chunkSize ? parsePositiveInteger(options.chunkSize, "chunk size") : session.chunkSize; - session.taskTimeoutMs = options.taskTimeoutMs - ? parsePositiveInteger(options.taskTimeoutMs, "task timeout") - : session.taskTimeoutMs; - session.plannerTimeoutMs = options.plannerTimeoutMs - ? parsePositiveInteger(options.plannerTimeoutMs, "planner timeout") - : session.plannerTimeoutMs; - session.synthesisTimeoutMs = options.synthesisTimeoutMs - ? parsePositiveInteger(options.synthesisTimeoutMs, "synthesis timeout") - : session.synthesisTimeoutMs; - session.runTimeoutMs = options.runTimeoutMs - ? parsePositiveInteger(options.runTimeoutMs, "run timeout") - : session.runTimeoutMs; - session.maxQueuedTasks = options.maxQueuedTasks - ? parsePositiveInteger(options.maxQueuedTasks, "max queued tasks") - : session.maxQueuedTasks; - session.maxRetries = options.maxRetries ? parseNonNegativeInteger(options.maxRetries, "max retries") : session.maxRetries; - session.ollamaTimeoutMs = options.ollamaTimeout - ? parsePositiveInteger(options.ollamaTimeout, "ollama timeout") - : session.ollamaTimeoutMs; - - await launchTerminalConsole({ - orchestrator, - aiRouter, - initialSession: session - }); - } - ); - -program - .command("models") - .description("Show available local models and configured cloud model routing.") - .action(async () => { - const inventory = await aiRouter.listModels(); - console.log("Ollama models available:"); - if (inventory.availableModels.length === 0) { - console.log("- None detected via Ollama"); - } else { - for (const model of inventory.availableModels) { - console.log(`- ${model.name} (${model.residency}, offline=${model.offlineCapable ? "yes" : "no"})`); - } - } - console.log(`Configured local model: ${inventory.config.localModel}`); - console.log(`Configured fallback model: ${inventory.config.fallbackModel}`); - console.log(`Configured reasoning model: ${inventory.config.reasoningModel}`); - console.log("Model profiles:"); - console.log(`- worker: ${inventory.resolvedProfiles.worker}`); - console.log(`- reviewer: ${inventory.resolvedProfiles.reviewer}`); - console.log(`- reasoning: ${inventory.resolvedProfiles.reasoning}`); - console.log(`- planner: ${inventory.resolvedProfiles.planner}`); - console.log(`- synthesizer: ${inventory.resolvedProfiles.synthesizer}`); - console.log("Cloud model configured:"); - console.log(`- provider: ${inventory.cloudConfigured.provider}`); - console.log(`- model: ${inventory.cloudConfigured.model}`); - console.log("Routing rules:"); - for (const [task, route] of Object.entries(inventory.routing)) { - console.log(`- ${task}: ${route}`); - } - console.log("Task profiles:"); - for (const [task, profile] of Object.entries(inventory.taskProfiles)) { - console.log(`- ${task}: ${profile}`); - } - console.log(`Offline mode: ${inventory.offlineMode ? "yes" : "no"}`); - console.log(`Remote Ollama allowed: ${inventory.remoteOllamaAllowed ? "yes" : "no"}`); - console.log(`Offline ready: ${inventory.offlineReady ? "yes" : "no"}`); - }); - -program - .command("doctor") - .argument("[target]", "Repository target to validate", ".") - .option("-o, --output ", "Output directory") - .description("Run install, environment, model, and swarm readiness checks.") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.doctor(targetPath, outputPath); - console.log(`Doctor report: ${result.reportPath}`); - console.log(`Doctor memory: ${result.memoryPath}`); - console.log( - `Summary: passed=${result.summary.passed}, warnings=${result.summary.warnings}, failed=${result.summary.failed}` - ); - console.log(`Headline: ${result.summary.headline}`); - for (const check of result.checks) { - console.log(`- ${check.label}: ${check.status.toUpperCase()} - ${check.summary}`); - } - printDoctorSetup(result.setupItems); - printSuggestions(result.suggestions); - }); - -program - .command("security-audit") - .argument("[target]", "Repository or workspace target to audit", ".") - .option("-o, --output ", "Output directory") - .option("-t, --trigger ", "Governance trigger", "security-audit") - .option("--verbose", "Print structured runtime logs") - .description("Run a structured multi-agent security audit with verified context and evidence-based findings.") - .action(async (target: string, options: { output?: string; trigger?: string; verbose?: boolean }) => { - setLoggerOptions({ verbose: Boolean(options.verbose) }); - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.securityAudit(targetPath, outputPath, resolveTrigger(options.trigger)); - const counts = result.findings.reduce>((accumulator, finding) => { - accumulator[finding.severity] = (accumulator[finding.severity] ?? 0) + 1; - return accumulator; - }, {}); - - console.log(`Security audit report: ${result.reportPath}`); - console.log(`Security audit memory: ${result.memoryPath}`); - if (result.contextLiteReportPath) { - console.log(`Context-lite report: ${result.contextLiteReportPath}`); - } - console.log(`Headline: ${result.headline}`); - console.log(`Verdict: ${result.verdict}`); - console.log( - `Findings: critical=${counts.critical ?? 0}, high=${counts.high ?? 0}, medium=${counts.medium ?? 0}, low=${counts.low ?? 0}, info=${counts.info ?? 0}` - ); - console.log(`Coverage gaps: ${result.coverage.filter((entry) => entry.status === "not-reviewed").length}`); - }); - -program - .command("status") - .argument("[target]", "Repository target to summarize", ".") - .option("-o, --output ", "Output directory") - .description("Show repository operational status, recent artifacts, and health signals.") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.status(targetPath, outputPath); - const present = result.artifacts.filter((artifact) => artifact.exists).map((artifact) => artifact.label); - const missing = result.artifacts.filter((artifact) => !artifact.exists).map((artifact) => artifact.label); - console.log(`Status report: ${result.reportPath}`); - console.log(`Status memory: ${result.memoryPath}`); - console.log(`Git: repo=${result.git.isGitRepo ? "yes" : "no"}, branch=${result.git.branch ?? "unknown"}`); - console.log(`Headline: ${result.summary.headline}`); - console.log(`Memory: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - console.log(`Ready: ${present.join(", ") || "None"}`); - console.log(`Missing: ${missing.slice(0, 6).join(", ") || "None"}${missing.length > 6 ? ", plus more" : ""}`); - printSuggestions(result.suggestions); - }); - -program - .command("resume") - .argument("[target]", "Repository target to resume from", ".") - .option("-o, --output ", "Output directory") - .description("Recover the latest useful project-brain checkpoint and suggest the next step.") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.resume(targetPath, outputPath); - console.log(`Resume report: ${result.reportPath}`); - console.log(`Resume memory: ${result.memoryPath}`); - console.log(`Executive summary: ${result.executiveSummary.reportPath}`); - console.log(`Git: repo=${result.git.isGitRepo ? "yes" : "no"}, branch=${result.git.branch ?? "unknown"}`); - console.log(`Stage: ${result.summary.stage}`); - console.log(`Headline: ${result.summary.headline}`); - console.log(`Memory: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - if (result.latestArtifact) { - console.log( - `Latest artifact: ${result.latestArtifact.label}${result.latestArtifact.updatedAt ? ` (${result.latestArtifact.updatedAt})` : ""}` - ); - } - for (const note of result.notes) { - console.log(`- ${note}`); - } - printSuggestions(result.suggestions); - }); - -program - .command("start") - .alias("go") - .argument("[intent]", "Plain-language goal", "optimize analysis and cost") - .argument("[target]", "Repository target", ".") - .option("-o, --output ", "Output directory") - .option("--with-swarm", "Also run the model-heavy bounded swarm after cheap preflight") - .description("Run the simple guided path: cheap memory, facts, runbook, harness audit, firewall, then suggest next step.") - .action(async (intent: string, target: string, options: { output?: string; withSwarm?: boolean }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.start(targetPath, outputPath, intent, { - withSwarm: Boolean(options.withSwarm) - }); - console.log(`Start report: ${result.reportPath}`); - console.log(`Start memory: ${result.memoryPath}`); - console.log(`Executive summary: ${result.executiveSummary.reportPath}`); - console.log(`Headline: ${result.headline}`); - console.log(`Memory: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - for (const step of result.executedSteps) { - console.log(`- [${step.status}] ${step.label}: ${step.summary}`); - } - if (result.nextCommand) { - console.log(`Next: ${result.nextCommand}`); - } - }); - -program - .command("new") - .alias("scaffold-context") - .argument("", "Directory for the new project context") - .option("--name ", "Project name") - .option("--problem ", "Problem the project solves") - .option("--audience ", "Target audience") - .option("--type ", "Project archetype") - .option("--template ", "Alias for --type") - .option("--stack ", "Preferred stack or 'recomiendame uno'") - .option("--features ", "Initial feature list") - .option("--auth ", "Whether authentication is required") - .option("--roles ", "Expected roles") - .option("--data ", "Primary data entities") - .option("--integrations ", "External integrations") - .option("--priority ", "mvp-fast, solid-architecture, low-cost, or security-first") - .option("--language ", "Project language", "es") - .option("--notes ", "Additional notes") - .option("--force", "Overwrite existing generated project seed artifacts") - .option("--yes", "Use provided values and defaults without interactive questions") - .description("Create a new project context with guided AI_CONTEXT, architecture, memory, and initial backlog artifacts.") - .action(async (target: string, options: ProjectSeedOptions) => { - const targetPath = resolveTarget(target); - const input = await collectProjectSeedInput(targetPath, options); - const result = await orchestrator.scaffoldProject(targetPath, input); - - console.log(`Project seed: ${result.projectName}`); - console.log(`Target: ${result.targetPath}`); - console.log(`Archetype: ${result.archetype}`); - console.log(`Context only: ${result.contextOnly ? "yes" : "no"}`); - console.log(`Charter: ${result.artifactPaths.projectCharterPath}`); - console.log(`Requirements: ${result.artifactPaths.requirementsPath}`); - console.log(`Blueprint: ${result.artifactPaths.blueprintPath}`); - console.log(`Memory brief: ${result.artifactPaths.memoryBriefPath}`); - console.log(`Backlog: ${result.artifactPaths.backlogPath}`); - console.log(`CLAUDE: ${result.artifactPaths.claudePath}`); - console.log(`Next: ${result.nextSteps.join(" | ")}`); - }); - -program - .command("init") - .argument("[target]", "Repository to initialize", ".") - .option("-o, --output ", "Output directory") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const context = await orchestrator.initTarget(targetPath, outputPath); - console.log(`Initialized project memory for ${context.repoName} at ${context.memoryDir}`); - }); - -program - .command("map-codebase") - .alias("map") - .argument("[target]", "Repository or workspace to map", ".") - .option("-o, --output ", "Output directory") - .option("--verbose", "Print structured runtime logs") - .action(async (target: string, options: { output?: string; verbose?: boolean }) => { - setLoggerOptions({ verbose: Boolean(options.verbose) }); - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - logger.info("CLI map-codebase invoked", { - component: "cli", - action: "command_start", - command: "map-codebase", - targetPath, - outputPath - }); - - const result = await orchestrator.mapScope(targetPath, outputPath); - - if (isEcosystemCodebaseMapResult(result)) { - logger.info("CLI map-codebase completed", { - component: "cli", - action: "command_complete", - command: "map-codebase", - repositories: result.repositories.map((repository) => repository.repoName) - }); - console.log(`Mapped workspace at ${result.rootPath}`); - console.log(`Repositories: ${result.repositories.map((repository) => repository.repoName).join(", ")}`); - console.log(`Summary: ${result.summaryPath}`); - return; - } - - logger.info("CLI map-codebase completed", { - component: "cli", - action: "command_complete", - command: "map-codebase", - repoName: result.context.repoName, - codebaseMapDir: result.codebaseMapDir - }); - console.log(`Mapped ${result.context.repoName}`); - console.log(`Codebase map: ${result.codebaseMapDir}`); - console.log(`Summary: ${result.summaryPath}`); - console.log(`Documents: ${result.files.map((filePath) => path.basename(filePath)).join(", ")}`); - }); - -program - .command("context-lite") - .argument("[target]", "Repository target to materialize lightweight AI context for", ".") - .option("-o, --output ", "Output directory") - .description("Generate a compact AI_CONTEXT pack for smaller apps without running the full project-brain pipeline.") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.contextLite(targetPath, outputPath); - console.log(`Context-lite report: ${result.reportPath}`); - console.log(`AI_CONTEXT: ${result.context.memoryDir}`); - console.log(`Artifacts: ${result.artifactPaths.map((artifactPath) => path.basename(artifactPath)).join(", ")}`); - console.log("Summary:"); - for (const line of result.summary) { - console.log(`- ${line}`); - } - console.log("Requires confirmation:"); - for (const item of result.openQuestions) { - console.log(`- ${item}`); - } - }); - -program - .command("fact-query") - .alias("fq") - .argument("", "Query over MEMORY_BRIEF and repository_fact_graph") - .argument("[target]", "Repository target", ".") - .option("-o, --output ", "Output directory") - .description("Query compact factual memory without calling an AI model.") - .action(async (query: string, target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.factQuery(targetPath, outputPath, query); - console.log(`Fact query report: ${result.reportPath}`); - console.log(`Fact query memory: ${result.memoryPath}`); - console.log(`Answer: ${result.answer}`); - console.log(`Memory matches: ${result.memoryMatches.length}`); - console.log(`Node matches: ${result.nodeMatches.length}`); - console.log(`Edge matches: ${result.edgeMatches.length}`); - console.log(`Evidence refs: ${result.evidenceRefs.join(", ") || "None"}`); - if (result.unknowns.length > 0) { - console.log(`Unknowns: ${result.unknowns.join(" | ")}`); - } - }); - -program - .command("harness-audit") - .alias("ha") - .argument("[target]", "Repository target", ".") - .option("-o, --output ", "Output directory") - .description("Audit progressive memory, cost gates, and continuity before model-heavy analysis.") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.harnessAudit(targetPath, outputPath); - console.log(`Harness audit report: ${result.reportPath}`); - console.log(`Harness audit memory: ${result.memoryPath}`); - console.log(`Score: ${result.score}`); - console.log(`Token risk: ${result.tokenRisk}`); - console.log(`Memory: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - for (const item of result.checks) { - console.log(`- [${item.status}] ${item.label}: ${item.summary}`); - } - if (result.suggestedCommands.length > 0) { - console.log("Suggested commands:"); - for (const command of result.suggestedCommands) { - console.log(`- ${command}`); - } - } - }); - -program - .command("runbook") - .argument("", "Goal to organize into a token-aware project-brain runbook") - .argument("[target]", "Repository target", ".") - .option("-o, --output ", "Output directory") - .description("Create a deterministic, token-aware runbook before expensive model analysis.") - .action(async (intent: string, target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.runbook(targetPath, outputPath, intent); - console.log(`Runbook report: ${result.reportPath}`); - console.log(`Runbook memory: ${result.memoryPath}`); - console.log(`Executive summary: ${result.executiveSummary.reportPath}`); - for (const item of result.steps) { - console.log(`- [${item.status}] ${item.id}. ${item.title}: ${item.command}`); - } - }); - -program - .command("analyze") - .argument("", "Repository to analyze") - .option("-o, --output ", "Output directory") - .option("-t, --trigger ", "Governance trigger") - .option("--ollama-timeout ", "Override Ollama inference timeout in milliseconds") - .option("--verbose", "Print structured runtime logs") - .action(async (target: string, options: { output?: string; trigger?: string; ollamaTimeout?: string; verbose?: boolean }) => { - setLoggerOptions({ verbose: Boolean(options.verbose) }); - if (options.ollamaTimeout) { - process.env.OLLAMA_TIMEOUT_MS = String(parseTimeoutMs(options.ollamaTimeout)); - } - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - logger.info("CLI analyze invoked", { - component: "cli", - action: "command_start", - command: "analyze", - targetPath, - outputPath, - trigger: resolveTrigger(options.trigger), - ollamaTimeoutMs: process.env.OLLAMA_TIMEOUT_MS ? Number(process.env.OLLAMA_TIMEOUT_MS) : undefined - }); - const result = await orchestrator.analyzeScope(targetPath, outputPath, resolveTrigger(options.trigger)); - - if (isEcosystemResult(result)) { - logger.info("CLI analyze completed", { - component: "cli", - action: "command_complete", - command: "analyze", - repositories: result.repositories.map((repository) => repository.repoName) - }); - console.log(`Analyzed ecosystem at ${result.rootPath}`); - console.log(`Repositories: ${result.repositories.map((repository) => repository.repoName).join(", ")}`); - console.log(`Knowledge graph: ${result.knowledgeGraphPath}`); - console.log(`Ecosystem report: ${result.ecosystemReportPath}`); - console.log(`Runtime observability: ${result.runtimeObservabilityPath}`); - console.log(`Telemetry: ${result.telemetryPath}`); - return; - } - - logger.info("CLI analyze completed", { - component: "cli", - action: "command_complete", - command: "analyze", - repoName: result.context.repoName - }); - console.log(`Analyzed ${result.context.repoName}`); - console.log(`AI_CONTEXT: ${result.context.memoryDir}`); - console.log(`Reports: ${result.context.reportsDir}`); - if (result.reportQualityPath) { - console.log(`Report quality: ${result.reportQualityPath}`); - } - console.log(`Docs: ${result.context.docsDir}`); - console.log(`Tasks: ${result.context.taskBoardDir}`); - console.log(`Learnings: ${result.context.learningDir}`); - console.log(`Proposals: ${result.context.proposalDir}`); - }); - -program - .command("agents") - .argument("", "Repository to evaluate with specialist agents") - .option("-o, --output ", "Output directory") - .option("-t, --trigger ", "Governance trigger") - .option("--verbose", "Print structured runtime logs") - .action(async (target: string, options: { output?: string; trigger?: string; verbose?: boolean }) => { - setLoggerOptions({ verbose: Boolean(options.verbose) }); - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const reports = await orchestrator.runAgents(targetPath, outputPath, resolveTrigger(options.trigger)); - console.log(`Ran ${reports.length} agents for ${targetPath}`); - }); - -program - .command("weekly") - .argument("", "Repository to generate weekly artifacts for") - .option("-o, --output ", "Output directory") - .option("--verbose", "Print structured runtime logs") - .action(async (target: string, options: { output?: string; verbose?: boolean }) => { - setLoggerOptions({ verbose: Boolean(options.verbose) }); - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - logger.info("CLI weekly invoked", { - component: "cli", - action: "command_start", - command: "weekly", - targetPath, - outputPath - }); - const result = await orchestrator.generateWeeklyScope(targetPath, outputPath); - - if (isEcosystemResult(result)) { - logger.info("CLI weekly completed", { - component: "cli", - action: "command_complete", - command: "weekly", - repositories: result.repositories.map((repository) => repository.repoName) - }); - console.log(`Weekly ecosystem reports generated for ${result.repositories.length} repositories`); - console.log(`Ecosystem report: ${result.ecosystemReportPath}`); - console.log(`Knowledge graph: ${result.knowledgeGraphPath}`); - return; - } - - logger.info("CLI weekly completed", { - component: "cli", - action: "command_complete", - command: "weekly", - repoName: result.context.repoName - }); - console.log(`Weekly reports generated for ${result.context.repoName}`); - console.log(`Weekly report: ${result.weeklyReportPath}`); - console.log(`Risk report: ${result.riskReportPath}`); - if (result.reportQualityPath) { - console.log(`Report quality: ${result.reportQualityPath}`); - } - }); - -program - .command("code-graph") - .alias("graph") - .argument("[target]", "Repository to index into code-graph-v2", ".") - .option("-o, --output ", "Output directory") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.buildCodeGraph(targetPath, outputPath); - console.log(`Code graph: ${result.graphPath}`); - if (result.factGraphPath) { - console.log(`Repository fact graph: ${result.factGraphPath}`); - } - if (result.factReportPath) { - console.log(`Repository fact report: ${result.factReportPath}`); - } - console.log(`Build mode: ${result.graph.build.mode}`); - console.log(`Files: ${result.graph.stats.files}`); - console.log(`Symbols: ${result.graph.stats.symbols}`); - console.log(`Nodes: ${result.graph.stats.nodes}`); - console.log(`Edges: ${result.graph.stats.edges}`); - if (result.factGraph) { - console.log(`Fact graph nodes: ${result.factGraph.stats.nodes}`); - console.log(`Fact graph edges: ${result.factGraph.stats.edges}`); - } - console.log(`Updated files: ${result.graph.build.updatedFiles.join(", ") || "None"}`); - }); - -program - .command("impact-radius") - .alias("impact") - .argument("[target]", "Repository to analyze for blast radius", ".") - .option("-o, --output ", "Output directory") - .option("--files ", "Comma-separated repository-relative files to analyze") - .option("--base ", "Base git ref for changed files") - .option("--head ", "Head git ref for changed files") - .action(async ( - target: string, - options: { output?: string; files?: string; base?: string; head?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.analyzeImpact(targetPath, outputPath, { - files: options.files?.split(",").map((filePath) => filePath.trim()).filter(Boolean), - baseRef: options.base, - headRef: options.head - }); - console.log(`Impact report: ${result.reportPath}`); - console.log(`Graph: ${result.graphPath}`); - console.log(`Changed files: ${result.changedFiles.join(", ") || "None"}`); - console.log(`Review set: ${result.reviewFiles.join(", ") || "None"}`); - }); - -program - .command("review-delta") - .argument("[target]", "Repository to review from git diff", ".") - .option("-o, --output ", "Output directory") - .option("--base ", "Base git ref", "HEAD~1") - .option("--head ", "Head git ref", "HEAD") - .action(async ( - target: string, - options: { output?: string; base?: string; head?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.reviewDelta(targetPath, outputPath, { - baseRef: options.base, - headRef: options.head - }); - console.log(`Impact report: ${result.reportPath}`); - console.log(`Changed files: ${result.changedFiles.join(", ") || "None"}`); - console.log(`Review set: ${result.reviewFiles.join(", ") || "None"}`); - console.log(`Related tests: ${result.impactedTests.join(", ") || "None"}`); - }); - -program - .command("ask") - .argument("", "Plain-language request such as \"identifica este proyecto\"") - .argument("[target]", "Repository or workspace target", ".") - .option("-o, --output ", "Output directory") - .action(async ( - intent: string, - target: string, - options: { output?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.ask(targetPath, outputPath, intent); - console.log(`Workflow: ${result.workflow}`); - console.log(`Brief: ${result.briefPath}`); - console.log(`Headline: ${result.headline}`); - console.log(`Reason: ${result.routingReason}`); - console.log(`Artifacts: ${result.artifacts.map((artifact) => `${artifact.label}=${artifact.path}`).join(" | ") || "None"}`); - if (result.guidedExecution) { - console.log(`Guided: ${result.guidedExecution.label} -> ${result.guidedExecution.command}`); - } - if (result.aiAssistance) { - console.log( - `AI assist: ${result.aiAssistance.model} (${result.aiAssistance.provider}, ${result.aiAssistance.residency}, profile=${result.aiAssistance.profile})` - ); - } - console.log(`Next: ${result.followUps.join(" | ")}`); - }); - -program - .command("swarm") - .argument("", "Delegated analysis request such as \"ayudame a mejorar este repo\"") - .argument("[target]", "Repository target", ".") - .option("-o, --output ", "Output directory") - .option("--engine ", "Swarm engine: bounded or deepagents", "bounded") - .option("--preset ", "Execution preset: cheap, balanced, or thorough") - .option("--parallel ", "Maximum parallel workers for the swarm") - .option("--chunk-size ", "How many top-level areas each worker should inspect at once") - .option("--task-timeout-ms ", "Per-worker timeout budget in milliseconds") - .option("--planner-timeout-ms ", "Planner timeout budget in milliseconds") - .option("--synthesis-timeout-ms ", "Synthesis timeout budget in milliseconds") - .option("--run-timeout-ms ", "Global timeout budget for the whole swarm run") - .option("--max-queued-tasks ", "Hard cap for how many chunked worker tasks can be queued") - .option("--max-retries ", "How many retries to allow before a worker chunk is marked failed") - .action(async ( - intent: string, - target: string, - options: { - output?: string; - engine?: string; - preset?: string; - parallel?: string; - chunkSize?: string; - taskTimeoutMs?: string; - plannerTimeoutMs?: string; - synthesisTimeoutMs?: string; - runTimeoutMs?: string; - maxQueuedTasks?: string; - maxRetries?: string; - } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const preset = parseSwarmPreset(options.preset); - const presetOptions = swarmPresetOptions(preset); - const result = await orchestrator.swarm(targetPath, outputPath, intent, { - engine: options.engine ? parseSwarmEngine(options.engine) : undefined, - preset, - parallelism: options.parallel ? parsePositiveInteger(options.parallel, "parallel worker count") : presetOptions.parallelism, - chunkSize: options.chunkSize ? parsePositiveInteger(options.chunkSize, "chunk size") : presetOptions.chunkSize, - taskTimeoutMs: options.taskTimeoutMs ? parsePositiveInteger(options.taskTimeoutMs, "task timeout") : presetOptions.taskTimeoutMs, - plannerTimeoutMs: options.plannerTimeoutMs ? parsePositiveInteger(options.plannerTimeoutMs, "planner timeout") : presetOptions.plannerTimeoutMs, - synthesisTimeoutMs: options.synthesisTimeoutMs ? parsePositiveInteger(options.synthesisTimeoutMs, "synthesis timeout") : presetOptions.synthesisTimeoutMs, - runTimeoutMs: options.runTimeoutMs ? parsePositiveInteger(options.runTimeoutMs, "run timeout") : presetOptions.runTimeoutMs, - maxQueuedTasks: options.maxQueuedTasks ? parsePositiveInteger(options.maxQueuedTasks, "max queued tasks") : presetOptions.maxQueuedTasks, - maxRetries: options.maxRetries ? parseNonNegativeInteger(options.maxRetries, "max retries") : presetOptions.maxRetries - }); - console.log(`Engine: ${result.engine}`); - console.log(`Swarm report: ${result.reportPath}`); - console.log(`Swarm memory: ${result.memoryPath}`); - console.log(`Planner: ${result.planner.model} (${result.planner.provider}, ${result.planner.residency})`); - console.log( - `Chunking: size=${result.chunking.selectedChunkSize}, strategy=${result.chunking.queueStrategy}, scopeBias=${result.chunking.scopeBias}, scopeChunks=${result.chunking.scopeChunks}, queuedTasks=${result.chunking.queuedTasks}` - ); - console.log( - `Resilience: localBudgetMode=${result.resilience.localBudgetMode}, adaptiveQueueBudget=${result.resilience.adaptiveQueueBudget}, runTimeoutMs=${result.resilience.runTimeoutMs}, plannerTimeoutMs=${result.resilience.plannerTimeoutMs}, synthesisTimeoutMs=${result.resilience.synthesisTimeoutMs}, taskTimeoutMs=${result.resilience.taskTimeoutMs}, queueBudget=${result.resilience.queueBudget}, maxRetries=${result.resilience.maxRetries}, timedOut=${result.resilience.timedOutTasks}, retried=${result.resilience.retriedTasks}, failed=${result.resilience.failedTasks}, dropped=${result.resilience.droppedTasks}` - ); - console.log( - `Parallelism: ${result.parallelism.selected} workers (cpu=${result.parallelism.cpuCount}, load1m=${result.parallelism.loadAverage1m}, freeMemMb=${result.parallelism.freeMemoryMb}, pressure=${result.parallelism.pressure})` - ); - console.log(`Tasks: ${result.tasks.map((task) => `${task.title}[${task.profile}]`).join(" | ") || "None"}`); - console.log(`Synthesis: ${result.synthesis.headline}`); - }); - -program - .command("self-improve") - .argument("[target]", "Repository target to improve with the bounded swarm", ".") - .option("-o, --output ", "Output directory") - .option("--intent ", "Override the default self-improvement intent") - .action(async ( - target: string, - options: { - output?: string; - intent?: string; - } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.selfImprove(targetPath, outputPath, options.intent); - const planPath = path.join(outputPath, "docs", "improvement_plan", "SUMMARY.md"); - console.log(`Self-improve report: ${result.reportPath}`); - console.log(`Improvement plan: ${planPath}`); - console.log(`Swarm memory: ${result.memoryPath}`); - console.log(`Planner: ${result.planner.model} (${result.planner.provider}, ${result.planner.residency})`); - console.log( - `Chunking: size=${result.chunking.selectedChunkSize}, strategy=${result.chunking.queueStrategy}, scopeChunks=${result.chunking.scopeChunks}, queuedTasks=${result.chunking.queuedTasks}` - ); - console.log(`Scope bias: ${result.chunking.scopeBias}`); - console.log( - `Resilience: localBudgetMode=${result.resilience.localBudgetMode}, adaptiveQueueBudget=${result.resilience.adaptiveQueueBudget}, runTimeoutMs=${result.resilience.runTimeoutMs}, plannerTimeoutMs=${result.resilience.plannerTimeoutMs}, synthesisTimeoutMs=${result.resilience.synthesisTimeoutMs}, taskTimeoutMs=${result.resilience.taskTimeoutMs}, queueBudget=${result.resilience.queueBudget}, maxRetries=${result.resilience.maxRetries}, timedOut=${result.resilience.timedOutTasks}, retried=${result.resilience.retriedTasks}, failed=${result.resilience.failedTasks}, dropped=${result.resilience.droppedTasks}` - ); - console.log( - `Parallelism: ${result.parallelism.selected} workers (cpu=${result.parallelism.cpuCount}, load1m=${result.parallelism.loadAverage1m}, freeMemMb=${result.parallelism.freeMemoryMb}, pressure=${result.parallelism.pressure})` - ); - console.log(`Tasks: ${result.tasks.map((task) => `${task.title}[${task.profile}]`).join(" | ") || "None"}`); - console.log(`Synthesis: ${result.synthesis.headline}`); - }); - -program - .command("context-search") - .argument("", "Context query such as \"express observability\"") - .argument("[target]", "Repository that owns the output context", ".") - .option("-o, --output ", "Output directory") - .option("--trust ", "Trust filter: official, maintainer, or community") - .action(async ( - query: string, - target: string, - options: { output?: string; trust?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.contextSearch(targetPath, outputPath, query, resolveTrustLevel(options.trust)); - console.log(`Context search report: ${result.reportPath}`); - console.log(`Cache: ${result.cachePath}`); - console.log( - `Hits: ${result.hits.map((hit) => `${hit.entry.id}(${hit.entry.trustLevel}, score=${hit.score})`).join(" | ") || "None"}` - ); - }); - -program - .command("context-get") - .argument("", "Context entry id") - .argument("[target]", "Repository that owns the output context", ".") - .option("-o, --output ", "Output directory") - .action(async ( - id: string, - target: string, - options: { output?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.contextGet(targetPath, outputPath, id); - console.log(`Context artifact: ${result.artifactPath}`); - console.log(`Cache: ${result.cachePath}`); - console.log(`Title: ${result.entry.title}`); - console.log(`Trust: ${result.entry.trustLevel}`); - }); - -program - .command("context-sources") - .argument("[target]", "Repository that owns the output context", ".") - .option("-o, --output ", "Output directory") - .action(async ( - target: string, - options: { output?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.contextSources(targetPath, outputPath); - console.log(`Context sources report: ${result.reportPath}`); - console.log( - `Sources: ${result.sources.map((source) => `${source.source}(${source.trustLevel}, entries=${source.entries})`).join(" | ") || "None"}` - ); - }); - -program - .command("ecosystem-radar") - .argument("[target]", "Repository that owns the output context", ".") - .option("-o, --output ", "Output directory") - .option("--limit ", "Maximum additional discovered repositories to materialize", "6") - .option("--bucket ", "Only run a specific radar bucket") - .option("--seed-only", "Refresh only the curated seed repositories") - .action(async ( - target: string, - options: { output?: string; limit?: string; bucket?: string; seedOnly?: boolean } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const parsedLimit = Number.parseInt(options.limit ?? "6", 10); - const result = await orchestrator.ecosystemRadar(targetPath, outputPath, { - limit: Number.isFinite(parsedLimit) ? parsedLimit : 6, - bucketId: options.bucket, - seedOnly: options.seedOnly ?? false - }); - console.log(`Ecosystem radar report: ${result.reportPath}`); - console.log(`Cache: ${result.cachePath}`); - console.log( - `Candidates: ${result.candidates.map((candidate) => `${candidate.repoFullName}(score=${candidate.score})`).join(" | ") || "None"}` - ); - }); - -program - .command("architecture-plan") - .argument("[target]", "Repository to generate architecture evidence plan", ".") - .option("-o, --output ", "Output directory") - .description("Generate architecture evidence artifacts and temporary execution context.") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.architecturePlan(targetPath, outputPath); - console.log(`Architecture plan: ${result.planDir}`); - console.log(`Blueprint: ${result.blueprintPath}`); - console.log(`State: ${result.statePath}`); - console.log(`Claude context: ${result.claudeContextPath}`); - console.log(`Memory: ${result.memoryPath}`); - }); - -program - .command("plan-improvements") - .argument("[target]", "Repository to turn into a persistent improvement plan", ".") - .option("-o, --output ", "Output directory") - .option("-t, --trigger ", "Governance trigger") - .action(async ( - target: string, - options: { output?: string; trigger?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.planImprovements(targetPath, outputPath, resolveTrigger(options.trigger)); - console.log(`Improvement plan: ${result.planDir}`); - console.log(`Summary: ${result.summaryPath}`); - console.log(`State: ${result.statePath}`); - console.log(`Known risks: ${result.risksPath}`); - console.log(`Roadmap: ${result.roadmapPath}`); - console.log(`Tracks: ${result.tracksPath}`); - }); - -program - .command("firewall") - .argument("[target]", "Repository to assess with the agent firewall", ".") - .option("-o, --output ", "Output directory") - .option("-t, --trigger ", "Governance trigger") - .action(async ( - target: string, - options: { output?: string; trigger?: string } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const result = await orchestrator.inspectFirewall(targetPath, outputPath, resolveTrigger(options.trigger)); - console.log(`Firewall report: ${result.firewall.reportPath}`); - console.log(`Firewall policy: ${result.firewall.policyPath}`); - console.log(`Task packets: ${result.firewall.packets.length}`); - console.log(`Allowed: ${result.firewall.stats.allowed}`); - console.log(`Review required: ${result.firewall.stats.reviewRequired}`); - console.log(`Blocked: ${result.firewall.stats.blocked}`); - }); - -program - .command("report") - .argument("[target]", "Directory containing generated output", ".") - .option("-o, --output ", "Output directory") - .action(async (target: string, options: { output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = options.output ? resolveTarget(options.output) : targetPath; - const manifest = await orchestrator.collectReportManifest(outputPath); - console.log(JSON.stringify(manifest, null, 2)); - }); - -program - .command("annotate") - .argument("", "Repository that owns the generated context") - .argument("[note]", "Persistent local note to save for future runs") - .option("--scope ", "Annotation scope", "repo") - .option("--list", "List all annotations") - .option("--clear", "Clear the annotation for the selected scope") - .option("-o, --output ", "Output directory") - .action(async (target: string, note: string | undefined, options: { scope?: string; list?: boolean; clear?: boolean; output?: string }) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const scope = options.scope ?? "repo"; - - if (options.list) { - const annotations = await orchestrator.listAnnotations(targetPath, outputPath); - if (annotations.length === 0) { - console.log("No annotations recorded."); - return; - } - - for (const annotation of annotations) { - console.log(`[${annotation.scope}] ${annotation.updatedAt}`); - console.log(annotation.note); - console.log(""); - } - return; - } - - if (options.clear) { - const cleared = await orchestrator.clearAnnotation(targetPath, outputPath, scope); - console.log(cleared ? `Cleared annotation for ${scope}` : `No annotation found for ${scope}`); - return; - } - - if (!note) { - const annotation = await orchestrator.readAnnotation(targetPath, outputPath, scope); - if (!annotation) { - console.log(`No annotation found for ${scope}`); - return; - } - - console.log(`[${annotation.scope}] ${annotation.updatedAt}`); - console.log(annotation.note); - return; - } - - const annotation = await orchestrator.annotateTarget(targetPath, outputPath, { - scope, - note - }); - console.log(`Saved annotation for ${annotation.scope}`); - }); - -program - .command("feedback") - .argument("", "Repository that owns the generated governance memory") - .requiredOption("--agent ", "Agent identifier") - .requiredOption("--task ", "Task identifier") - .requiredOption("--context ", "Learning context") - .requiredOption("--problem ", "Detected problem") - .requiredOption("--action ", "Action taken") - .requiredOption("--outcome ", "Learning outcome") - .option("--confidence ", "Confidence score", "0.8") - .option("-o, --output ", "Output directory") - .action( - async ( - target: string, - options: { - agent: string; - task: string; - context: string; - problem: string; - action: string; - outcome: LearningOutcome; - confidence: string; - output?: string; - } - ) => { - const targetPath = resolveTarget(target); - const outputPath = resolveOutput(targetPath, options.output); - const record = await orchestrator.recordFeedback(targetPath, outputPath, { - agentId: options.agent, - taskId: options.task, - context: options.context, - detectedProblem: options.problem, - actionTaken: options.action, - outcome: options.outcome, - confidenceScore: Number(options.confidence) - }); - console.log(`Recorded learning ${record.lessonId} for ${record.agentId}`); - } - ); - -program.parseAsync(process.argv).catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - console.error(message); - process.exit(1); -}); diff --git a/cli/terminal-console.ts b/cli/terminal-console.ts deleted file mode 100644 index 8f3ee00..0000000 --- a/cli/terminal-console.ts +++ /dev/null @@ -1,1017 +0,0 @@ -import path from "node:path"; -import process from "node:process"; -import { createInterface, type Interface } from "node:readline/promises"; - -import type { AIRouter } from "../core/ai_router/router"; -import type { ProjectBrainOrchestrator } from "../core/orchestrator/main"; -import { getWorkflowDefinition, type WorkflowId } from "../core/workflow_registry"; -import { setLoggerOptions } from "../shared/logger"; -import type { - AskResult, - CodeGraphBuildResult, - ArchitecturePlanResult, - ContextLiteResult, - DoctorResult, - EcosystemAnalysisResult, - FactQueryResult, - FirewallInspectionResult, - GovernanceTrigger, - HarnessAuditResult, - ImpactAnalysisResult, - ImprovementPlanResult, - OrchestrationResult, - ProjectSeedArchetype, - ProjectSeedInput, - ProjectSeedPriority, - ProjectSeedResult, - ResumeResult, - RunbookResult, - SecurityAuditResult, - StartResult, - StatusResult, - SwarmEngine, - SwarmRunResult -} from "../shared/types"; - -type WorkflowChoice = - | "start" - | "doctor" - | "status" - | "resume" - | "security-audit" - | "analyze" - | "weekly" - | "context-lite" - | "ask" - | "fact-query" - | "runbook" - | "harness-audit" - | "swarm" - | "self-improve" - | "code-graph" - | "impact-radius" - | "review-delta" - | "project-new" - | "architecture-plan" - | "firewall" - | "plan-improvements" - | "report"; - -type MenuChoice = "config" | "paths" | "executive-summary" | "swarm" | "run" | "models" | "setup" | "exit"; -type SwarmPreset = "custom" | "cheap" | "balanced" | "thorough"; - -interface ChoiceOption { - value: T; - label: string; -} - -export interface TerminalSessionState { - targetPath: string; - outputPath: string; - trigger: GovernanceTrigger; - verbose: boolean; - ollamaTimeoutMs?: number; - swarmEngine: SwarmEngine; - tokenPreset?: Exclude; - parallelism?: number; - chunkSize?: number; - taskTimeoutMs?: number; - plannerTimeoutMs?: number; - synthesisTimeoutMs?: number; - runTimeoutMs?: number; - maxQueuedTasks?: number; - maxRetries?: number; -} - -interface LaunchTerminalConsoleOptions { - orchestrator: ProjectBrainOrchestrator; - aiRouter: AIRouter; - initialSession?: Partial; - cwd?: string; -} - -const MAIN_MENU: ChoiceOption[] = [ - { value: "run", label: "Inicio recomendado / ejecutar workflow" }, - { value: "executive-summary", label: "Ver resumen ejecutivo" }, - { value: "config", label: "Ver configuracion actual" }, - { value: "paths", label: "Configurar target y output" }, - { value: "swarm", label: "Configurar analisis con agentes" }, - { value: "models", label: "Ver modelos y routing" }, - { value: "setup", label: "Ver setup local y toolchains open source" }, - { value: "exit", label: "Salir" } -]; - -function workflowLabel(workflowId: WorkflowId, fallback: string): string { - const definition = getWorkflowDefinition(workflowId); - return `${fallback}: ${definition.humanLabel}`; -} - -const WORKFLOW_MENU: ChoiceOption[] = [ - { value: "start", label: workflowLabel("start", "Inicio guiado") }, - { value: "status", label: "Ver estado y siguiente paso" }, - { value: "resume", label: workflowLabel("resume", "Continuar") }, - { value: "runbook", label: workflowLabel("runbook", "Preparar ejecucion barata") }, - { value: "harness-audit", label: workflowLabel("harness-audit", "Revisar memoria y costos") }, - { value: "fact-query", label: workflowLabel("fact-query", "Buscar en memoria local") }, - { value: "swarm", label: workflowLabel("swarm", "Analizar con agentes") }, - { value: "project-new", label: "Crear contexto para proyecto nuevo" }, - { value: "architecture-plan", label: "Generar plan de arquitectura" }, - { value: "plan-improvements", label: workflowLabel("plan-improvements", "Crear plan ejecutivo persistente") }, - { value: "doctor", label: workflowLabel("doctor", "Revisar entorno local") }, - { value: "code-graph", label: workflowLabel("code-graph", "Construir mapa factual") }, - { value: "firewall", label: workflowLabel("firewall", "Revisar limites de agentes") }, - { value: "ask", label: workflowLabel("ask", "Pedir algo en lenguaje natural") }, - { value: "security-audit", label: "Auditoria de seguridad" }, - { value: "analyze", label: "Analisis completo legacy" }, - { value: "weekly", label: "Reporte semanal" }, - { value: "context-lite", label: "Contexto ligero" }, - { value: "self-improve", label: "Auto-mejora con swarm" }, - { value: "impact-radius", label: "Impacto de cambios" }, - { value: "review-delta", label: workflowLabel("review-delta", "Revisar delta git") }, - { value: "report", label: "report manifest" } -]; - -const TRIGGER_CHOICES: ChoiceOption[] = [ - { value: "manual", label: "manual" }, - { value: "repository-change", label: "repository-change" }, - { value: "weekly-review", label: "weekly-review" }, - { value: "security-audit", label: "security-audit" }, - { value: "security-advisory", label: "security-advisory" }, - { value: "architecture-review", label: "architecture-review" }, - { value: "incident-detection", label: "incident-detection" }, - { value: "dependency-update", label: "dependency-update" } -]; - -const DEFAULT_OUTPUT_DIR_NAME = "BRAIN"; - -const SWARM_ENGINE_CHOICES: ChoiceOption[] = [ - { value: "bounded", label: "bounded" }, - { value: "deepagents", label: "deepagents" } -]; - -const SWARM_PRESET_CHOICES: ChoiceOption[] = [ - { value: "cheap", label: "Barato: rapido/economico, pocas tareas" }, - { value: "balanced", label: "Balanceado: recomendado, mejor cobertura" }, - { value: "thorough", label: "Profundo: mas lento/caro, maxima cobertura" }, - { value: "custom", label: "Avanzado: configurar manualmente" } -]; - -const PROJECT_ARCHETYPE_CHOICES: ChoiceOption[] = [ - { value: "saas-webapp", label: "SaaS / web app" }, - { value: "marketing-site", label: "Marketing site" }, - { value: "mobile-app", label: "Mobile app" }, - { value: "api-backend", label: "API / backend" }, - { value: "internal-tool", label: "Internal tool" }, - { value: "content-platform", label: "Content platform" }, - { value: "custom", label: "Custom" } -]; - -const PROJECT_PRIORITY_CHOICES: ChoiceOption[] = [ - { value: "mvp-fast", label: "MVP rapido" }, - { value: "solid-architecture", label: "Arquitectura solida" }, - { value: "low-cost", label: "Costo bajo" }, - { value: "security-first", label: "Seguridad alta" } -]; - -export function createDefaultTerminalSession(cwd: string): TerminalSessionState { - const resolvedCwd = path.resolve(cwd); - return { - targetPath: resolvedCwd, - outputPath: path.join(resolvedCwd, DEFAULT_OUTPUT_DIR_NAME), - trigger: "manual", - verbose: false, - swarmEngine: "bounded" - }; -} - -export function summarizeTerminalSession(state: TerminalSessionState): string[] { - return [ - `Target: ${state.targetPath}`, - `Output: ${state.outputPath}`, - `Trigger: ${state.trigger}`, - `Verbose logs: ${state.verbose ? "on" : "off"}`, - `Ollama timeout: ${state.ollamaTimeoutMs ?? "default"}`, - [ - "Swarm defaults:", - `engine=${state.swarmEngine}`, - `preset=${state.tokenPreset ?? "custom"}`, - `parallel=${state.parallelism ?? "auto"}`, - `chunkSize=${state.chunkSize ?? "auto"}`, - `taskTimeoutMs=${state.taskTimeoutMs ?? "auto"}`, - `plannerTimeoutMs=${state.plannerTimeoutMs ?? "auto"}`, - `synthesisTimeoutMs=${state.synthesisTimeoutMs ?? "auto"}`, - `runTimeoutMs=${state.runTimeoutMs ?? "auto"}`, - `maxQueuedTasks=${state.maxQueuedTasks ?? "auto"}`, - `maxRetries=${state.maxRetries ?? "auto"}` - ].join(" ") - ]; -} - -export async function launchTerminalConsole(options: LaunchTerminalConsoleOptions): Promise { - if (!process.stdin.isTTY || !process.stdout.isTTY) { - throw new Error("project-brain console requires an interactive terminal."); - } - - const cwd = options.cwd ?? process.cwd(); - const session: TerminalSessionState = { - ...createDefaultTerminalSession(cwd), - ...options.initialSession - }; - const rl = createInterface({ - input: process.stdin, - output: process.stdout - }); - - try { - console.log(""); - console.log("project-brain terminal console"); - console.log("Entrada guiada para analizar, continuar, buscar hechos y revisar pendientes sin recordar comandos internos."); - - while (true) { - console.log(""); - console.log("Session"); - for (const line of summarizeTerminalSession(session)) { - console.log(`- ${line}`); - } - console.log(""); - - const action = await promptChoice(rl, "Menu principal", MAIN_MENU, "run"); - if (action === "exit") { - console.log("Console closed."); - return; - } - - try { - switch (action) { - case "config": - await configureGeneralDefaults(rl, session); - break; - case "paths": - await configurePaths(rl, session, cwd); - break; - case "executive-summary": { - const result = await options.orchestrator.status(session.targetPath, session.outputPath); - printExecutiveSummaryShortcut(result); - break; - } - case "swarm": - await configureSwarmDefaults(rl, session); - break; - case "run": - await runWorkflow(rl, session, options.orchestrator); - break; - case "models": - await showModels(options.aiRouter); - break; - case "setup": - await showRuntimeSetup(session, options.orchestrator); - break; - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`Console action failed: ${message}`); - } - } - } finally { - rl.close(); - } -} - -function isEcosystemResult(result: OrchestrationResult | EcosystemAnalysisResult): result is EcosystemAnalysisResult { - return "repositories" in result && "knowledgeGraphPath" in result; -} - -async function configurePaths(rl: Interface, session: TerminalSessionState, cwd: string): Promise { - console.log(""); - console.log("Configurar target y output"); - const targetInput = await promptLine(rl, "Target repo/workspace", session.targetPath); - const nextTarget = path.resolve(cwd, targetInput); - const outputInput = await promptLine(rl, "Output dir", session.outputPath); - session.targetPath = nextTarget; - session.outputPath = path.resolve(cwd, outputInput); -} - -async function configureGeneralDefaults(rl: Interface, session: TerminalSessionState): Promise { - console.log(""); - console.log("Configuracion general"); - session.trigger = await promptChoice(rl, "Trigger por defecto", TRIGGER_CHOICES, session.trigger); - session.verbose = await promptYesNo(rl, "Verbose logs", session.verbose); - session.ollamaTimeoutMs = await promptOptionalInteger(rl, "Ollama timeout ms", session.ollamaTimeoutMs); -} - -async function configureSwarmDefaults(rl: Interface, session: TerminalSessionState): Promise { - console.log(""); - console.log("Configuracion del analisis con agentes"); - session.swarmEngine = await promptChoice(rl, "Motor", SWARM_ENGINE_CHOICES, session.swarmEngine); - const preset = await promptChoice(rl, "Preset", SWARM_PRESET_CHOICES, "balanced"); - if (preset !== "custom") { - session.tokenPreset = preset; - applySwarmPreset(session, preset); - console.log(`Costo seleccionado: ${describeSwarmCost(session)}`); - return; - } - session.tokenPreset = undefined; - session.parallelism = await promptOptionalInteger(rl, "Trabajo en paralelo", session.parallelism); - session.chunkSize = await promptOptionalInteger(rl, "Alcance por tarea", session.chunkSize); - session.taskTimeoutMs = await promptOptionalInteger(rl, "Limite de tiempo por tarea ms", session.taskTimeoutMs); - session.plannerTimeoutMs = await promptOptionalInteger(rl, "Limite de planeacion ms", session.plannerTimeoutMs); - session.synthesisTimeoutMs = await promptOptionalInteger(rl, "Limite de sintesis ms", session.synthesisTimeoutMs); - session.runTimeoutMs = await promptOptionalInteger(rl, "Limite total ms", session.runTimeoutMs); - session.maxQueuedTasks = await promptOptionalInteger(rl, "Maximo de tareas", session.maxQueuedTasks); - session.maxRetries = await promptOptionalInteger(rl, "Reintentos", session.maxRetries, { allowZero: true }); -} - -function describeSwarmCost(session: TerminalSessionState): string { - return `tiempo max ${Math.round((session.runTimeoutMs ?? 0) / 60_000)} min, tareas ${session.maxQueuedTasks ?? "auto"}, reintentos ${session.maxRetries ?? "auto"}`; -} - -function applySwarmPreset(session: TerminalSessionState, preset: Exclude): void { - if (preset === "cheap") { - session.parallelism = 2; - session.chunkSize = 1; - session.taskTimeoutMs = 90_000; - session.plannerTimeoutMs = 60_000; - session.synthesisTimeoutMs = 60_000; - session.runTimeoutMs = 120_000; - session.maxQueuedTasks = 4; - session.maxRetries = 0; - return; - } - - if (preset === "balanced") { - session.parallelism = undefined; - session.chunkSize = 1; - session.taskTimeoutMs = 120_000; - session.plannerTimeoutMs = 80_000; - session.synthesisTimeoutMs = 90_000; - session.runTimeoutMs = 180_000; - session.maxQueuedTasks = 6; - session.maxRetries = 1; - return; - } - - session.parallelism = 4; - session.chunkSize = 2; - session.taskTimeoutMs = 180_000; - session.plannerTimeoutMs = 120_000; - session.synthesisTimeoutMs = 120_000; - session.runTimeoutMs = 360_000; - session.maxQueuedTasks = 12; - session.maxRetries = 1; -} - -async function runWorkflow( - rl: Interface, - session: TerminalSessionState, - orchestrator: ProjectBrainOrchestrator -): Promise { - applyRuntimeToggles(session); - const workflow = await promptChoice(rl, "Workflow", WORKFLOW_MENU, "start"); - - switch (workflow) { - case "start": { - const intent = await promptLine(rl, "Objetivo", "optimize analysis and cost"); - const withSwarm = await promptYesNo(rl, "Ejecutar swarm tambien", false); - const result = await orchestrator.start(session.targetPath, session.outputPath, intent, { withSwarm }); - printStartResult(result); - return; - } - case "doctor": { - const result = await orchestrator.doctor(session.targetPath, session.outputPath); - printDoctorResult(result); - return; - } - case "status": { - const result = await orchestrator.status(session.targetPath, session.outputPath); - printStatusResult(result); - return; - } - case "resume": { - const result = await orchestrator.resume(session.targetPath, session.outputPath); - printResumeResult(result); - return; - } - case "analyze": { - const result = await orchestrator.analyzeScope(session.targetPath, session.outputPath, session.trigger); - printAnalyzeResult(result); - return; - } - case "security-audit": { - const result = await orchestrator.securityAudit(session.targetPath, session.outputPath, session.trigger); - printSecurityAuditResult(result); - return; - } - case "weekly": { - const result = await orchestrator.generateWeeklyScope(session.targetPath, session.outputPath); - printWeeklyResult(result); - return; - } - case "context-lite": { - const result = await orchestrator.contextLite(session.targetPath, session.outputPath); - printContextLiteResult(result); - return; - } - case "ask": { - const intent = await promptRequiredLine(rl, "Intent"); - const result = await orchestrator.ask(session.targetPath, session.outputPath, intent); - printAskResult(result); - return; - } - case "fact-query": { - const query = await promptRequiredLine(rl, "Busqueda en memoria"); - const result = await orchestrator.factQuery(session.targetPath, session.outputPath, query); - printFactQueryResult(result); - return; - } - case "runbook": { - const intent = await promptLine(rl, "Objetivo", "optimize analysis and cost"); - const result = await orchestrator.runbook(session.targetPath, session.outputPath, intent); - printRunbookResult(result); - return; - } - case "harness-audit": { - const result = await orchestrator.harnessAudit(session.targetPath, session.outputPath); - printHarnessAuditResult(result); - return; - } - case "swarm": { - const intent = await promptRequiredLine(rl, "Swarm intent"); - const result = await orchestrator.swarm(session.targetPath, session.outputPath, intent, { - engine: session.swarmEngine, - preset: session.tokenPreset, - parallelism: session.parallelism, - chunkSize: session.chunkSize, - taskTimeoutMs: session.taskTimeoutMs, - plannerTimeoutMs: session.plannerTimeoutMs, - synthesisTimeoutMs: session.synthesisTimeoutMs, - runTimeoutMs: session.runTimeoutMs, - maxQueuedTasks: session.maxQueuedTasks, - maxRetries: session.maxRetries - }); - printSwarmResult(result); - return; - } - case "project-new": { - const target = await promptLine(rl, "Directorio del nuevo proyecto", session.targetPath); - const projectName = await promptLine(rl, "Nombre del proyecto", path.basename(target)); - const problem = await promptRequiredLine(rl, "Que problema resuelve"); - const audience = await promptRequiredLine(rl, "Para quien es"); - const archetype = await promptChoice(rl, "Tipo de proyecto", PROJECT_ARCHETYPE_CHOICES, "saas-webapp"); - const stackPreference = await promptLine(rl, "Stack preferido", "recomiendame uno"); - const features = parseCsv(await promptLine(rl, "Features iniciales CSV", "onboarding,dashboard,admin settings")); - const authRequired = await promptYesNo(rl, "Necesita autenticacion", true); - const roles = authRequired ? parseCsv(await promptLine(rl, "Roles CSV", "owner,admin,member")) : []; - const dataEntities = parseCsv(await promptLine(rl, "Entidades principales CSV", "User,Project,ActivityLog")); - const integrations = parseCsv(await promptLine(rl, "Integraciones CSV", "email,storage,analytics")); - const priority = await promptChoice(rl, "Prioridad", PROJECT_PRIORITY_CHOICES, "solid-architecture"); - const language = await promptLine(rl, "Idioma", "es"); - const overwrite = await promptYesNo(rl, "Sobrescribir artefactos existentes si existen", false); - const input: ProjectSeedInput = { - projectName, - problem, - audience, - archetype, - stackPreference, - features, - authRequired, - roles, - dataEntities, - integrations, - priority, - language, - notes: [], - contextOnly: true, - overwrite - }; - const result = await orchestrator.scaffoldProject(path.resolve(target), input); - printProjectSeedResult(result); - return; - } - case "self-improve": { - const intent = await promptLine(rl, "Override intent (optional)"); - const result = await orchestrator.selfImprove( - session.targetPath, - session.outputPath, - intent.trim().length > 0 ? intent : undefined - ); - printSwarmResult(result, "Self-improve"); - return; - } - case "code-graph": { - const result = await orchestrator.buildCodeGraph(session.targetPath, session.outputPath); - printCodeGraphResult(result); - return; - } - case "impact-radius": { - const filesInput = await promptLine(rl, "Changed files CSV (optional)"); - const baseRef = await promptLine(rl, "Base ref", "HEAD~1"); - const headRef = await promptLine(rl, "Head ref", "HEAD"); - const result = await orchestrator.analyzeImpact(session.targetPath, session.outputPath, { - files: filesInput - .split(",") - .map((filePath) => filePath.trim()) - .filter(Boolean), - baseRef, - headRef - }); - printImpactResult(result); - return; - } - case "review-delta": { - const baseRef = await promptLine(rl, "Base ref", "HEAD~1"); - const headRef = await promptLine(rl, "Head ref", "HEAD"); - const result = await orchestrator.reviewDelta(session.targetPath, session.outputPath, { - baseRef, - headRef - }); - printReviewDeltaResult(result); - return; - } - case "firewall": { - const result = await orchestrator.inspectFirewall(session.targetPath, session.outputPath, session.trigger); - printFirewallResult(result); - return; - } - case "architecture-plan": { - const result = await orchestrator.architecturePlan(session.targetPath, session.outputPath); - printArchitecturePlanResult(result); - return; - } - case "plan-improvements": { - const result = await orchestrator.planImprovements(session.targetPath, session.outputPath, session.trigger); - printImprovementPlanResult(result); - return; - } - case "report": { - const manifest = await orchestrator.collectReportManifest(session.outputPath); - console.log(JSON.stringify(manifest, null, 2)); - return; - } - } -} - -async function showModels(aiRouter: AIRouter): Promise { - const inventory = await aiRouter.listModels(); - console.log(""); - console.log("Model inventory"); - console.log(`- Local model: ${inventory.config.localModel}`); - console.log(`- Fallback model: ${inventory.config.fallbackModel}`); - console.log(`- Reasoning model: ${inventory.config.reasoningModel}`); - console.log(`- Cloud provider: ${inventory.cloudConfigured.provider}`); - console.log(`- Cloud model: ${inventory.cloudConfigured.model}`); - console.log(`- Offline mode: ${inventory.offlineMode ? "yes" : "no"}`); - console.log(`- Offline ready: ${inventory.offlineReady ? "yes" : "no"}`); - console.log("Ollama models:"); - if (inventory.availableModels.length === 0) { - console.log("- None detected via Ollama"); - return; - } - - for (const model of inventory.availableModels) { - console.log(`- ${model.name} (${model.residency}, offline=${model.offlineCapable ? "yes" : "no"})`); - } -} - -async function showRuntimeSetup( - session: TerminalSessionState, - orchestrator: ProjectBrainOrchestrator -): Promise { - applyRuntimeToggles(session); - const result = await orchestrator.doctor(session.targetPath, session.outputPath); - console.log(""); - console.log("Runtime setup"); - for (const tier of ["required", "recommended", "optional"] as const) { - const items = result.setupItems.filter((item) => item.tier === tier); - if (items.length === 0) { - continue; - } - const title = - tier === "required" - ? "Required local runtime" - : tier === "recommended" - ? "Recommended for this target" - : "Optional open-source expansion"; - console.log(title); - for (const item of items) { - console.log(`- ${item.label}: ${item.status.toUpperCase()} - ${item.summary}`); - console.log(` Install / enable: ${item.installHint}`); - } - console.log(""); - } -} - -function applyRuntimeToggles(session: TerminalSessionState): void { - setLoggerOptions({ verbose: session.verbose }); - if (session.ollamaTimeoutMs) { - process.env.OLLAMA_TIMEOUT_MS = String(session.ollamaTimeoutMs); - } else { - delete process.env.OLLAMA_TIMEOUT_MS; - } -} - -function parseCsv(value: string): string[] { - return value - .split(",") - .map((item) => item.trim()) - .filter(Boolean); -} - -async function promptLine(rl: Interface, label: string, defaultValue = ""): Promise { - const prompt = defaultValue.length > 0 ? `${label} [${defaultValue}]: ` : `${label}: `; - const answer = (await rl.question(prompt)).trim(); - return answer.length > 0 ? answer : defaultValue; -} - -async function promptRequiredLine(rl: Interface, label: string): Promise { - while (true) { - const answer = (await rl.question(`${label}: `)).trim(); - if (answer.length > 0) { - return answer; - } - console.log("Este campo no puede quedar vacio."); - } -} - -async function promptOptionalInteger( - rl: Interface, - label: string, - current?: number, - options: { allowZero?: boolean } = {} -): Promise { - while (true) { - const placeholder = current === undefined ? "auto" : String(current); - const answer = (await rl.question(`${label} [${placeholder}; escribe auto para limpiar]: `)).trim().toLowerCase(); - if (answer.length === 0) { - return current; - } - if (answer === "auto" || answer === "none" || answer === "default") { - return undefined; - } - const parsed = Number(answer); - if (Number.isFinite(parsed) && (options.allowZero ? parsed >= 0 : parsed > 0)) { - return Math.trunc(parsed); - } - console.log(options.allowZero ? "Ingresa cero, un entero positivo o 'auto'." : "Ingresa un entero positivo o 'auto'."); - } -} - -async function promptYesNo(rl: Interface, label: string, current: boolean): Promise { - while (true) { - const answer = (await rl.question(`${label} [${current ? "Y/n" : "y/N"}]: `)).trim().toLowerCase(); - if (answer.length === 0) { - return current; - } - if (answer === "y" || answer === "yes" || answer === "s" || answer === "si") { - return true; - } - if (answer === "n" || answer === "no") { - return false; - } - console.log("Responde y/n."); - } -} - -async function promptChoice( - rl: Interface, - label: string, - options: ChoiceOption[], - defaultValue?: T -): Promise { - while (true) { - console.log(label); - options.forEach((option, index) => { - const suffix = option.value === defaultValue ? " (default)" : ""; - console.log(` ${index + 1}. ${option.label}${suffix}`); - }); - const answer = (await rl.question("> ")).trim().toLowerCase(); - if (answer.length === 0 && defaultValue) { - return defaultValue; - } - - const numeric = Number.parseInt(answer, 10); - if (Number.isFinite(numeric) && numeric >= 1 && numeric <= options.length) { - return options[numeric - 1].value; - } - - const directMatch = options.find((option) => option.value === answer); - if (directMatch) { - return directMatch.value; - } - - console.log("Seleccion invalida. Usa el numero o el valor exacto."); - } -} - -function printDoctorResult(result: DoctorResult): void { - console.log(""); - console.log("Doctor"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log( - `- Summary: passed=${result.summary.passed}, warnings=${result.summary.warnings}, failed=${result.summary.failed}` - ); - console.log(`- Headline: ${result.summary.headline}`); - for (const check of result.checks) { - console.log(`- ${check.label}: ${check.status.toUpperCase()} - ${check.summary}`); - } - for (const tier of ["required", "recommended", "optional"] as const) { - const items = result.setupItems.filter((item) => item.tier === tier); - if (items.length === 0) { - continue; - } - const title = - tier === "required" - ? "Required local runtime" - : tier === "recommended" - ? "Recommended for this target" - : "Optional open-source expansion"; - console.log(`- ${title}:`); - for (const item of items) { - console.log(` - ${item.label}: ${item.status.toUpperCase()} - ${item.installHint}`); - } - } -} - -function printStatusResult(result: StatusResult): void { - console.log(""); - console.log("Status"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log(`- Git: repo=${result.git.isGitRepo ? "yes" : "no"}, branch=${result.git.branch ?? "unknown"}`); - console.log(`- Headline: ${result.summary.headline}`); - console.log(`- Memory: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - for (const artifact of result.artifacts) { - console.log(`- ${artifact.label}: ${artifact.exists ? "present" : "missing"}`); - } -} - -function printExecutiveSummaryShortcut(result: StatusResult): void { - console.log(""); - console.log("Resumen ejecutivo"); - console.log(`- Markdown: ${result.executiveSummary.reportPath}`); - console.log(`- JSON: ${result.executiveSummary.memoryPath}`); - console.log(`- Scopes: ${result.executiveSummary.status.scopeCount}`); - console.log(`- Scopes listos: ${result.executiveSummary.status.completeFreshScopes}`); - console.log(`- Scopes obsoletos: ${result.executiveSummary.status.staleScopes}`); - console.log(`- Ultimo analisis: ${result.executiveSummary.status.latestSwarmHeadline ?? "Sin swarm registrado"}`); -} - -function printResumeResult(result: ResumeResult): void { - console.log(""); - console.log("Resume"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log(`- Executive summary: ${result.executiveSummary.reportPath}`); - console.log(`- Stage: ${result.summary.stage}`); - console.log(`- Headline: ${result.summary.headline}`); - console.log(`- Memory readiness: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - if (result.latestArtifact) { - console.log(`- Latest artifact: ${result.latestArtifact.label}`); - } - for (const note of result.notes) { - console.log(`- ${note}`); - } -} - -function printStartResult(result: StartResult): void { - console.log(""); - console.log("Inicio guiado"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log(`- Executive summary: ${result.executiveSummary.reportPath}`); - console.log(`- Headline: ${result.headline}`); - console.log(`- Memory readiness: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - for (const step of result.executedSteps) { - console.log(`- [${step.status}] ${step.label}: ${step.summary}`); - } - if (result.nextCommand) { - console.log(`- Siguiente: ${result.nextCommand}`); - } -} - -function printFactQueryResult(result: FactQueryResult): void { - console.log(""); - console.log("Busqueda en memoria local"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log(`- Answer: ${result.answer}`); - console.log(`- Evidence refs: ${result.evidenceRefs.join(", ") || "None"}`); - if (result.unknowns.length > 0) { - console.log(`- Unknowns: ${result.unknowns.join(" | ")}`); - } -} - -function printRunbookResult(result: RunbookResult): void { - console.log(""); - console.log("Ruta barata"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log(`- Executive summary: ${result.executiveSummary.reportPath}`); - for (const step of result.steps) { - console.log(`- [${step.status}] ${step.id}. ${step.title}: ${step.command}`); - } -} - -function printHarnessAuditResult(result: HarnessAuditResult): void { - console.log(""); - console.log("Memoria y costos"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log(`- Score: ${result.score}`); - console.log(`- Token risk: ${result.tokenRisk}`); - console.log(`- Memory readiness: ${result.memoryReadiness.status} - ${result.memoryReadiness.reason}`); - for (const check of result.checks) { - console.log(`- [${check.status}] ${check.label}: ${check.summary}`); - } - if (result.suggestedCommands.length > 0) { - console.log(`- Siguiente: ${result.suggestedCommands[0]}`); - } -} - -function printAnalyzeResult(result: OrchestrationResult | EcosystemAnalysisResult): void { - console.log(""); - console.log("Analyze"); - if (isEcosystemResult(result)) { - console.log(`- Workspace: ${result.rootPath}`); - console.log(`- Repositories: ${result.repositories.map((repository) => repository.repoName).join(", ")}`); - console.log(`- Knowledge graph: ${result.knowledgeGraphPath}`); - console.log(`- Ecosystem report: ${result.ecosystemReportPath}`); - return; - } - - console.log(`- Repo: ${result.context.repoName}`); - console.log(`- AI_CONTEXT: ${result.context.memoryDir}`); - console.log(`- Reports: ${result.context.reportsDir}`); - console.log(`- Docs: ${result.context.docsDir}`); -} - -function printWeeklyResult(result: OrchestrationResult | EcosystemAnalysisResult): void { - console.log(""); - console.log("Weekly"); - if (isEcosystemResult(result)) { - console.log(`- Repositories: ${result.repositories.map((repository) => repository.repoName).join(", ")}`); - console.log(`- Ecosystem report: ${result.ecosystemReportPath}`); - console.log(`- Knowledge graph: ${result.knowledgeGraphPath}`); - return; - } - - console.log(`- Weekly report: ${result.weeklyReportPath}`); - console.log(`- Risk report: ${result.riskReportPath}`); - if (result.reportQualityPath) { - console.log(`- Report quality: ${result.reportQualityPath}`); - } -} - -function printContextLiteResult(result: ContextLiteResult): void { - console.log(""); - console.log("Context-lite"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- AI_CONTEXT: ${result.context.memoryDir}`); - console.log(`- Artifacts: ${result.artifactPaths.map((artifactPath) => path.basename(artifactPath)).join(", ")}`); - for (const line of result.summary) { - console.log(`- ${line}`); - } -} - -function printAskResult(result: AskResult): void { - console.log(""); - console.log("Ask"); - console.log(`- Workflow: ${result.workflow}`); - console.log(`- Brief: ${result.briefPath}`); - console.log(`- Headline: ${result.headline}`); - console.log(`- Reason: ${result.routingReason}`); - console.log(`- Artifacts: ${result.artifacts.map((artifact) => `${artifact.label}=${artifact.path}`).join(" | ") || "None"}`); - if (result.guidedExecution) { - console.log(`- Guided: ${result.guidedExecution.label} -> ${result.guidedExecution.command}`); - } - if (result.aiAssistance) { - console.log( - `- AI assist: ${result.aiAssistance.model} (${result.aiAssistance.provider}, ${result.aiAssistance.residency}, profile=${result.aiAssistance.profile})` - ); - } - console.log(`- Next: ${result.followUps.join(" | ") || "None"}`); -} - -function printSwarmResult(result: SwarmRunResult, label = "Swarm"): void { - console.log(""); - console.log(label); - console.log(`- Engine: ${result.engine}`); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - console.log(`- Planner: ${result.planner.model} (${result.planner.provider}, ${result.planner.residency})`); - console.log( - `- Chunking: size=${result.chunking.selectedChunkSize}, scopeChunks=${result.chunking.scopeChunks}, queuedTasks=${result.chunking.queuedTasks}, scopeBias=${result.chunking.scopeBias}` - ); - console.log( - `- Resilience: runTimeoutMs=${result.resilience.runTimeoutMs}, taskTimeoutMs=${result.resilience.taskTimeoutMs}, plannerTimeoutMs=${result.resilience.plannerTimeoutMs}, synthesisTimeoutMs=${result.resilience.synthesisTimeoutMs}, maxRetries=${result.resilience.maxRetries}` - ); - console.log(`- Parallelism: ${result.parallelism.selected} workers, pressure=${result.parallelism.pressure}`); - console.log(`- Tasks: ${result.tasks.map((task) => `${task.title}[${task.profile}]`).join(" | ") || "None"}`); - console.log(`- Headline: ${result.synthesis.headline}`); - if (result.optimization) { - console.log( - `- Optimization: cacheHits=${result.optimization.cacheHits}, cacheMisses=${result.optimization.cacheMisses}, derivedQueued=${result.optimization.derivedTasksQueued}, derivedSkipped=${result.optimization.derivedTasksSkipped}` - ); - } -} - -function printSecurityAuditResult(result: SecurityAuditResult): void { - const counts = result.findings.reduce>((accumulator, finding) => { - accumulator[finding.severity] = (accumulator[finding.severity] ?? 0) + 1; - return accumulator; - }, {}); - - console.log(""); - console.log("Security audit"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Memory: ${result.memoryPath}`); - if (result.contextLiteReportPath) { - console.log(`- Context-lite: ${result.contextLiteReportPath}`); - } - console.log(`- Verdict: ${result.verdict}`); - console.log(`- Headline: ${result.headline}`); - console.log( - `- Findings: critical=${counts.critical ?? 0}, high=${counts.high ?? 0}, medium=${counts.medium ?? 0}, low=${counts.low ?? 0}, info=${counts.info ?? 0}` - ); - console.log(`- Coverage gaps: ${result.coverage.filter((entry) => entry.status === "not-reviewed").length}`); -} - -function printCodeGraphResult(result: CodeGraphBuildResult): void { - console.log(""); - console.log("Code graph"); - console.log(`- Graph: ${result.graphPath}`); - console.log(`- Files: ${result.graph.stats.files}`); - console.log(`- Symbols: ${result.graph.stats.symbols}`); - console.log(`- Nodes: ${result.graph.stats.nodes}`); - console.log(`- Edges: ${result.graph.stats.edges}`); -} - -function printImpactResult(result: ImpactAnalysisResult): void { - console.log(""); - console.log("Impact radius"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Graph: ${result.graphPath}`); - console.log(`- Changed files: ${result.changedFiles.join(", ") || "None"}`); - console.log(`- Review set: ${result.reviewFiles.join(", ") || "None"}`); - console.log(`- Related tests: ${result.impactedTests.join(", ") || "None"}`); -} - -function printReviewDeltaResult(result: ImpactAnalysisResult): void { - console.log(""); - console.log("Review delta"); - console.log(`- Report: ${result.reportPath}`); - console.log(`- Changed files: ${result.changedFiles.join(", ") || "None"}`); - console.log(`- Review set: ${result.reviewFiles.join(", ") || "None"}`); - console.log(`- Related tests: ${result.impactedTests.join(", ") || "None"}`); -} - -function printFirewallResult(result: FirewallInspectionResult): void { - console.log(""); - console.log("Firewall"); - console.log(`- Report: ${result.firewall.reportPath}`); - console.log(`- Policy: ${result.firewall.policyPath}`); - console.log(`- Packets: ${result.firewall.packets.length}`); - console.log(`- Allowed: ${result.firewall.stats.allowed}`); - console.log(`- Review required: ${result.firewall.stats.reviewRequired}`); - console.log(`- Blocked: ${result.firewall.stats.blocked}`); -} - -function printImprovementPlanResult(result: ImprovementPlanResult): void { - console.log(""); - console.log("Improvement plan"); - console.log(`- Plan dir: ${result.planDir}`); - console.log(`- Summary: ${result.summaryPath}`); - console.log(`- State: ${result.statePath}`); - console.log(`- Risks: ${result.risksPath}`); - console.log(`- Roadmap: ${result.roadmapPath}`); - console.log(`- Tracks: ${result.tracksPath}`); -} - -function printArchitecturePlanResult(result: ArchitecturePlanResult): void { - console.log(""); - console.log("Architecture plan"); - console.log(`- Plan dir: ${result.planDir}`); - console.log(`- Blueprint: ${result.blueprintPath}`); - console.log(`- State: ${result.statePath}`); - console.log(`- CLAUDE context: ${result.claudeContextPath}`); - console.log(`- Memory: ${result.memoryPath}`); -} - -function printProjectSeedResult(result: ProjectSeedResult): void { - console.log(""); - console.log("Project seed"); - console.log(`- Project: ${result.projectName}`); - console.log(`- Target: ${result.targetPath}`); - console.log(`- Archetype: ${result.archetype}`); - console.log(`- Charter: ${result.artifactPaths.projectCharterPath}`); - console.log(`- Requirements: ${result.artifactPaths.requirementsPath}`); - console.log(`- Blueprint: ${result.artifactPaths.blueprintPath}`); - console.log(`- Decisions: ${result.artifactPaths.decisionsPath}`); - console.log(`- Memory brief: ${result.artifactPaths.memoryBriefPath}`); - console.log(`- Runbook: ${result.artifactPaths.runbookPath}`); - console.log(`- Architecture blueprint: ${result.artifactPaths.architectureBlueprintPath}`); - console.log(`- Architecture state: ${result.artifactPaths.architectureStatePath}`); - console.log(`- Project seed memory: ${result.artifactPaths.projectSeedMemoryPath}`); - console.log(`- Backlog: ${result.artifactPaths.backlogPath}`); - console.log(`- CLAUDE: ${result.artifactPaths.claudePath}`); -} diff --git a/config/agents.json b/config/agents.json deleted file mode 100644 index a6b64af..0000000 --- a/config/agents.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "runtime_prompt_directory": "agents/prompts", - "export_prompt_directory": "prompts/agent_prompts", - "agents": [ - { - "id": "product-owner-agent", - "display_name": "ProductOwnerAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "qa-agent", - "display_name": "QAAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "ux-agent", - "display_name": "UXAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "ux-improvement-agent", - "display_name": "UXImprovementAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "security-agent", - "display_name": "SecurityAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "dependency-agent", - "display_name": "DependencyAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "architecture-agent", - "display_name": "ArchitectureAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "observability-agent", - "display_name": "ObservabilityAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "legal-agent", - "display_name": "LegalAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "optimization-agent", - "display_name": "OptimizationAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "documentation-agent", - "display_name": "DocumentationAgent", - "allowed_actions": ["analyze", "propose", "report"] - }, - { - "id": "dev-agent", - "display_name": "DevAgent", - "allowed_actions": ["analyze", "propose", "report"] - } - ] -} diff --git a/config/models.json b/config/models.json deleted file mode 100644 index 5b0861d..0000000 --- a/config/models.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "local_model": "deepseek-coder:6.7b", - "fallback_model": "qwen2.5-coder:7b", - "reasoning_model": "llama3.1:8b", - "reviewer_model": "qwen2.5-coder:7b", - "planner_model": "qwen2.5-coder:7b", - "synthesizer_model": "qwen2.5-coder:7b", - "cloud_provider": "openai", - "cloud_model": "gpt-4.1", - "ollama_timeout_ms": 180000, - "offline_mode": true, - "allow_remote_ollama": true, - "routing": { - "ux-audit": "local", - "ux-improvement": "local", - "qa-analysis": "local", - "performance-analysis": "local", - "documentation-review": "local", - "intent-routing": "cloud", - "report-synthesis": "local", - "architecture-review": "cloud", - "large-refactor-analysis": "cloud" - }, - "task_profiles": { - "repository-scanning": "worker", - "code-smell-detection": "reviewer", - "ux-audit": "reviewer", - "ux-improvement": "reviewer", - "qa-analysis": "reviewer", - "performance-analysis": "reviewer", - "documentation-review": "synthesizer", - "intent-routing": "planner", - "report-synthesis": "synthesizer", - "architecture-review": "planner", - "large-refactor-analysis": "planner", - "generic-analysis": "worker" - } -} diff --git a/core/ai_router/router.ts b/core/ai_router/router.ts deleted file mode 100644 index e7d5a1b..0000000 --- a/core/ai_router/router.ts +++ /dev/null @@ -1,681 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import path from "node:path"; - -import { - DEFAULT_OLLAMA_TIMEOUT_MS, - OllamaAdapter, - type LocalModelAdapter, - type OllamaModelDescriptor, - type OllamaModelResidency -} from "../../integrations/ollama_adapter"; -import { StructuredLogger } from "../../shared/logger"; -import { applyTokenPolicy } from "../token_policy"; - -export type ModelRoute = "local" | "cloud"; -export type LocalProvider = "ollama"; -export type CloudProvider = "openai" | "codex" | "gemini"; -export type ModelProvider = LocalProvider | CloudProvider; -export type ModelProfile = "worker" | "reviewer" | "reasoning" | "planner" | "synthesizer"; -export type AIRouterTask = - | "repository-scanning" - | "code-smell-detection" - | "ux-audit" - | "ux-improvement" - | "qa-analysis" - | "architecture-review" - | "performance-analysis" - | "documentation-review" - | "large-refactor-analysis" - | "intent-routing" - | "report-synthesis" - | "generic-analysis"; - -export interface ModelProfileConfig { - worker: string; - reviewer: string; - reasoning: string; - planner: string; - synthesizer: string; -} - -export interface ModelConfig { - localModel: string; - cloudModel: string; - fallbackModel: string; - reasoningModel: string; - offlineMode: boolean; - allowRemoteOllama: boolean; - ollamaTimeoutMs: number; - profiles: ModelProfileConfig; - routing: Partial>; - taskProfiles: Partial>; -} - -export interface ModelSelection { - preferredRoute: ModelRoute; - selectedRoute: ModelRoute; - provider: ModelProvider; - model: string; - profile: ModelProfile; - residency: OllamaModelResidency | "remote"; - reason: string; - offlineCapable: boolean; -} - -export interface ModelInventory { - config: ModelConfig; - localProvider: LocalProvider; - localModelsAvailable: string[]; - availableModels: OllamaModelDescriptor[]; - localConfigured: string; - fallbackConfigured: string; - resolvedProfiles: Record; - cloudConfigured: { - provider: CloudProvider; - model: string; - }; - routing: Partial>; - taskProfiles: Partial>; - offlineMode: boolean; - remoteOllamaAllowed: boolean; - offlineReady: boolean; -} - -export interface AIRouterRequest { - task?: AIRouterTask; - prompt: string; - context?: string; - profile?: ModelProfile; - allowRemote?: boolean; - timeoutMs?: number; -} - -interface AIRouterOptions { - config?: Partial; - localAdapter?: LocalModelAdapter; - cloudEnabled?: boolean; -} - -interface ModelMatch { - descriptor: OllamaModelDescriptor; - candidate: string; - strategy: "profile" | "fallback"; -} - -const DEFAULT_ROUTING: Partial> = { - "repository-scanning": "local", - "code-smell-detection": "local", - "ux-audit": "local", - "ux-improvement": "local", - "qa-analysis": "local", - "performance-analysis": "local", - "documentation-review": "local", - "intent-routing": "cloud", - "report-synthesis": "local", - "architecture-review": "cloud", - "large-refactor-analysis": "cloud", - "generic-analysis": "local" -}; - -const DEFAULT_TASK_PROFILES: Partial> = { - "repository-scanning": "worker", - "code-smell-detection": "reviewer", - "ux-audit": "reviewer", - "ux-improvement": "reviewer", - "qa-analysis": "reviewer", - "performance-analysis": "reviewer", - "documentation-review": "synthesizer", - "intent-routing": "planner", - "report-synthesis": "synthesizer", - "architecture-review": "planner", - "large-refactor-analysis": "planner", - "generic-analysis": "worker" -}; - -const DEFAULT_PROFILES: ModelProfileConfig = { - worker: "qwen2.5-coder:7b", - reviewer: "deepseek-coder:6.7b", - reasoning: "llama3.1:8b", - planner: "kimi-k2.5:cloud", - synthesizer: "llama3.1:8b" -}; - -function parseTimeoutMs(value: unknown): number | undefined { - const numeric = typeof value === "number" ? value : Number(value); - if (!Number.isFinite(numeric) || numeric <= 0) { - return undefined; - } - - return numeric; -} - -function parseBoolean(value: unknown): boolean | undefined { - if (typeof value === "boolean") { - return value; - } - - if (typeof value === "string") { - const normalized = value.trim().toLowerCase(); - if (normalized === "true") { - return true; - } - if (normalized === "false") { - return false; - } - } - - return undefined; -} - -function unique(items: T[]): T[] { - return [...new Set(items)]; -} - -const DEFAULT_OLLAMA_TIMEOUT = parseTimeoutMs(process.env.OLLAMA_TIMEOUT_MS) ?? DEFAULT_OLLAMA_TIMEOUT_MS; - -const DEFAULT_CONFIG: ModelConfig = { - localModel: DEFAULT_PROFILES.worker, - cloudModel: "gpt-4.1", - fallbackModel: DEFAULT_PROFILES.reviewer, - reasoningModel: DEFAULT_PROFILES.reasoning, - offlineMode: true, - allowRemoteOllama: true, - ollamaTimeoutMs: DEFAULT_OLLAMA_TIMEOUT, - profiles: { ...DEFAULT_PROFILES }, - routing: { ...DEFAULT_ROUTING }, - taskProfiles: { ...DEFAULT_TASK_PROFILES } -}; - -const PROMPT_LOCAL_HINTS = [ - /repository\s+scann/i, - /scan\s+the\s+repo/i, - /repo(sitory)?\s+discover/i, - /code\s+smell/i, - /smell\s+detection/i, - /ux\s+audit/i, - /ux\s+improvement/i, - /ui\s+audit/i, - /usability\s+audit/i, - /performance\s+analysis/i, - /documentation\s+review/i, - /qa\s+analysis/i -]; - -const PROMPT_CLOUD_HINTS = [ - /architecture\s+redesign/i, - /redesign\s+the\s+architecture/i, - /architecture\s+review/i, - /re-?architect/i, - /large\s+refactor\s+proposal/i, - /large\s+refactor/i, - /major\s+refactor/i, - /system\s+redesign/i, - /strategy/i, - /roadmap/i, - /deploy/i -]; - -const PROMPT_SYNTHESIS_HINTS = [ - /synthesi[sz]e/i, - /summary/i, - /resumen/i, - /executive/i, - /brief/i, - /handoff/i -]; - -const PROMPT_REASONING_HINTS = [ - /trade-?off/i, - /reason/i, - /compare/i, - /decision/i, - /scope/i, - /alcance/i -]; - -function resolveProjectBrainRoot(startDir: string): string { - let current = startDir; - - while (true) { - const candidate = path.join(current, "package.json"); - if (existsSync(candidate)) { - try { - const parsed = JSON.parse(readFileSync(candidate, "utf8")) as { name?: string }; - if (parsed.name === "project-brain") { - return current; - } - } catch { - // Keep walking until the package root is found. - } - } - - const parent = path.dirname(current); - if (parent === current) { - return process.cwd(); - } - current = parent; - } -} - -function loadConfigFromDisk(): ModelConfig { - const root = resolveProjectBrainRoot(__dirname); - const configPath = path.join(root, "config", "models.json"); - - try { - const parsed = JSON.parse(readFileSync(configPath, "utf8")) as Partial & { - local?: string; - cloud?: string; - fallback?: string; - local_model?: string; - cloud_model?: string; - fallback_model?: string; - reasoning_model?: string; - reasoningModel?: string; - offline_mode?: boolean | string; - allow_remote_ollama?: boolean | string; - allowRemoteOllama?: boolean | string; - worker_model?: string; - workerModel?: string; - reviewer_model?: string; - reviewerModel?: string; - planner_model?: string; - plannerModel?: string; - synthesizer_model?: string; - synthesizerModel?: string; - profiles?: Partial; - ollama_timeout_ms?: number; - routing?: Partial>; - task_profiles?: Partial>; - taskProfiles?: Partial>; - }; - - const workerModel = - parsed.profiles?.worker ?? - parsed.workerModel ?? - parsed.worker_model ?? - parsed.localModel ?? - parsed.local_model ?? - parsed.local ?? - DEFAULT_CONFIG.localModel; - const fallbackModel = parsed.fallbackModel ?? parsed.fallback_model ?? parsed.fallback ?? DEFAULT_CONFIG.fallbackModel; - const reasoningModel = - parsed.profiles?.reasoning ?? - parsed.reasoningModel ?? - parsed.reasoning_model ?? - DEFAULT_CONFIG.reasoningModel; - const profiles: ModelProfileConfig = { - worker: workerModel, - reviewer: parsed.profiles?.reviewer ?? parsed.reviewerModel ?? parsed.reviewer_model ?? fallbackModel, - reasoning: reasoningModel, - planner: parsed.profiles?.planner ?? parsed.plannerModel ?? parsed.planner_model ?? DEFAULT_CONFIG.profiles.planner, - synthesizer: - parsed.profiles?.synthesizer ?? - parsed.synthesizerModel ?? - parsed.synthesizer_model ?? - reasoningModel - }; - - return { - localModel: workerModel, - cloudModel: parsed.cloudModel ?? parsed.cloud_model ?? parsed.cloud ?? DEFAULT_CONFIG.cloudModel, - fallbackModel, - reasoningModel, - offlineMode: parseBoolean(parsed.offlineMode ?? parsed.offline_mode) ?? DEFAULT_CONFIG.offlineMode, - allowRemoteOllama: - parseBoolean(parsed.allowRemoteOllama ?? parsed.allow_remote_ollama) ?? DEFAULT_CONFIG.allowRemoteOllama, - ollamaTimeoutMs: parseTimeoutMs(parsed.ollamaTimeoutMs ?? parsed.ollama_timeout_ms) ?? DEFAULT_CONFIG.ollamaTimeoutMs, - profiles, - routing: { - ...DEFAULT_ROUTING, - ...(parsed.routing ?? {}) - }, - taskProfiles: { - ...DEFAULT_TASK_PROFILES, - ...(parsed.taskProfiles ?? parsed.task_profiles ?? {}) - } - }; - } catch { - return { - ...DEFAULT_CONFIG, - profiles: { ...DEFAULT_CONFIG.profiles }, - routing: { ...DEFAULT_ROUTING }, - taskProfiles: { ...DEFAULT_TASK_PROFILES } - }; - } -} - -function inferCloudProvider(model: string): CloudProvider { - const normalized = model.toLowerCase(); - if (normalized.includes("gemini")) { - return "gemini"; - } - if (normalized.includes("codex")) { - return "codex"; - } - return "openai"; -} - -function withDefaultTag(model: string): string { - return model.includes(":") ? model : `${model}:latest`; -} - -function createLocalDescriptor(model: string): OllamaModelDescriptor { - return { - name: withDefaultTag(model), - residency: "local", - offlineCapable: true - }; -} - -function resolveDescriptor(preferred: string, available: OllamaModelDescriptor[]): OllamaModelDescriptor | undefined { - const exact = available.find((model) => model.name === preferred || model.name === withDefaultTag(preferred)); - if (exact) { - return exact; - } - - return available.find((model) => model.name === preferred || model.name.startsWith(`${preferred}:`)); -} - -function normalizeRequest(request: string | AIRouterRequest): AIRouterRequest { - if (typeof request === "string") { - return { - prompt: request, - task: "generic-analysis" - }; - } - - return { - task: request.task ?? "generic-analysis", - prompt: request.prompt, - context: request.context, - profile: request.profile, - allowRemote: request.allowRemote, - timeoutMs: request.timeoutMs - }; -} - -function inferPreferredRoute(request: AIRouterRequest, config: ModelConfig): ModelRoute { - if (request.task && config.routing[request.task]) { - return config.routing[request.task] as ModelRoute; - } - - if (PROMPT_CLOUD_HINTS.some((rule) => rule.test(request.prompt))) { - return "cloud"; - } - - if (PROMPT_LOCAL_HINTS.some((rule) => rule.test(request.prompt))) { - return "local"; - } - - return "local"; -} - -function inferPreferredProfile(request: AIRouterRequest, config: ModelConfig): ModelProfile { - if (request.profile) { - return request.profile; - } - - if (request.task && config.taskProfiles[request.task]) { - return config.taskProfiles[request.task] as ModelProfile; - } - - if (PROMPT_SYNTHESIS_HINTS.some((rule) => rule.test(request.prompt))) { - return "synthesizer"; - } - - if (PROMPT_CLOUD_HINTS.some((rule) => rule.test(request.prompt))) { - return "planner"; - } - - if (PROMPT_REASONING_HINTS.some((rule) => rule.test(request.prompt))) { - return "reasoning"; - } - - if (/review|audit|qa|bug|smell/i.test(request.prompt)) { - return "reviewer"; - } - - return "worker"; -} - -function profileCandidates(profile: ModelProfile, config: ModelConfig): string[] { - const candidatesByProfile: Record = { - worker: [config.profiles.worker, config.localModel, config.fallbackModel, config.reasoningModel], - reviewer: [config.profiles.reviewer, config.fallbackModel, config.localModel, config.reasoningModel], - reasoning: [config.profiles.reasoning, config.reasoningModel, config.profiles.synthesizer, config.localModel], - planner: [config.profiles.planner, config.profiles.reasoning, config.reasoningModel, config.localModel, config.fallbackModel], - synthesizer: [config.profiles.synthesizer, config.profiles.reasoning, config.reasoningModel, config.localModel, config.fallbackModel] - }; - - return unique(candidatesByProfile[profile].filter(Boolean)); -} - -function allowRemoteForRequest(request: AIRouterRequest, profile: ModelProfile, config: ModelConfig): boolean { - if (typeof request.allowRemote === "boolean") { - return request.allowRemote; - } - - if (!config.allowRemoteOllama) { - return false; - } - - if (!config.offlineMode) { - return true; - } - - return profile === "planner" || profile === "synthesizer"; -} - -function buildOllamaReason(match: ModelMatch, task: AIRouterTask, profile: ModelProfile, preferredRoute: ModelRoute): string { - const residencyText = match.descriptor.residency === "local" ? "local" : "remote"; - const fallbackNote = - match.strategy === "fallback" ? ` The configured ${profile} profile was unavailable, so the router fell back.` : ""; - const routeNote = - preferredRoute === "cloud" && match.descriptor.residency === "local" - ? " Cloud-preferred work was kept local because no planner-grade remote model was required." - : preferredRoute === "local" && match.descriptor.residency === "remote" - ? " The task still runs through Ollama, but this model is not offline-capable." - : ""; - - return `Task ${task} will use the ${profile} profile on the ${residencyText} Ollama model ${match.descriptor.name}.${fallbackNote}${routeNote}`; -} - -export class AIRouter { - private readonly logger = new StructuredLogger("ai-router"); - private readonly config: ModelConfig; - private readonly localAdapter: LocalModelAdapter; - private readonly cloudEnabled: boolean; - - constructor(options: AIRouterOptions = {}) { - const diskConfig = loadConfigFromDisk(); - this.config = { - ...diskConfig, - ...options.config, - profiles: { - ...diskConfig.profiles, - ...(options.config?.profiles ?? {}) - }, - routing: { - ...diskConfig.routing, - ...(options.config?.routing ?? {}) - }, - taskProfiles: { - ...diskConfig.taskProfiles, - ...(options.config?.taskProfiles ?? {}) - } - }; - this.localAdapter = options.localAdapter ?? new OllamaAdapter(undefined, this.config.ollamaTimeoutMs); - this.cloudEnabled = options.cloudEnabled ?? false; - } - - routeForPrompt(request: string | AIRouterRequest): ModelRoute { - return inferPreferredRoute(normalizeRequest(request), this.config); - } - - private async listAvailableModels(): Promise { - const descriptors = this.localAdapter.listModelDescriptors ? await this.localAdapter.listModelDescriptors() : undefined; - if (descriptors && descriptors.length > 0) { - return descriptors - .slice() - .sort((left, right) => left.name.localeCompare(right.name)) - .map((descriptor) => ({ - ...descriptor, - name: descriptor.name, - residency: descriptor.residency, - offlineCapable: descriptor.offlineCapable - })); - } - - return (await this.localAdapter.listModels()) - .map((model) => createLocalDescriptor(model)) - .sort((left, right) => left.name.localeCompare(right.name)); - } - - async listModels(): Promise { - const availableModels = await this.listAvailableModels(); - const localModelsAvailable = availableModels.map((model) => model.name); - const resolveConfigured = (value: string) => resolveDescriptor(value, availableModels)?.name ?? value; - - return { - config: { - ...this.config, - profiles: { ...this.config.profiles }, - routing: { ...this.config.routing }, - taskProfiles: { ...this.config.taskProfiles } - }, - localProvider: "ollama", - localModelsAvailable, - availableModels, - localConfigured: resolveConfigured(this.config.localModel), - fallbackConfigured: resolveConfigured(this.config.fallbackModel), - resolvedProfiles: { - worker: resolveConfigured(this.config.profiles.worker), - reviewer: resolveConfigured(this.config.profiles.reviewer), - reasoning: resolveConfigured(this.config.profiles.reasoning), - planner: resolveConfigured(this.config.profiles.planner), - synthesizer: resolveConfigured(this.config.profiles.synthesizer) - }, - cloudConfigured: { - provider: inferCloudProvider(this.config.cloudModel), - model: this.config.cloudModel - }, - routing: { ...this.config.routing }, - taskProfiles: { ...this.config.taskProfiles }, - offlineMode: this.config.offlineMode, - remoteOllamaAllowed: this.config.allowRemoteOllama, - offlineReady: availableModels.some((model) => model.offlineCapable) - }; - } - - private matchOllamaModel(profile: ModelProfile, available: OllamaModelDescriptor[]): ModelMatch | undefined { - const primaryCandidates = profileCandidates(profile, this.config); - for (const candidate of primaryCandidates) { - const descriptor = resolveDescriptor(candidate, available); - if (descriptor) { - return { - descriptor, - candidate, - strategy: "profile" - }; - } - } - - const fallbackCandidates = unique([this.config.localModel, this.config.fallbackModel, this.config.reasoningModel]); - for (const candidate of fallbackCandidates) { - const descriptor = resolveDescriptor(candidate, available); - if (descriptor) { - return { - descriptor, - candidate, - strategy: "fallback" - }; - } - } - - if (available[0]) { - return { - descriptor: available[0], - candidate: available[0].name, - strategy: "fallback" - }; - } - - return undefined; - } - - async selectModel(input: string | AIRouterRequest): Promise { - const request = normalizeRequest(input); - const preferredRoute = inferPreferredRoute(request, this.config); - const preferredProfile = inferPreferredProfile(request, this.config); - const inventory = await this.listModels(); - const allowRemote = allowRemoteForRequest(request, preferredProfile, this.config); - const allowedOllamaModels = inventory.availableModels.filter((model) => allowRemote || model.offlineCapable); - const match = this.matchOllamaModel(preferredProfile, allowedOllamaModels); - - if (match) { - return { - preferredRoute, - selectedRoute: match.descriptor.offlineCapable ? "local" : "cloud", - provider: "ollama", - model: match.descriptor.name, - profile: preferredProfile, - residency: match.descriptor.residency, - reason: buildOllamaReason(match, request.task ?? "generic-analysis", preferredProfile, preferredRoute), - offlineCapable: match.descriptor.offlineCapable - }; - } - - if (preferredRoute === "cloud" && this.cloudEnabled) { - return { - preferredRoute, - selectedRoute: "cloud", - provider: inferCloudProvider(this.config.cloudModel), - model: this.config.cloudModel, - profile: preferredProfile, - residency: "remote", - reason: `Task ${request.task ?? "generic-analysis"} is cloud-preferred and no Ollama profile match was available.`, - offlineCapable: false - }; - } - - return { - preferredRoute, - selectedRoute: preferredRoute, - provider: inferCloudProvider(this.config.cloudModel), - model: this.config.cloudModel, - profile: preferredProfile, - residency: "remote", - reason: `Task ${request.task ?? "generic-analysis"} had no matching Ollama model for the ${preferredProfile} profile.`, - offlineCapable: false - }; - } - - async ask(input: string | AIRouterRequest): Promise { - const request = applyTokenPolicy(normalizeRequest(input)); - const selection = await this.selectModel(request); - this.logger.info("AI route selected", { - component: "ai-router", - action: "route_select", - task: request.task ?? "generic-analysis", - provider: selection.provider, - model: selection.model, - profile: selection.profile, - residency: selection.residency, - selectedRoute: selection.selectedRoute, - preferredRoute: selection.preferredRoute - }); - - if (selection.provider === "ollama") { - const composedPrompt = request.context - ? `${request.prompt.trim()}\n\nAdditional context:\n${request.context.trim()}` - : request.prompt; - return this.localAdapter.ask(composedPrompt, selection.model, { - timeoutMs: request.timeoutMs - }); - } - - throw new Error( - `Cloud model routing selected ${selection.provider}:${selection.model}, but cloud execution is not enabled in this runtime. Configure a cloud adapter or ensure an Ollama fallback model is available.` - ); - } -} diff --git a/core/codebase_map/index.ts b/core/codebase_map/index.ts deleted file mode 100644 index 5dea041..0000000 --- a/core/codebase_map/index.ts +++ /dev/null @@ -1,534 +0,0 @@ -import path from "node:path"; - -import { listContextAnnotations } from "../../memory/annotations"; -import { ensureDir, writeFileEnsured } from "../../shared/fs-utils"; -import type { CodebaseMapArtifact, ContextAnnotation, DiscoveryResult, ProjectContext } from "../../shared/types"; - -type ConcernSeverity = "high" | "medium" | "low"; - -interface ConcernSignal { - severity: ConcernSeverity; - title: string; - detail: string; -} - -const TEST_FILE_PATTERN = /(^|\/)(__tests__|tests?|spec)(\/|\.|$)/i; -const SOURCE_FILE_PATTERN = /\.(ts|tsx|js|jsx|py|go|java|rs|cs|rb|php)$/i; -const CONFIG_FILE_PATTERN = - /(^|\/)(package(-lock)?\.json|tsconfig\.json|vitest\.config\.[a-z]+|jest\.config\.[a-z]+|requirements\.txt|go\.mod|pom\.xml|Cargo\.toml|Gemfile|composer\.json|Dockerfile|docker-compose\.(ya?ml)|\.github\/workflows\/.+|eslint.*|prettier.*|\.editorconfig)$/i; - -function analysisDate(scannedAt: string): string { - return scannedAt.split("T")[0] ?? scannedAt; -} - -function renderList(items: string[], formatter?: (item: string) => string): string { - if (items.length === 0) { - return "- None detected"; - } - - return items.map((item) => (formatter ? formatter(item) : `- ${item}`)).join("\n"); -} - -function renderFileList(files: string[]): string { - return renderList(files, (file) => `- \`${file}\``); -} - -function renderConcernList(concerns: ConcernSignal[]): string { - return concerns - .map( - (concern) => - `### ${concern.severity.toUpperCase()}: ${concern.title}\n\n${concern.detail}` - ) - .join("\n\n"); -} - -function renderAnnotations(annotations: ContextAnnotation[]): string { - if (annotations.length === 0) { - return "- None recorded"; - } - - return annotations - .map( - (annotation) => - `- ${annotation.scope}: ${annotation.note} (updated ${annotation.updatedAt})` - ) - .join("\n"); -} - -function inferProjectShape(discovery: DiscoveryResult): string { - const frameworks = new Set(discovery.frameworks); - const infrastructure = new Set(discovery.infrastructure); - - if (frameworks.has("NextJS") && frameworks.has("NestJS")) { - return "Full-stack web platform"; - } - - if (frameworks.has("React") || frameworks.has("NextJS")) { - return "Frontend-oriented application"; - } - - if (frameworks.has("Express") || frameworks.has("FastAPI") || frameworks.has("Spring") || frameworks.has("Rails")) { - return "Backend/API service"; - } - - if (infrastructure.has("Terraform") || infrastructure.has("Kubernetes")) { - return "Infrastructure-oriented repository"; - } - - if (discovery.languages.length > 2) { - return "Polyglot software platform"; - } - - return "General software repository"; -} - -function sampleSourceFiles(discovery: DiscoveryResult, limit = 8): string[] { - return discovery.files.filter((file) => SOURCE_FILE_PATTERN.test(file)).slice(0, limit); -} - -function sampleTestFiles(discovery: DiscoveryResult, limit = 8): string[] { - return discovery.files.filter((file) => TEST_FILE_PATTERN.test(file)).slice(0, limit); -} - -function configFiles(discovery: DiscoveryResult, limit = 12): string[] { - return discovery.files.filter((file) => CONFIG_FILE_PATTERN.test(file)).slice(0, limit); -} - -function packageManagers(discovery: DiscoveryResult): string[] { - const managers: string[] = []; - - if (discovery.files.includes("package-lock.json")) { - managers.push("npm (`package-lock.json`)"); - } - if (discovery.files.includes("yarn.lock")) { - managers.push("Yarn (`yarn.lock`)"); - } - if (discovery.files.includes("pnpm-lock.yaml")) { - managers.push("pnpm (`pnpm-lock.yaml`)"); - } - if (discovery.files.includes("requirements.txt")) { - managers.push("pip (`requirements.txt`)"); - } - if (discovery.files.includes("go.mod")) { - managers.push("Go modules (`go.mod`)"); - } - if (discovery.files.includes("Cargo.toml")) { - managers.push("Cargo (`Cargo.toml`)"); - } - if (discovery.files.includes("Gemfile")) { - managers.push("Bundler (`Gemfile`)"); - } - - return managers; -} - -function highlightDependencies(discovery: DiscoveryResult, limit = 10): string[] { - const seen = new Set(); - const highlights: string[] = []; - - for (const manifest of discovery.dependencies) { - for (const dependency of manifest.dependencies) { - if (seen.has(dependency)) { - continue; - } - - seen.add(dependency); - highlights.push(`${dependency} (${manifest.path})`); - - if (highlights.length >= limit) { - return highlights; - } - } - } - - return highlights; -} - -function testingHealth(discovery: DiscoveryResult): string { - if (discovery.testing.length === 0 || discovery.structure.testFileCount === 0) { - return "No clear automated testing baseline was detected."; - } - - const ratio = - discovery.structure.sourceFileCount > 0 - ? discovery.structure.testFileCount / discovery.structure.sourceFileCount - : 0; - - if (ratio < 0.15) { - return "Tests exist, but coverage density looks light relative to the amount of source code."; - } - - return "Testing signals look present and proportionate for a first-pass repository scan."; -} - -function buildConcernSignals(discovery: DiscoveryResult): ConcernSignal[] { - const concerns: ConcernSignal[] = []; - - if (discovery.ci.providers.length === 0) { - concerns.push({ - severity: "high", - title: "No CI pipeline detected", - detail: "Automated validation does not appear to run on every change. That weakens confidence in proposals, refactors, and cross-repository governance." - }); - } - - if (discovery.testing.length === 0 || discovery.structure.testFileCount === 0) { - concerns.push({ - severity: "high", - title: "Testing baseline is missing or opaque", - detail: "The repository scan did not find a reliable automated test surface. Any future improvement plan should start by defining the minimum validation contract." - }); - } else if ( - discovery.structure.sourceFileCount >= 20 && - discovery.structure.testFileCount / discovery.structure.sourceFileCount < 0.15 - ) { - concerns.push({ - severity: "medium", - title: "Test density looks thin", - detail: `Only ${discovery.structure.testFileCount} test files were detected for ${discovery.structure.sourceFileCount} source files. That makes change safety uneven.` - }); - } - - if (discovery.apis.includes("REST") && !discovery.apis.includes("OpenAPI")) { - concerns.push({ - severity: "medium", - title: "REST surface lacks a contract signal", - detail: "REST-style APIs were detected without an OpenAPI or Swagger contract. That raises onboarding cost and weakens downstream agent context." - }); - } - - if (!discovery.logging.structured) { - concerns.push({ - severity: "medium", - title: "Structured logging was not detected", - detail: "Operational analysis and incident reconstruction become harder when logs are ad hoc or absent." - }); - } - - if (discovery.metrics.tools.length === 0) { - concerns.push({ - severity: "medium", - title: "Metrics and tracing signals are absent", - detail: "The scan did not find metrics tooling or alerting configuration. Reliability and performance proposals will have weaker production feedback loops." - }); - } - - if (discovery.structure.subrepos.length > 0 || discovery.structure.submodules.length > 0) { - concerns.push({ - severity: "medium", - title: "Repository boundaries are non-trivial", - detail: "Nested repositories or git submodules were detected. Governance, ownership, and change planning should treat these boundaries explicitly." - }); - } - - if (discovery.languages.length > 2) { - concerns.push({ - severity: "low", - title: "Polyglot surface increases coordination cost", - detail: `Multiple languages were detected (${discovery.languages.join(", ")}). Improvement workflows should keep artifacts and validation commands language-aware.` - }); - } - - if (concerns.length === 0) { - concerns.push({ - severity: "low", - title: "No critical structural gaps detected by first-pass heuristics", - detail: "The repository still benefits from deeper agent analysis, but the deterministic map did not surface obvious execution blockers." - }); - } - - return concerns; -} - -function buildSummary(context: ProjectContext, annotations: ContextAnnotation[]): string { - const discovery = context.discovery; - const concerns = buildConcernSignals(discovery).slice(0, 3); - - return `# Codebase Map Summary - -**Repository:** ${context.repoName} -**Analysis Date:** ${analysisDate(discovery.scannedAt)} -**Repository Type:** ${inferProjectShape(discovery)} - -## Snapshot - -- Languages: ${discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${discovery.frameworks.join(", ") || "Not detected"} -- API styles: ${discovery.apis.join(", ") || "Not detected"} -- Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"} -- Test frameworks: ${discovery.testing.join(", ") || "Not detected"} -- CI providers: ${discovery.ci.providers.join(", ") || "Not detected"} -- Source files: ${discovery.structure.sourceFileCount} -- Test files: ${discovery.structure.testFileCount} - -## Documents - -- \`STACK.md\` for runtime, dependencies, and configuration -- \`INTEGRATIONS.md\` for external surfaces and operational hooks -- \`ARCHITECTURE.md\` for system shape and boundaries -- \`STRUCTURE.md\` for layout and key file locations -- \`CONVENTIONS.md\` for working norms inferred from the repo -- \`TESTING.md\` for validation posture and gaps -- \`CONCERNS.md\` for first-pass risks and follow-up priorities - -## Immediate Concerns - -${renderConcernList(concerns)} - -## Local Notes - -${renderAnnotations(annotations)} - -## Recommended Next Steps - -1. Run \`project-brain analyze ${context.targetPath} --output ${context.outputPath}\` to generate specialist-agent reports and proposals. -2. Review \`CONCERNS.md\` first, then \`TESTING.md\`, before acting on deeper architectural changes. -3. Keep generated artifacts in a dedicated output directory when analyzing external repositories to avoid polluting future scans. -`; -} - -function buildStack(discovery: DiscoveryResult): string { - return `# Technology Stack - -**Analysis Date:** ${analysisDate(discovery.scannedAt)} - -## Languages - -${renderList(discovery.languages)} - -## Runtime and Package Management - -${renderList(packageManagers(discovery))} - -## Frameworks - -${renderList(discovery.frameworks)} - -## Testing Tooling - -${renderList(discovery.testing)} - -## Dependency Highlights - -${renderList(highlightDependencies(discovery))} - -## Configuration Files - -${renderFileList(configFiles(discovery))} -`; -} - -function buildIntegrations(discovery: DiscoveryResult): string { - return `# Integrations - -**Analysis Date:** ${analysisDate(discovery.scannedAt)} - -## API Surface - -- Styles: ${discovery.apis.join(", ") || "Not detected"} -- API-related files: -${renderFileList(discovery.apiFiles)} - -## Delivery and Infrastructure - -- CI providers: ${discovery.ci.providers.join(", ") || "Not detected"} -- Infrastructure signals: ${discovery.infrastructure.join(", ") || "Not detected"} -- Infrastructure files: -${renderFileList(discovery.infraFiles)} - -## Observability - -- Logging frameworks: ${discovery.logging.frameworks.join(", ") || "Not detected"} -- Logging config files: -${renderFileList(discovery.logging.configFiles)} -- Metrics tooling: ${discovery.metrics.tools.join(", ") || "Not detected"} -- Metrics config files: -${renderFileList(discovery.metrics.configFiles)} -- Alerts configured: ${discovery.metrics.alertsConfigured ? "Yes" : "No signal detected"} - -## Repository Integration - -- Git repository: ${discovery.git.isGitRepo ? "Yes" : "No"} -- Active branch: ${discovery.git.branch ?? "Unknown"} -- Latest commit: ${discovery.git.latestCommit ?? "Unavailable"} -- Git submodules: ${discovery.git.hasSubmodules ? "Present" : "Not detected"} -`; -} - -function buildArchitecture(discovery: DiscoveryResult): string { - return `# Architecture - -**Analysis Date:** ${analysisDate(discovery.scannedAt)} - -## Inferred Shape - -- Repository type: ${inferProjectShape(discovery)} -- Top-level directories: ${discovery.structure.topLevelDirectories.length} -- Nested repositories: ${discovery.structure.subrepos.length} -- Git submodules: ${discovery.structure.submodules.length} - -## Primary Boundaries - -${renderList(discovery.structure.topLevelDirectories, (directory) => `- \`${directory}/\``)} - -## Runtime Signals - -- Frameworks: ${discovery.frameworks.join(", ") || "Not detected"} -- API styles: ${discovery.apis.join(", ") || "Not detected"} -- Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"} -- CI: ${discovery.ci.providers.join(", ") || "Not detected"} - -## Source Entry Points - -${renderFileList(sampleSourceFiles(discovery))} -`; -} - -function buildStructure(discovery: DiscoveryResult): string { - return `# Structure - -**Analysis Date:** ${analysisDate(discovery.scannedAt)} - -## Layout Overview - -- Total files scanned: ${discovery.structure.fileCount} -- Source files: ${discovery.structure.sourceFileCount} -- Test files: ${discovery.structure.testFileCount} -- Dependency manifests: ${discovery.manifests.length} - -## Key Directories - -${renderList(discovery.structure.topLevelDirectories, (directory) => `- \`${directory}/\``)} - -## Dependency Manifests - -${renderFileList(discovery.manifests)} - -## Representative Source Files - -${renderFileList(sampleSourceFiles(discovery))} - -## Representative Test Files - -${renderFileList(sampleTestFiles(discovery))} -`; -} - -function buildConventions(discovery: DiscoveryResult): string { - const conventionSignals: string[] = []; - - if (discovery.files.some((file) => file.startsWith("src/"))) { - conventionSignals.push("Application code is primarily organized under `src/`."); - } - if (discovery.files.some((file) => TEST_FILE_PATTERN.test(file))) { - conventionSignals.push("Tests follow repository-local conventions such as `tests/`, `spec`, or `*.test.*` naming."); - } - if (discovery.files.some((file) => /eslint/i.test(file))) { - conventionSignals.push("Linting configuration is present, which suggests enforceable code style expectations."); - } - if (discovery.files.includes("tsconfig.json")) { - conventionSignals.push("TypeScript compiler configuration exists, so type-driven boundaries are part of the workflow."); - } - if (discovery.files.some((file) => file.startsWith("docs/")) || discovery.files.includes("README.md")) { - conventionSignals.push("The repository maintains a documentation surface alongside code."); - } - if (conventionSignals.length === 0) { - conventionSignals.push("No strong repository-wide conventions could be inferred from deterministic file and manifest analysis alone."); - } - - return `# Conventions - -**Analysis Date:** ${analysisDate(discovery.scannedAt)} - -## Observed Working Conventions - -${renderList(conventionSignals)} - -## Configuration Signals - -${renderFileList(configFiles(discovery))} - -## Directory Signals - -${renderList(discovery.structure.topLevelDirectories, (directory) => `- \`${directory}/\``)} - -## Caveat - -This document captures conventions that are visible from repository structure and configuration. Deep style rules, exception handling patterns, and naming nuances still require specialist-agent or human review. -`; -} - -function buildTesting(discovery: DiscoveryResult): string { - return `# Testing - -**Analysis Date:** ${analysisDate(discovery.scannedAt)} - -## Detected Test Tooling - -${renderList(discovery.testing)} - -## Test Surface - -- Test files detected: ${discovery.structure.testFileCount} -- Source files detected: ${discovery.structure.sourceFileCount} -- Assessment: ${testingHealth(discovery)} - -## Example Test Files - -${renderFileList(sampleTestFiles(discovery))} - -## Recommendations - -${renderList( - discovery.recommendations.filter((recommendation) => /test|validation|OpenAPI/i.test(recommendation)) -)} -`; -} - -function buildConcerns(discovery: DiscoveryResult): string { - const concerns = buildConcernSignals(discovery); - - return `# Concerns - -**Analysis Date:** ${analysisDate(discovery.scannedAt)} - -## Prioritized Concerns - -${renderConcernList(concerns)} - -## Deterministic Recommendations - -${renderList(discovery.recommendations)} -`; -} - -export async function writeCodebaseMapArtifacts(context: ProjectContext): Promise { - const codebaseMapDir = path.join(context.docsDir, "codebase_map"); - await ensureDir(codebaseMapDir); - const annotations = await listContextAnnotations(context.outputPath); - - const artifacts = new Map([ - ["SUMMARY.md", buildSummary(context, annotations)], - ["STACK.md", buildStack(context.discovery)], - ["INTEGRATIONS.md", buildIntegrations(context.discovery)], - ["ARCHITECTURE.md", buildArchitecture(context.discovery)], - ["STRUCTURE.md", buildStructure(context.discovery)], - ["CONVENTIONS.md", buildConventions(context.discovery)], - ["TESTING.md", buildTesting(context.discovery)], - ["CONCERNS.md", buildConcerns(context.discovery)] - ]); - - await Promise.all( - [...artifacts.entries()].map(async ([fileName, content]) => { - await writeFileEnsured(path.join(codebaseMapDir, fileName), content); - }) - ); - - return { - repoName: context.repoName, - outputPath: context.outputPath, - codebaseMapDir, - files: [...artifacts.keys()].map((fileName) => path.join(codebaseMapDir, fileName)), - summaryPath: path.join(codebaseMapDir, "SUMMARY.md") - }; -} diff --git a/core/context_builder/index.ts b/core/context_builder/index.ts deleted file mode 100644 index b0d4f57..0000000 --- a/core/context_builder/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { listContextAnnotations, writeAnnotationsArtifact } from "../../memory/annotations"; -import { summarizeOpenApiFiles } from "../../tools/openapi_tools"; -import { initializeProjectMemory, writeDiscoveryArtifacts } from "../../memory/context_store"; -import { StructuredLogger } from "../../shared/logger"; -import type { DiscoveryResult, ProjectContext } from "../../shared/types"; - -export class ContextBuilder { - private readonly logger = new StructuredLogger("context-builder"); - - async build(discovery: DiscoveryResult, outputPath: string): Promise { - this.logger.info("Initializing project memory", { - component: "memory", - action: "memory_init_start", - repoName: discovery.repoName, - outputPath - }); - - const { memoryDir, reportsDir, docsDir, runtimeMemoryDir, learningDir, taskBoardDir, proposalDir, patchProposalDir } = - await initializeProjectMemory(outputPath, discovery); - const openApiSummaries = await summarizeOpenApiFiles( - discovery.targetPath, - discovery.apiFiles.filter((file) => /openapi|swagger/i.test(file)) - ); - - await writeDiscoveryArtifacts(memoryDir, discovery, openApiSummaries); - await writeAnnotationsArtifact(outputPath, await listContextAnnotations(outputPath)); - - this.logger.info("Project memory initialized", { - component: "memory", - action: "memory_init_complete", - repoName: discovery.repoName, - memoryDir, - reportsDir - }); - - return { - repoName: discovery.repoName, - targetPath: discovery.targetPath, - outputPath, - scannedAt: discovery.scannedAt, - discovery, - memoryDir, - reportsDir, - docsDir, - runtimeMemoryDir, - learningDir, - taskBoardDir, - proposalDir, - patchProposalDir - }; - } -} diff --git a/core/context_lite/index.ts b/core/context_lite/index.ts deleted file mode 100644 index db56fb6..0000000 --- a/core/context_lite/index.ts +++ /dev/null @@ -1,3050 +0,0 @@ -import path from "node:path"; - -import { readJsonSafe, readTextSafe, uniqueSorted, writeFileEnsured } from "../../shared/fs-utils"; -import type { ContextLiteResult, DiscoveryResult, ProjectContext } from "../../shared/types"; - -interface PackageJsonShape { - name?: string; - scripts?: Record; -} - -interface RepoMetadata { - readmeSummary?: string; - scripts: string[]; -} - -interface FrontendSignals { - routeFiles: string[]; - layoutFiles: string[]; - pageFiles: string[]; - componentRoots: string[]; - componentFiles: string[]; - renderingStrategy?: string; - i18nSignals: string[]; - stateSignals: string[]; - dataFetchingSignals: string[]; - uiSignals: string[]; - adminSignals: string[]; -} - -interface BackendSignals { - routeFiles: string[]; - endpointExamples: string[]; - contractFiles: string[]; - authSignals: string[]; - validationSignals: string[]; - dataSignals: string[]; - queueSignals: string[]; - webhookSignals: string[]; - integrationSignals: string[]; -} - -interface NavigationSignals { - navFiles: string[]; - navLabels: string[]; - navSectionLabels: string[]; - actorScopes: string[]; - guardFiles: string[]; - guardSummaries: string[]; - permissionFiles: string[]; -} - -interface ModuleSignal { - label: string; - files: string[]; - status: "active" | "legacy" | "fallback"; - note: string; -} - -interface DocumentationInsight { - filePath: string; - title: string; - domainKey?: string; - summary?: string; - actorHighlights: string[]; - ruleHighlights: string[]; - flowHighlights: string[]; - decisionHighlights: string[]; - pendingNotes: string[]; -} - -interface DomainInventoryEntry { - label: string; - docFiles: string[]; - codeFiles: string[]; - highlights: string[]; -} - -interface SourceAuthoritySignals { - primarySources: string[]; - secondarySources: string[]; - declaredCanonicalSources: string[]; - authorityNotes: string[]; - ambiguousSources: string[]; - notes: string[]; -} - -interface DocumentationSignals { - docFiles: string[]; - insights: DocumentationInsight[]; - domainEntries: DomainInventoryEntry[]; - actorEntries: string[]; - ruleEntries: string[]; - flowEntries: string[]; - decisionEntries: string[]; - pending: string[]; - summary: string[]; - authority: SourceAuthoritySignals; -} - -interface ContextLiteDocumentSet { - systemOverview: string; - domainInventory: string; - modulesMap: string; - frontendArchitecture: string; - backendFlowsAndContracts: string; - uiRules: string; - masterContextPrompt: string; - decisionsBlock: string; - learningsBlock: string; - tasksBlock: string; - summary: string[]; - openQuestions: string[]; -} - -const GENERATED_START = ""; -const GENERATED_END = ""; - -const UI_DEPENDENCY_MAP = new Map([ - ["tailwindcss", "Tailwind CSS"], - ["@radix-ui/", "Radix UI"], - ["@mui/", "Material UI"], - ["@chakra-ui/", "Chakra UI"], - ["antd", "Ant Design"], - ["bootstrap", "Bootstrap"], - ["styled-components", "styled-components"], - ["@emotion/", "Emotion"], - ["shadcn", "shadcn/ui"] -]); - -const STATE_DEPENDENCY_MAP = new Map([ - ["zustand", "Zustand"], - ["redux", "Redux"], - ["@reduxjs/toolkit", "Redux Toolkit"], - ["jotai", "Jotai"], - ["mobx", "MobX"] -]); - -const DATA_FETCHING_DEPENDENCY_MAP = new Map([ - ["@tanstack/react-query", "TanStack Query"], - ["react-query", "React Query"], - ["swr", "SWR"], - ["apollo", "Apollo"], - ["urql", "urql"] -]); - -const I18N_DEPENDENCY_MAP = new Map([ - ["i18next", "i18next"], - ["react-i18next", "react-i18next"], - ["next-intl", "next-intl"], - ["next-i18next", "next-i18next"] -]); - -const AUTH_DEPENDENCY_MAP = new Map([ - ["next-auth", "NextAuth"], - ["@auth/", "Auth.js"], - ["clerk", "Clerk"], - ["auth0", "Auth0"], - ["lucia", "Lucia"], - ["passport", "Passport"], - ["jsonwebtoken", "JWT"], - ["express-session", "Express session"] -]); - -const VALIDATION_DEPENDENCY_MAP = new Map([ - ["zod", "Zod"], - ["yup", "Yup"], - ["joi", "Joi"], - ["ajv", "AJV"], - ["class-validator", "class-validator"], - ["pydantic", "Pydantic"] -]); - -const DATA_DEPENDENCY_MAP = new Map([ - ["prisma", "Prisma"], - ["drizzle-orm", "Drizzle"], - ["typeorm", "TypeORM"], - ["sequelize", "Sequelize"], - ["mongoose", "Mongoose"], - ["knex", "Knex"], - ["supabase", "Supabase"], - ["firebase", "Firebase"] -]); - -const QUEUE_DEPENDENCY_MAP = new Map([ - ["bullmq", "BullMQ"], - ["bull", "Bull"], - ["agenda", "Agenda"], - ["pg-boss", "pg-boss"], - ["kafkajs", "KafkaJS"], - ["amqplib", "AMQP"] -]); - -const DOCUMENT_EXTENSIONS = /\.(md|mdx|txt)$/i; -const ACTOR_KEYWORDS = [ - "admin", - "administrator", - "operator", - "moderator", - "manager", - "user", - "customer", - "client", - "member", - "vendor", - "merchant", - "seller", - "business", - "guide", - "owner", - "staff", - "guest", - "public" -]; -const NAVIGATION_SCOPE_MAP = new Map([ - ["admin", "admin"], - ["administrator", "admin"], - ["moderator", "moderator"], - ["operator", "operator"], - ["user", "user"], - ["customer", "customer"], - ["vendor", "vendor"], - ["business", "business"], - ["guide", "guide"], - ["creator", "creator"], - ["public", "public"] -]); -const DOMAIN_SURFACE_GENERIC_TOKENS = new Set([ - "dashboard", - "system", - "module", - "feature", - "features", - "technical", - "engineering", - "current", - "final", - "manual", - "testing", - "tests", - "global", - "public", - "private", - "admin" -]); -const DOMAIN_ALIAS_MAP = new Map([ - ["access-control", ["access", "auth", "permission", "permissions", "session", "role", "roles", "rbac", "acl", "guard", "login"]], - ["authentication", ["auth", "login", "session", "token", "identity"]], - ["authorization", ["auth", "permission", "permissions", "role", "roles", "access", "rbac", "acl", "guard"]], - ["events", ["event", "events", "registration", "registrations", "announcement", "announcements", "attachment", "attachments", "resource", "resources"]], - ["profiles", ["profile", "profiles", "guide-profile", "business-profile", "public-profile", "user-profile", "privacy", "username", "certifications"]], - ["reports", ["report", "reports", "moderation", "flag", "flags"]], - ["audit", ["audit", "auditlog", "log", "logs", "history"]], - ["tags", ["tag", "tags"]], - ["reviews", ["review", "reviews", "rating", "ratings"]], - ["routes", ["route", "routes", "gpx", "waypoint", "waypoints"]], - ["community", ["community", "member", "members", "social"]], - ["explore", ["explore", "featured", "discover"]], - ["notifications", ["notification", "notifications", "notify", "dispatch", "message", "messages", "inbox"]], - ["vendor-dashboard", ["vendor", "dashboard", "listing", "profile", "service", "services"]], - ["business-dashboard", ["business", "dashboard", "branch", "branches", "service", "services", "analytics"]], - ["admin-dashboard", ["admin", "dashboard", "backoffice", "moderation"]], - ["customer-dashboard", ["customer", "dashboard", "profile", "account"]] -]); -const DOMAIN_STOPWORDS = new Set([ - "docs", - "doc", - "readme", - "feature", - "features", - "technical", - "engineering", - "api", - "apis", - "flow", - "flows", - "definition", - "definitions", - "decision", - "decisions", - "testing", - "test", - "tests", - "verify", - "verification", - "audit", - "final", - "current", - "schema", - "design", - "manual", - "status", - "architecture", - "app", - "src", - "page", - "layout", - "route", - "routes", - "dashboard", - "public", - "components", - "component" -]); -const NAV_TITLE_STOPWORDS = /\b(sidebar|nav|navigation|menu|topbar|sheet|layout|mobile)\b/i; -const ROOT_OPERATIONAL_DOC_PATTERN = /(^|\/)(README|API|ARCHITECTURE|FLOWS|BUSINESS_RULES)\.(md|mdx|txt)$/i; -const AUTHORITY_SECTION_TITLE_PATTERN = - /\b(scope(?:\s*&\s*|\s+and\s+)sources?|primary sources?|fuentes primarias?|source of truth|fuente de verdad|authority|canon)\b/i; -const ROOT_SECTION_DOMAIN_MAP: Array<{ pattern: RegExp; domainKey: string }> = [ - { pattern: /^events?$/i, domainKey: "events" }, - { pattern: /^profiles?$/i, domainKey: "profiles" }, - { pattern: /^guide profiles?$/i, domainKey: "profiles" }, - { pattern: /^business profiles?$/i, domainKey: "profiles" }, - { pattern: /^privacy(?:\s*\(.*profiles?\))?$/i, domainKey: "profiles" }, - { pattern: /^public user profile$/i, domainKey: "profiles" }, - { pattern: /^reportes?$/i, domainKey: "reports" }, - { pattern: /^reports?$/i, domainKey: "reports" }, - { pattern: /^auditor[ií]a$/i, domainKey: "audit" }, - { pattern: /^audit$/i, domainKey: "audit" }, - { pattern: /^tags?$/i, domainKey: "tags" }, - { pattern: /^notifications?$/i, domainKey: "notifications" }, - { pattern: /^reviews?$/i, domainKey: "reviews" }, - { pattern: /^routes?$/i, domainKey: "routes" }, - { pattern: /^community$/i, domainKey: "community" }, - { pattern: /^explore$/i, domainKey: "explore" }, - { pattern: /^business(?: dashboard)?$/i, domainKey: "business-dashboard" }, - { pattern: /^academy$/i, domainKey: "academy" }, - { pattern: /^badges?$/i, domainKey: "badges" } -]; - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None confirmed"; -} - -function toEvidence(paths: string[]): string { - return paths.length > 0 ? ` Evidence: ${paths.map((filePath) => `\`${filePath}\``).join(", ")}.` : ""; -} - -function sample(items: string[], limit = 5): string[] { - return items.slice(0, limit); -} - -function isGeneratedContextPath(filePath: string): boolean { - return /^(AI_CONTEXT|reports|memory|tasks)\//i.test(filePath); -} - -function sampleSourceEvidence(items: string[], limit = 5): string[] { - return sample( - items.filter((filePath) => !isGeneratedContextPath(filePath)), - limit - ); -} - -function uniquePreserved(items: string[]): string[] { - const seen = new Set(); - const unique: string[] = []; - - for (const item of items) { - if (!item || seen.has(item)) { - continue; - } - - seen.add(item); - unique.push(item); - } - - return unique; -} - -function flattenDependencies(discovery: DiscoveryResult): string[] { - return uniqueSorted(discovery.dependencies.flatMap((manifest) => manifest.dependencies)); -} - -function flattenLowerDependencies(discovery: DiscoveryResult): string[] { - return flattenDependencies(discovery).map((dependency) => dependency.toLowerCase()); -} - -function matchingDependencies(flatDependencies: string[], mapping: Map): string[] { - const matches = new Set(); - - for (const dependency of flatDependencies) { - for (const [needle, label] of mapping.entries()) { - if (dependency.includes(needle)) { - matches.add(label); - } - } - } - - return [...matches].sort((left, right) => left.localeCompare(right)); -} - -function matchingFiles(discovery: DiscoveryResult, pattern: RegExp, limit = 20): string[] { - return discovery.files.filter((filePath) => pattern.test(filePath)).slice(0, limit); -} - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function normalizeDocText(value: string): string { - return value - .replace(/\[(.*?)\]\([^)]*\)/g, "$1") - .replace(/[`*_>#]/g, "") - .replace(/\s+/g, " ") - .trim(); -} - -function toSlug(value: string): string { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); -} - -function humanizeLabel(value: string): string { - return value - .split(/[-_]+/) - .filter(Boolean) - .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) - .join(" "); -} - -function tokenizeSearchTerms(value: string): string[] { - return value - .split(/[^a-z0-9]+/i) - .map((token) => token.trim().toLowerCase()) - .filter((token) => token.length >= 3 && !DOMAIN_STOPWORDS.has(token)); -} - -function countMatchedDomainTokens(filePath: string, tokens: string[]): { specific: number; generic: number } { - let specific = 0; - let generic = 0; - const searchablePath = filePath.replace(/([a-z0-9])([A-Z])/g, "$1-$2"); - - for (const token of tokens) { - if ( - token === "route" && - /\/route\.(ts|tsx|js|jsx)$/i.test(searchablePath) && - !/(^|[\\/()_.-])routes([\\/()_.-]|$)/i.test(searchablePath) - ) { - continue; - } - - if (!new RegExp(`(^|[\\\\/()_.-])${escapeRegExp(token)}([\\\\/()_.-]|$)`, "i").test(searchablePath)) { - continue; - } - - if (DOMAIN_SURFACE_GENERIC_TOKENS.has(token)) { - generic += 1; - } else { - specific += 1; - } - } - - return { specific, generic }; -} - -function scoreDomainSurface(filePath: string, tokens: string[], domainLabel?: string): number { - if (/^(AI_CONTEXT|reports|memory|tasks)\//i.test(filePath) || /(^|\/)(docs|doc)\//i.test(filePath)) { - return -100; - } - - if (/\.(md|mdx|txt)$/i.test(filePath)) { - return -60; - } - - const matches = countMatchedDomainTokens(filePath, tokens); - const hasSpecificTokenSet = tokens.some((token) => !DOMAIN_SURFACE_GENERIC_TOKENS.has(token)); - if (matches.specific === 0 && matches.generic === 0) { - return -100; - } - if (hasSpecificTokenSet && matches.specific === 0) { - return -100; - } - - let score = matches.specific * 10 + matches.generic * 3; - - if (/(^|\/)(app\/src|src)\/app\/api\/.+\/route\.(ts|tsx|js|jsx)$/i.test(filePath)) { - score += 16; - } - if (/(^|\/)(app\/src|src)\/app\/.+\/(page|layout)\.(ts|tsx|js|jsx|mdx)$/i.test(filePath)) { - score += 14; - } - if (/(^|\/)(app\/src|src)\/(components|lib|services|modules|actions|hooks|features)\//i.test(filePath)) { - score += 12; - } - if (/(^|\/)(app\/src|src)\/(app|components|lib|services|modules|actions|hooks|features)\//i.test(filePath)) { - score += 8; - } - if (/(^|\/)app\/prisma\/schema\.prisma$/i.test(filePath)) { - score += 5; - } - if (/\.(ts|tsx|js|jsx)$/i.test(filePath)) { - score += 4; - } - if (/\.(prisma|sql)$/i.test(filePath)) { - score += 1; - } - - if (/(^|\/)(tests?|spec)\//i.test(filePath)) { - score -= 6; - } - if (/(^|\/)(scripts?)\//i.test(filePath)) { - score -= 5; - } - if (/(^|\/)app\/prisma\/migrations\//i.test(filePath)) { - score -= 6; - } - if (/(^|\/)app\/prisma\/seeds?\//i.test(filePath)) { - score -= 5; - } - if (/(^|\/)app\/backups\//i.test(filePath)) { - score -= 12; - } - if (/\.(json|ya?ml)$/i.test(filePath)) { - score -= 8; - } - - if (domainLabel === "profiles") { - if (/(^|\/)(app\/src|src)\/app\/api\/profile\/(guide|business|public)\/route\.(ts|tsx|js|jsx)$/i.test(filePath)) { - score += 30; - } - if (/(^|\/)(app\/src|src)\/app\/api\/profile\/(guide|business)\/(activate|status|update)\/route\.(ts|tsx|js|jsx)$/i.test(filePath)) { - score += 28; - } - if (/(^|\/)(app\/src|src)\/app\/api\/profile\//i.test(filePath)) { - score += 24; - } - if (/(^|\/)(app\/src|src)\/app\/api\/users\/\[username\]\/public\/route\.(ts|tsx|js|jsx)$/i.test(filePath)) { - score += 22; - } - if (/(^|\/)(app\/src|src)\/services\/(guide|business).*profile.*service\.(ts|tsx|js|jsx)$/i.test(filePath)) { - score += 20; - } - if (/(^|\/)(app\/src|src)\/services\/public.*profile.*service\.(ts|tsx|js|jsx)$/i.test(filePath)) { - score += 20; - } - if (/(^|\/)(app\/src|src)\/components\/(guide|business|user|profile)\/.*profile/i.test(filePath)) { - score += 20; - } - if (/(^|\/)(app\/src|src)\/components\/profile\//i.test(filePath)) { - score += 18; - } - if (/(^|\/)(app\/src|src)\/app\/.+\/(profile|public-profile|privacy)\/page\.(ts|tsx|js|jsx|mdx)$/i.test(filePath)) { - score += 18; - } - if (/(^|\/)(app\/src|src)\/app\/.+\/u\/\[username\]\/page\.(ts|tsx|js|jsx|mdx)$/i.test(filePath)) { - score += 18; - } - if (/(^|\/)(app\/src|src)\/app\/.+\/profiles\//i.test(filePath)) { - score += 14; - } - if (/(^|\/)(app\/src|src)\/(app\/components|components)\/routes?\//i.test(filePath)) { - score -= 18; - } - if (/(^|\/)(app\/src|src)\/(components|app\/components)\/admin\//i.test(filePath) && !/profile/i.test(filePath)) { - score -= 10; - } - if (/(^|\/)(app\/src|src)\/lib\/auth\//i.test(filePath) && !/profile/i.test(filePath)) { - score -= 8; - } - } - - return score; -} - -function pickBalancedDomainFiles(candidates: Array<{ filePath: string; score: number }>, limit: number): string[] { - const selected: string[] = []; - const seenGroups = new Set(); - - for (const candidate of candidates) { - const group = inferPrimarySurfaceGroup(candidate.filePath); - if (seenGroups.has(group)) { - continue; - } - selected.push(candidate.filePath); - seenGroups.add(group); - if (selected.length >= limit) { - return selected; - } - } - - for (const candidate of candidates) { - if (selected.includes(candidate.filePath)) { - continue; - } - selected.push(candidate.filePath); - if (selected.length >= limit) { - break; - } - } - - return selected; -} - -function parseReadmeSummary(content: string): string | undefined { - const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); - - for (const rawLine of lines) { - if (/^(!\[|\[!\[|---+$)/.test(rawLine) || /^https?:\/\//i.test(rawLine)) { - continue; - } - - const normalized = normalizeDocText(rawLine.replace(/^#{1,6}\s+/, "")); - if ( - !normalized || - normalized.length < 8 || - /^(change log|license|manual|comunity|community|english|espa[ñn]ol)$/i.test(normalized) || - /\b(kumbiaphp logo|welcome to kumbiaphp|fast and easy php framework|scrutinizer|code climate|php[57]\s+ready)\b/i.test(normalized) - ) { - continue; - } - - return normalized.replace(/\s+/g, " ").trim(); - } - - return undefined; -} - -async function loadRepoMetadata(context: ProjectContext): Promise { - const packageJson = await readJsonSafe(path.join(context.targetPath, "package.json")); - const readme = - (await readTextSafe(path.join(context.targetPath, "README.md"))) || - (await readTextSafe(path.join(context.targetPath, "README.mdx"))) || - (await readTextSafe(path.join(context.targetPath, "README.txt"))); - - return { - readmeSummary: parseReadmeSummary(readme), - scripts: Object.keys(packageJson?.scripts ?? {}).sort((left, right) => left.localeCompare(right)) - }; -} - -function inferActorScopesFromPath(filePath: string): string[] { - const lowerPath = filePath.toLowerCase(); - const scopes = new Set(); - - for (const [needle, label] of NAVIGATION_SCOPE_MAP.entries()) { - if (lowerPath.includes(needle)) { - scopes.add(label); - } - } - - return [...scopes].sort((left, right) => left.localeCompare(right)); -} - -function inferPrimarySurfaceGroup(filePath: string): string { - const normalized = filePath.toLowerCase(); - - if (normalized.includes("dashboard-root")) { - return "dashboard-root"; - } - if (normalized.includes("(dashboard-user)") || normalized.includes("/user/")) { - return "user"; - } - if (normalized.includes("(dashboard-business)") || normalized.includes("/business/")) { - return "business"; - } - if (normalized.includes("(dashboard-guide)") || normalized.includes("/guide/")) { - return "guide"; - } - if (normalized.includes("(dashboard-creator)") || normalized.includes("/creator/")) { - return "creator"; - } - if (normalized.includes("(dashboard-admin)") || normalized.includes("/admin/")) { - return "admin"; - } - if (normalized.includes("(public)") || normalized.includes("public")) { - return "public"; - } - if (normalized.includes("(auth)") || normalized.includes("/auth/")) { - return "auth"; - } - if (/(topbar|footer|bottomnav|navsheet|switcher|shell)/i.test(filePath)) { - return "shared-shell"; - } - - return inferActorScopesFromPath(filePath)[0] ?? "general"; -} - -function scoreRepresentativeSurface(filePath: string): number { - let score = 0; - - if (/dashboard-root/i.test(filePath)) { - score += 20; - } - if (/\(dashboard-user\)|\/user\//i.test(filePath)) { - score += 18; - } - if (/\(dashboard-business\)|\/business\//i.test(filePath)) { - score += 17; - } - if (/\(dashboard-guide\)|\/guide\//i.test(filePath)) { - score += 16; - } - if (/\(dashboard-creator\)|\/creator\//i.test(filePath)) { - score += 15; - } - if (/\(dashboard-admin\)|\/admin\//i.test(filePath)) { - score += 14; - } - if (/\(public\)|public/i.test(filePath)) { - score += 13; - } - if (/\(auth\)|\/auth\//i.test(filePath)) { - score += 12; - } - if (/(Topbar|Footer|BottomNav|NavSheet|Switcher|Shell)/i.test(filePath)) { - score += 11; - } - if (/(Sidebar|Navigation|NavConfig|navConfig|Menu|MobileNav)/i.test(filePath)) { - score += 8; - } - if (/\/layout\.(ts|tsx|js|jsx|mdx)$/i.test(filePath)) { - score += 6; - } - if (/\/page\.(ts|tsx|js|jsx|mdx)$/i.test(filePath)) { - score += 5; - } - - return score; -} - -function pickRepresentativeFiles(files: string[], limit: number): string[] { - const sorted = uniqueSorted(files).sort( - (left, right) => scoreRepresentativeSurface(right) - scoreRepresentativeSurface(left) || left.localeCompare(right) - ); - const selected: string[] = []; - const seenGroups = new Set(); - - for (const filePath of sorted) { - const group = inferPrimarySurfaceGroup(filePath); - if (seenGroups.has(group)) { - continue; - } - selected.push(filePath); - seenGroups.add(group); - if (selected.length >= limit) { - return selected; - } - } - - for (const filePath of sorted) { - if (selected.includes(filePath)) { - continue; - } - selected.push(filePath); - if (selected.length >= limit) { - break; - } - } - - return selected; -} - -const IGNORED_MVC_DOMAIN_LABELS = new Set(["empty", "index", "pages", "blanco", "shared"]); - -function normalizeDomainLabel(value: string): string { - return toSlug(value.replace(/_controller$/i, "")); -} - -function inferPhpMvcDomain(filePath: string): { label: string; role: "controller" | "model" | "view" } | undefined { - const normalized = filePath.replace(/\\/g, "/"); - const controller = normalized.match(/(^|\/)(?:default\/)?app\/controllers\/([^/]+)_controller\.php$/i); - if (controller?.[2]) { - const label = normalizeDomainLabel(controller[2]); - return label && !IGNORED_MVC_DOMAIN_LABELS.has(label) ? { label, role: "controller" } : undefined; - } - - const model = normalized.match(/(^|\/)(?:default\/)?app\/models\/([^/]+)\.php$/i); - if (model?.[2]) { - const label = normalizeDomainLabel(model[2]); - return label && !IGNORED_MVC_DOMAIN_LABELS.has(label) && !label.startsWith("bak-") ? { label, role: "model" } : undefined; - } - - const view = normalized.match(/(^|\/)(?:default\/)?app\/views\/([^/]+)\//i); - if (view?.[2]) { - const label = normalizeDomainLabel(view[2]); - return label && !IGNORED_MVC_DOMAIN_LABELS.has(label) && !label.startsWith("_") ? { label, role: "view" } : undefined; - } - - return undefined; -} - -function inferPhpMvcDomainEntries(discovery: DiscoveryResult): DomainInventoryEntry[] { - const domains = new Map(); - const roles = new Map>(); - - for (const filePath of discovery.files) { - const domain = inferPhpMvcDomain(filePath); - if (!domain) { - continue; - } - - const entry = domains.get(domain.label) ?? { - label: domain.label, - docFiles: [], - codeFiles: [], - highlights: [] - }; - entry.codeFiles = uniqueSorted([...entry.codeFiles, filePath]).slice(0, 8); - domains.set(domain.label, entry); - - const roleSet = roles.get(domain.label) ?? new Set(); - roleSet.add(domain.role); - roles.set(domain.label, roleSet); - } - - for (const entry of domains.values()) { - const roleLabels = [...(roles.get(entry.label) ?? new Set())].sort(); - entry.highlights = [ - `Dominio inferido desde MVC PHP/Kumbia (${roleLabels.join(", ") || "superficie"})` - ]; - } - - return [...domains.values()].sort((left, right) => left.label.localeCompare(right.label)); -} - -function normalizeNavLabel(value: string): string | undefined { - const normalized = normalizeDocText(value).replace(/^[/:.-]+|[/:.-]+$/g, "").trim(); - if (!normalized || normalized.length < 2 || normalized.length > 48) { - return undefined; - } - - if (/^\/|^[A-Z0-9_/-]+$/.test(normalized)) { - return undefined; - } - - return normalized; -} - -async function detectNavigationSignals(context: ProjectContext): Promise { - const navFiles = uniqueSorted([ - ...matchingFiles(context.discovery, /(^|\/).*(Sidebar|Topbar|Footer|BottomNav|NavSheet|Switcher|Shell|Nav|Navigation|Menu|NavConfig|navConfig|MobileNav).*\.(ts|tsx|js|jsx)$/i, 40), - ...matchingFiles(context.discovery, /(^|\/).*(sidebar|topbar|footer|bottomnav|navsheet|switcher|shell|nav|navigation|menu).*\.(ts|tsx|js|jsx)$/i, 40) - ]).slice(0, 30); - const guardFiles = uniqueSorted([ - ...matchingFiles(context.discovery, /(^|\/)(src\/)?app\/.+\/layout\.(ts|tsx|js|jsx)$/i, 20), - ...matchingFiles(context.discovery, /(^|\/)(auth|session|permissions?|roles?|access|acl|rbac).*\.(ts|tsx|js|jsx)$/i, 20) - ]).slice(0, 30); - const permissionFiles = uniqueSorted( - matchingFiles(context.discovery, /(^|\/).*(permissions?|roles?|access|acl|rbac).*\.(ts|tsx|js|jsx)$/i, 20) - ); - const navLabels = new Set(); - const navSectionLabels = new Set(); - const actorScopes = new Set(); - const guardSummaries = new Set(); - - for (const filePath of navFiles) { - for (const scope of inferActorScopesFromPath(filePath)) { - actorScopes.add(scope); - } - - const content = await readTextSafe(path.join(context.targetPath, filePath)); - if (!content) { - continue; - } - - for (const match of content.matchAll(/\b(label|title|name)\s*:\s*["'`]([^"'`]+)["'`]/g)) { - const field = String(match[1]).toLowerCase(); - const label = normalizeNavLabel(String(match[2])); - if (!label) { - continue; - } - - if (field === "title" || NAV_TITLE_STOPWORDS.test(label)) { - navSectionLabels.add(label); - } else { - navLabels.add(label); - } - } - } - - for (const filePath of guardFiles) { - for (const scope of inferActorScopesFromPath(filePath)) { - actorScopes.add(scope); - } - - const content = await readTextSafe(path.join(context.targetPath, filePath)); - if (!content) { - continue; - } - - const facts: string[] = []; - if (/redirect\(\s*["'`][^"'`]+["'`]\s*\)/.test(content)) { - facts.push("redirect"); - } - if (/\b(getCurrentUser|getSession|requireAuth|useAuth)\b/.test(content)) { - facts.push("session"); - } - if (/\b(hasPermission|requirePermission|permissions?\b|CAN_[A-Z_]+|roleRelation)\b/.test(content)) { - facts.push("permission-check"); - } - if (/\b(requireRole|user\.role\b|roles\b|role\b)\b/.test(content)) { - facts.push("role-check"); - } - if (/\b(can[A-Z][A-Za-z]+|guideProfile|businessProfile|status\s*===\s*["'`]APPROVED["'`]|profileVisible)\b/.test(content)) { - facts.push("capability-check"); - } - - if (facts.length > 0) { - guardSummaries.add(`${filePath}: ${uniqueSorted(facts).join(", ")}`); - } - } - - return { - navFiles, - navLabels: [...navLabels].sort((left, right) => left.localeCompare(right)).slice(0, 12), - navSectionLabels: [...navSectionLabels].sort((left, right) => left.localeCompare(right)).slice(0, 8), - actorScopes: [...actorScopes].sort((left, right) => left.localeCompare(right)), - guardFiles: guardFiles.slice(0, 12), - guardSummaries: [...guardSummaries].sort((left, right) => left.localeCompare(right)).slice(0, 10), - permissionFiles - }; -} - -function documentationCandidateFiles(discovery: DiscoveryResult): string[] { - return uniqueSorted( - discovery.files.filter( - (filePath) => - !/^(AI_CONTEXT|reports|memory|tasks)\//i.test(filePath) && - DOCUMENT_EXTENSIONS.test(filePath) && - (/^README\.(md|mdx|txt)$/i.test(path.basename(filePath)) || - /(^|\/)(docs|doc)\//i.test(filePath) || - /(^|\/)(API|ARCHITECTURE|FLOWS|BUSINESS_RULES)\.(md|mdx|txt)$/i.test(filePath) || - /(^|\/)(SECURITY|CONTRIBUTING|CHANGELOG)\.(md|mdx|txt)$/i.test(filePath) || - /\/README\.(md|mdx|txt)$/i.test(filePath)) - ) - ).slice(0, 80); -} - -function extractParagraphs(content: string): string[] { - const paragraphs: string[] = []; - const lines = content.split(/\r?\n/); - let current: string[] = []; - let inCodeBlock = false; - - const flush = () => { - if (current.length === 0) { - return; - } - - const normalized = normalizeDocText(current.join(" ")); - if (normalized) { - paragraphs.push(normalized); - } - current = []; - }; - - for (const rawLine of lines) { - const line = rawLine.trim(); - - if (line.startsWith("```")) { - inCodeBlock = !inCodeBlock; - flush(); - continue; - } - - if (inCodeBlock) { - continue; - } - - if (!line) { - flush(); - continue; - } - - if (/^#{1,6}\s+/.test(line) || /^[-*]\s+/.test(line) || /^\d+[.)]\s+/.test(line) || /^\|/.test(line)) { - flush(); - continue; - } - - current.push(line); - } - - flush(); - return paragraphs; -} - -function extractBulletLines(content: string): string[] { - return uniquePreserved( - content - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => /^[-*]\s+/.test(line) || /^\d+[.)]\s+/.test(line)) - .map((line) => normalizeDocText(line.replace(/^[-*]\s+|^\d+[.)]\s+/, ""))) - .filter(Boolean) - ); -} - -function extractHeadingLines(content: string): string[] { - return uniquePreserved( - content - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => /^#{1,6}\s+/.test(line)) - .map((line) => normalizeDocText(line.replace(/^#{1,6}\s+/, ""))) - .filter(Boolean) - ); -} - -function extractHeadingSections(content: string): Array<{ title: string; level: number; body: string }> { - const sections: Array<{ title: string; level: number; body: string }> = []; - const lines = content.split(/\r?\n/); - - for (let index = 0; index < lines.length; index += 1) { - const match = lines[index]?.match(/^(#{1,6})\s+(.+?)\s*$/); - if (!match) { - continue; - } - - const level = match[1].length; - const title = normalizeDocText(match[2] ?? ""); - if (!title) { - continue; - } - - const bodyLines: string[] = []; - for (let cursor = index + 1; cursor < lines.length; cursor += 1) { - const nextMatch = lines[cursor]?.match(/^(#{1,6})\s+(.+?)\s*$/); - if (nextMatch && nextMatch[1].length <= level) { - break; - } - bodyLines.push(lines[cursor] ?? ""); - } - - sections.push({ - title, - level, - body: bodyLines.join("\n").trim() - }); - } - - return sections; -} - -function inferRootSectionDomainKey(title: string): string | undefined { - for (const entry of ROOT_SECTION_DOMAIN_MAP) { - if (entry.pattern.test(title.trim())) { - return entry.domainKey; - } - } - - return undefined; -} - -function extractStatusNote(filePath: string, content: string): string | undefined { - const line = content - .split(/\r?\n/) - .map((entry) => entry.trim()) - .find((entry) => /(?:\*\*)?(estado|status)(?:\*\*)?\s*:/.test(entry.toLowerCase())); - - if (!line) { - return undefined; - } - - const [, value = ""] = line.split(/:/, 2); - const normalized = normalizeDocText(value); - if (!normalized) { - return undefined; - } - - if (/(draft|borrador|pendiente|wip|proposed|todo)/i.test(normalized)) { - return `\`${filePath}\` está marcado como ${normalized}.`; - } - - return undefined; -} - -function inferDomainKeyFromDocPath(filePath: string): string | undefined { - const normalized = filePath.replace(/\\/g, "/"); - let match = normalized.match(/^docs\/([^/]+)\//i); - if (match?.[1]) { - return toSlug(match[1]); - } - - match = normalized.match(/^app\/docs\/FEATURES\/([^/]+)\.(md|mdx|txt)$/i); - if (match?.[1]) { - return toSlug(match[1]); - } - - match = normalized.match(/^app\/docs\/technical\/([^/]+)\.(md|mdx|txt)$/i); - if (match?.[1]) { - return toSlug(match[1]); - } - - match = normalized.match(/^app\/docs\/([^/]+)\//i); - if (match?.[1] && !/^(technical|features)$/i.test(match[1])) { - return toSlug(match[1]); - } - - if (/\/README\.(md|mdx|txt)$/i.test(normalized)) { - const parentDir = path.basename(path.dirname(normalized)); - if (parentDir && !/^(docs|app)$/i.test(parentDir)) { - return toSlug(parentDir); - } - } - - return undefined; -} - -function extractActorHighlights(headings: string[], bullets: string[], paragraphs: string[]): string[] { - const actorPattern = new RegExp(`\\b(${ACTOR_KEYWORDS.join("|")})\\b`, "i"); - const allowParagraphs = headings.some((entry) => /\b(actor|actors|roles|actores|alcance)\b/i.test(entry)); - if (!allowParagraphs) { - return []; - } - - const isEndpointLike = (entry: string): boolean => - /^(get|post|put|patch|delete)\b/i.test(entry) || - /\/api\//i.test(entry) || - (/\b(api|endpoint)\b/i.test(entry) && entry.includes("/")); - - const actorHeadings = headings.filter( - (entry) => - actorPattern.test(entry) && - entry.split(/\s+/).length <= 3 && - !isEndpointLike(entry) && - !/[():]/.test(entry) && - !/\b(decision|decisions|definition|principles|objectives|states|rules|overview|summary|dashboard|module|profile|system|portal)\b/i.test(entry) - ); - const shortBulletLabels = bullets.filter( - (entry) => - actorPattern.test(entry) && - entry.length <= 32 && - !isEndpointLike(entry) && - !/[.!?():]/.test(entry) && - entry.split(/\s+/).length <= 3 - ); - const candidates = [...actorHeadings, ...shortBulletLabels]; - return uniquePreserved( - candidates.filter((entry) => actorPattern.test(entry) && entry.length <= 40 && !isEndpointLike(entry)) - ).slice(0, 6); -} - -function collectKeywordHighlights( - descriptor: string, - bullets: string[], - paragraphs: string[], - keywords: string[] -): string[] { - const lowerDescriptor = descriptor.toLowerCase(); - const pattern = new RegExp(`\\b(${keywords.map((keyword) => escapeRegExp(keyword)).join("|")})\\b`, "i"); - const preferred = [...bullets, ...paragraphs].filter((entry) => pattern.test(entry)); - - if (preferred.length > 0) { - return uniquePreserved(preferred).slice(0, 4); - } - - if (keywords.some((keyword) => lowerDescriptor.includes(keyword.toLowerCase()))) { - return uniquePreserved([...bullets, ...paragraphs]).slice(0, 4); - } - - return []; -} - -function parseDocumentationInsight(filePath: string, content: string): DocumentationInsight { - const headings = extractHeadingLines(content); - const bullets = extractBulletLines(content); - const paragraphs = extractParagraphs(content); - const title = headings[0] ?? humanizeLabel(path.parse(filePath).name); - const descriptor = `${filePath} ${title} ${headings.join(" ")}`; - - return { - filePath, - title, - domainKey: inferDomainKeyFromDocPath(filePath), - summary: paragraphs[0], - actorHighlights: extractActorHighlights(headings, bullets, paragraphs), - ruleHighlights: collectKeywordHighlights(descriptor, bullets, paragraphs, [ - "rule", - "regla", - "principle", - "principio", - "state", - "estado", - "error", - "access", - "scope", - "permission", - "policy" - ]), - flowHighlights: collectKeywordHighlights(descriptor, bullets, paragraphs, [ - "flow", - "flujo", - "workflow", - "journey", - "step", - "activation" - ]), - decisionHighlights: collectKeywordHighlights(descriptor, bullets, paragraphs, [ - "decision", - "decisiones", - "why", - "por qué", - "default", - "optional" - ]), - pendingNotes: extractStatusNote(filePath, content) ? [extractStatusNote(filePath, content) as string] : [] - }; -} - -function parseSyntheticDomainInsights(filePath: string, content: string): DocumentationInsight[] { - if (!ROOT_OPERATIONAL_DOC_PATTERN.test(filePath)) { - return []; - } - - const sections = extractHeadingSections(content); - - return sections.flatMap((section) => { - const domainKey = inferRootSectionDomainKey(section.title); - if (!domainKey) { - return []; - } - - const headings = [section.title, ...extractHeadingLines(section.body)]; - const bullets = extractBulletLines(section.body); - const paragraphs = extractParagraphs(section.body); - - return [ - { - filePath, - title: section.title, - domainKey, - summary: paragraphs[0] ?? bullets[0], - actorHighlights: extractActorHighlights(headings, bullets, paragraphs), - ruleHighlights: collectKeywordHighlights(`${filePath} ${section.title}`, bullets, paragraphs, [ - "rule", - "regla", - "policy", - "permission", - "state", - "status" - ]), - flowHighlights: collectKeywordHighlights(`${filePath} ${section.title}`, bullets, paragraphs, [ - "flow", - "flujo", - "workflow", - "step", - "draft", - "publish", - "registration" - ]), - decisionHighlights: collectKeywordHighlights(`${filePath} ${section.title}`, bullets, paragraphs, [ - "decision", - "default", - "fallback", - "must", - "debe" - ]), - pendingNotes: [] - } - ]; - }); -} - -function extractPathLikeReferences(content: string): string[] { - return uniquePreserved( - [...content.matchAll(/`([^`]+)`/g)] - .map((match) => normalizeDocText(match[1] ?? "")) - .filter((reference) => Boolean(reference) && (/[\\/]/.test(reference) || /\*/.test(reference) || /\.[a-z0-9]+$/i.test(reference))) - ); -} - -function extractAuthorityReferenceBlocks(content: string): string[] { - const blocks: string[] = []; - const sections = extractHeadingSections(content) - .filter((section) => AUTHORITY_SECTION_TITLE_PATTERN.test(section.title)) - .map((section) => section.body); - - blocks.push(...sections); - - const lines = content.split(/\r?\n/); - for (let index = 0; index < lines.length; index += 1) { - if (!/\b(primary sources?|fuentes primarias?|source of truth)\b/i.test(lines[index] ?? "")) { - continue; - } - blocks.push(lines.slice(index, index + 12).join("\n")); - } - - return uniquePreserved(blocks.filter(Boolean)); -} - -function normalizeDeclaredSourceReference(reference: string, discovery: DiscoveryResult): string | undefined { - const cleaned = reference.trim().replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, ""); - if (!cleaned || /^(get|post|put|patch|delete)\b/i.test(cleaned)) { - return undefined; - } - - const variations = new Set([cleaned]); - - if (!cleaned.startsWith("app/")) { - variations.add(`app/${cleaned}`); - } - if (cleaned.startsWith("src/")) { - variations.add(`app/${cleaned}`); - } - if (cleaned.startsWith("prisma/")) { - variations.add(`app/${cleaned}`); - } - if (cleaned.startsWith("app/src/")) { - variations.add(cleaned.replace(/^app\//, "")); - } - if (cleaned.startsWith("app/prisma/")) { - variations.add(cleaned.replace(/^app\//, "")); - } - - for (const candidate of variations) { - const isGlob = candidate.endsWith("/*"); - const prefix = isGlob ? candidate.slice(0, -1) : candidate; - - if (isGlob) { - if (discovery.files.some((filePath) => filePath.startsWith(prefix))) { - return candidate; - } - continue; - } - - if (discovery.files.includes(candidate)) { - return candidate; - } - - if (!/\.[a-z0-9]+$/i.test(candidate) && discovery.files.some((filePath) => filePath.startsWith(`${candidate}/`))) { - return `${candidate}/*`; - } - } - - return undefined; -} - -function extractDeclaredCanonicalSignals( - candidates: Array<{ filePath: string; content: string }>, - discovery: DiscoveryResult -): { sources: string[]; notes: string[] } { - const sources = new Set(); - const notes = new Set(); - - for (const candidate of candidates) { - if (!/\.mdx?$/i.test(candidate.filePath)) { - continue; - } - - const authorityBlocks = extractAuthorityReferenceBlocks(candidate.content); - - const references = uniquePreserved( - authorityBlocks.flatMap((block) => extractPathLikeReferences(block)) - ) - .map((reference) => normalizeDeclaredSourceReference(reference, discovery)) - .filter((reference): reference is string => Boolean(reference)); - - for (const reference of references) { - sources.add(reference); - } - - if (/rules below are extracted from live code only|live code only|c[oó]digo vivo|comportamiento real/i.test(candidate.content)) { - notes.add(`\`${candidate.filePath}\` declara que la referencia principal es el código vivo o el comportamiento real.`); - } - - if (references.length > 0 && /(primary sources?|fuentes primarias?|source of truth)/i.test(candidate.content)) { - notes.add(`\`${candidate.filePath}\` declara fuentes canónicas: ${references.map((reference) => `\`${reference}\``).join(", ")}.`); - } - - } - - return { - sources: uniqueSorted([...sources]).slice(0, 8), - notes: uniqueSorted([...notes]).slice(0, 8) - }; -} - -function scoreAuthoritySource(filePath: string, content: string): number { - let score = 0; - const normalized = filePath.replace(/\\/g, "/"); - - if (/^app\/docs\/README\.(md|mdx|txt)$/i.test(normalized)) { - score = 100; - } else if (/^app\/API\.(md|mdx|txt)$/i.test(normalized)) { - score = 98; - } else if (/^app\/BUSINESS_RULES\.(md|mdx|txt)$/i.test(normalized)) { - score = 97; - } else if (/^app\/FLOWS\.(md|mdx|txt)$/i.test(normalized)) { - score = 96; - } else if (/^app\/docs\/technical\/ACCESS_CONTROL\.(md|mdx|txt)$/i.test(normalized)) { - score = 95; - } else if (/^app\/ARCHITECTURE\.(md|mdx|txt)$/i.test(normalized)) { - score = 93; - } else if (/^docs\/[^/]+\/(api|flows|decisions|README)\.(md|mdx|txt)$/i.test(normalized)) { - score = 90; - } else if (/^README\.(md|mdx|txt)$/i.test(path.basename(normalized))) { - score = 82; - } else if (/schema\.prisma$/i.test(normalized)) { - score = 72; - } else if (/\.mdx?$/i.test(normalized)) { - score = 70; - } - - if (/Documento alineado al comportamiento real|updated to the real behavior|comportamiento real/i.test(content)) { - score += 2; - } - if (/^app\/ARCHITECTURE\.(md|mdx|txt)$/i.test(normalized) && /\b(runtime architecture|authentication & authorization|service layer)\b/i.test(content)) { - score += 2; - } - - return score; -} - -function collectAmbiguitySignals(filePath: string, content: string): string[] { - const notes: string[] = []; - - if (/^<<<<<<<|^=======|^>>>>>>>/m.test(content)) { - notes.push(`\`${filePath}\` contiene marcadores de conflicto; no tratarlo como fuente única hasta resolverlos.`); - } - - const statusNote = extractStatusNote(filePath, content); - if (statusNote) { - notes.push(statusNote); - } - - if (/Server Actions?/i.test(content) && /(instead of API routes|adem[aá]s de API routes)/i.test(content)) { - notes.push(`\`${filePath}\` documenta flujos con Server Actions además de API routes; revisar ambos contratos antes de cambiar backend.`); - } - - if (/\bSSOT\b|source of truth|fuente de verdad/i.test(content) && /(draft|borrador|wip|pendiente)/i.test(content)) { - notes.push(`\`${filePath}\` se presenta como fuente de verdad, pero está en draft o pendiente; no tratarlo como canon estable.`); - } - - if (/double source of truth|doble fuente de verdad/i.test(content)) { - notes.push(`\`${filePath}\` documenta múltiples fuentes de verdad activas; degradar confianza hasta resolver la duplicidad.`); - } - - return notes; -} - -function buildSourceAuthoritySignals( - candidates: Array<{ filePath: string; content: string }>, - discovery: DiscoveryResult -): SourceAuthoritySignals { - const scored = candidates - .map((candidate) => ({ - filePath: candidate.filePath, - score: scoreAuthoritySource(candidate.filePath, candidate.content), - ambiguities: collectAmbiguitySignals(candidate.filePath, candidate.content) - })) - .filter((candidate) => candidate.score > 0) - .sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath)); - - const primarySources = scored.filter((candidate) => candidate.score >= 95).map((candidate) => candidate.filePath).slice(0, 8); - const secondarySources = scored - .filter((candidate) => candidate.score >= 80 && candidate.score < 95) - .map((candidate) => candidate.filePath) - .slice(0, 8); - const declaredCanonical = extractDeclaredCanonicalSignals(candidates, discovery); - const ambiguousSources = uniqueSorted(scored.flatMap((candidate) => candidate.ambiguities)).slice(0, 8); - const notes: string[] = []; - - if (declaredCanonical.sources.length > 0) { - notes.push(`Fuentes canónicas declaradas: ${declaredCanonical.sources.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - if (primarySources.length > 0) { - notes.push(`Fuentes primarias detectadas: ${primarySources.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - if (secondarySources.length > 0) { - notes.push(`Fuentes secundarias útiles: ${secondarySources.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - if (declaredCanonical.notes.length > 0) { - notes.push(...declaredCanonical.notes); - } - if (ambiguousSources.length > 0) { - notes.push(...ambiguousSources); - } - - return { - primarySources, - secondarySources, - declaredCanonicalSources: declaredCanonical.sources, - authorityNotes: declaredCanonical.notes, - ambiguousSources, - notes - }; -} - -function scoreDomainLabel(label: string): number { - const normalized = label.toLowerCase(); - if (normalized === "events") return 20; - if (normalized === "reports") return 19; - if (normalized === "audit") return 18; - if (normalized === "profiles") return 17; - if (normalized === "business-dashboard") return 16; - if (normalized === "notifications") return 15; - if (normalized === "routes") return 15; - if (normalized === "academy") return 14; - if (normalized === "global-map") return 13; - return 0; -} - -function buildDomainSearchTokens(label: string, domainInsights: DocumentationInsight[]): string[] { - const titleTokens = domainInsights.flatMap((insight) => tokenizeSearchTerms(insight.title)); - - if (label === "profiles") { - return uniqueSorted([ - ...tokenizeSearchTerms(label), - ...titleTokens.filter((token) => /profile/.test(token)), - ...(DOMAIN_ALIAS_MAP.get(label) ?? []) - ]).filter((token) => token.length >= 3); - } - - return uniqueSorted([ - ...tokenizeSearchTerms(label), - ...titleTokens, - ...(DOMAIN_ALIAS_MAP.get(label) ?? []) - ]).filter((token) => token.length >= 3); -} - -function pickRepresentativeDomainLabels(entries: DomainInventoryEntry[], limit: number): string[] { - return [...entries] - .sort((left, right) => { - const scoreDelta = scoreDomainLabel(right.label) - scoreDomainLabel(left.label); - if (scoreDelta !== 0) { - return scoreDelta; - } - const docDelta = right.docFiles.length - left.docFiles.length; - if (docDelta !== 0) { - return docDelta; - } - return left.label.localeCompare(right.label); - }) - .slice(0, limit) - .map((entry) => entry.label); -} - -function buildDomainEntries(discovery: DiscoveryResult, insights: DocumentationInsight[]): DomainInventoryEntry[] { - const domainMap = new Map(); - const insightMap = new Map(); - const mergeEntry = (entry: DomainInventoryEntry): void => { - const existing = domainMap.get(entry.label) ?? { - label: entry.label, - docFiles: [], - codeFiles: [], - highlights: [] - }; - - existing.docFiles = uniqueSorted([...existing.docFiles, ...entry.docFiles]); - existing.codeFiles = uniqueSorted([...existing.codeFiles, ...entry.codeFiles]).slice(0, 8); - existing.highlights = uniquePreserved([...existing.highlights, ...entry.highlights]).slice(0, 4); - domainMap.set(entry.label, existing); - }; - - for (const insight of insights) { - if (!insight.domainKey) { - continue; - } - - const entry = domainMap.get(insight.domainKey) ?? { - label: insight.domainKey, - docFiles: [], - codeFiles: [], - highlights: [] - }; - - entry.docFiles = uniqueSorted([...entry.docFiles, insight.filePath]); - entry.highlights = uniquePreserved([ - ...entry.highlights, - insight.summary ?? "", - ...insight.flowHighlights, - ...insight.ruleHighlights, - ...insight.decisionHighlights - ]).slice(0, 4); - - domainMap.set(insight.domainKey, entry); - insightMap.set(insight.domainKey, [...(insightMap.get(insight.domainKey) ?? []), insight]); - } - - for (const entry of inferPhpMvcDomainEntries(discovery)) { - mergeEntry(entry); - } - - for (const entry of domainMap.values()) { - const domainInsights = insightMap.get(entry.label) ?? []; - const tokens = buildDomainSearchTokens(entry.label, domainInsights); - const existingCodeFiles = entry.codeFiles; - - if (domainInsights.length === 0) { - entry.codeFiles = existingCodeFiles; - continue; - } - - const rankedCandidates = discovery.files - .filter((filePath) => !/(^|\/)(docs|doc)\//i.test(filePath)) - .map((filePath) => ({ - filePath, - score: scoreDomainSurface(filePath, tokens, entry.label) - })) - .filter((candidate) => candidate.score > 0) - .sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath)); - - entry.codeFiles = uniqueSorted([...existingCodeFiles, ...pickBalancedDomainFiles(rankedCandidates, 6)]).slice(0, 8); - } - - return [...domainMap.values()].sort((left, right) => left.label.localeCompare(right.label)); -} - -async function buildDocumentationSignals(context: ProjectContext): Promise { - const docFiles = documentationCandidateFiles(context.discovery); - const insights: DocumentationInsight[] = []; - const authorityCandidates: Array<{ filePath: string; content: string }> = []; - - for (const filePath of docFiles) { - const content = await readTextSafe(path.join(context.targetPath, filePath)); - if (!content) { - continue; - } - - authorityCandidates.push({ filePath, content }); - insights.push(parseDocumentationInsight(filePath, content)); - insights.push(...parseSyntheticDomainInsights(filePath, content)); - } - - for (const filePath of matchingFiles(context.discovery, /(^|\/)prisma\/schema\.prisma$/i, 4)) { - const content = await readTextSafe(path.join(context.targetPath, filePath)); - if (!content) { - continue; - } - authorityCandidates.push({ filePath, content }); - } - - const domainEntries = buildDomainEntries(context.discovery, insights); - const actorEntries = uniquePreserved( - insights.flatMap((insight) => insight.actorHighlights.map((entry) => `${entry}${toEvidence([insight.filePath])}`)) - ).slice(0, 8); - const ruleEntries = uniquePreserved( - insights.flatMap((insight) => insight.ruleHighlights.map((entry) => `${entry}${toEvidence([insight.filePath])}`)) - ).slice(0, 8); - const flowEntries = uniquePreserved( - insights.flatMap((insight) => insight.flowHighlights.map((entry) => `${entry}${toEvidence([insight.filePath])}`)) - ).slice(0, 8); - const decisionEntries = uniquePreserved( - insights.flatMap((insight) => insight.decisionHighlights.map((entry) => `${entry}${toEvidence([insight.filePath])}`)) - ).slice(0, 8); - const pending = uniquePreserved(insights.flatMap((insight) => insight.pendingNotes)).slice(0, 8); - const summary = docFiles.length > 0 - ? [`Documentación estructurada detectada: ${docFiles.slice(0, 6).map((filePath) => `\`${filePath}\``).join(", ")}`] - : []; - const authority = buildSourceAuthoritySignals(authorityCandidates, context.discovery); - - return { - docFiles, - insights, - domainEntries, - actorEntries, - ruleEntries, - flowEntries, - decisionEntries, - pending, - summary, - authority - }; -} - -function buildDomainInventoryDocument(documentation: DocumentationSignals): { content: string; pending: string[] } { - const confirmed: string[] = []; - const pending: string[] = [...documentation.pending]; - - for (const domain of documentation.domainEntries) { - const notes: string[] = []; - if (domain.docFiles.length > 0) { - notes.push(`docs=${domain.docFiles.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - if (domain.codeFiles.length > 0) { - notes.push(`superficies=${domain.codeFiles.map((filePath) => `\`${filePath}\``).join(", ")}`); - } else { - pending.push(`El dominio \`${domain.label}\` está documentado, pero no se asociaron superficies de código por heurística.`); - } - if (domain.highlights.length > 0) { - notes.push(`señales=${domain.highlights.map((entry) => `"${entry}"`).join(" | ")}`); - } - - confirmed.push(`Dominio \`${domain.label}\`: ${notes.join("; ")}`); - } - - if (documentation.domainEntries.length === 0) { - confirmed.push("No se detectaron dominios respaldados por documentación; el contexto sigue dependiendo de la estructura del código."); - if (documentation.docFiles.length === 0) { - pending.push("No se detectaron `README` o carpetas `docs/` suficientemente estructuradas para extraer dominios operativos."); - } - } - - return { - content: `# Domain Inventory - -## Estado actual - -${renderList(confirmed)} - -## Pendiente de confirmar - -${renderList(uniqueSorted(pending))} -`, - pending: uniqueSorted(pending) - }; -} - -function inferProjectShape(discovery: DiscoveryResult): string { - const frameworks = new Set(discovery.frameworks); - const hasInternalApiSurface = discovery.files.some((filePath) => - /(^|\/)(src\/)?app\/api\/.+\/route\.(ts|tsx|js|jsx)$/i.test(filePath) || - /(^|\/)(src\/)?pages\/api\/.+\.(ts|tsx|js|jsx)$/i.test(filePath) || - /(^|\/)(routes|controllers)\//i.test(filePath) || - /(^|\/)prisma\/schema\.prisma$/i.test(filePath) - ); - - if ( - frameworks.has("NextJS") && - (frameworks.has("Express") || frameworks.has("FastAPI") || frameworks.has("NestJS") || hasInternalApiSurface) - ) { - return "Full-stack web application"; - } - - if (frameworks.has("NextJS") || frameworks.has("React")) { - return "Frontend-oriented application"; - } - - if (discovery.languages.includes("PHP") && discovery.files.some((filePath) => /(^|\/)(?:default\/)?app\/controllers\/[^/]+_controller\.php$/i.test(filePath))) { - return "PHP MVC web application"; - } - - if (frameworks.has("Express") || frameworks.has("FastAPI") || frameworks.has("NestJS") || frameworks.has("Spring") || frameworks.has("Rails")) { - return "Backend/API service"; - } - - if (discovery.infrastructure.length > 0) { - return "Operational service or infrastructure-backed application"; - } - - return "Software application"; -} - -function inferSystemGoal(metadata: RepoMetadata, discovery: DiscoveryResult): { confirmed?: string; pending?: string } { - if (metadata.readmeSummary) { - return { - confirmed: `${metadata.readmeSummary}${toEvidence(["README.md"])}` - }; - } - - if (discovery.frameworks.length > 0 || discovery.apis.length > 0) { - return { - confirmed: `${inferProjectShape(discovery)} built around ${discovery.frameworks.join(", ") || "the detected runtime"} and ${discovery.apis.join(", ") || "its current interfaces"}${toEvidence(sampleSourceEvidence([...discovery.manifests, ...discovery.apiFiles, ...discovery.structure.sampleFiles], 4))}` - }; - } - - return { - pending: "El objetivo funcional del sistema no está explicitado en README ni en nombres de módulo suficientemente claros." - }; -} - -function detectActors(discovery: DiscoveryResult): { confirmed: string[]; pending: string[] } { - const actors: string[] = []; - - if (discovery.files.some((filePath) => /(^|\/)(admin|backoffice|dashboard)(\/|$)/i.test(filePath))) { - actors.push(`Operadores o administradores internos${toEvidence(sample(matchingFiles(discovery, /(^|\/)(admin|backoffice|dashboard)(\/|$)/i), 4))}`); - } - - if (discovery.files.some((filePath) => /(^|\/)(seguridad_usuarios|usuario|usuarios|cuenta|cuentas)(\/|_|\.|$)/i.test(filePath))) { - actors.push(`Operadores internos con cuentas de usuario${toEvidence(sample(matchingFiles(discovery, /(^|\/)(seguridad_usuarios|usuario|usuarios|cuenta|cuentas)(\/|_|\.|$)/i), 4))}`); - } - - if (discovery.files.some((filePath) => /(^|\/)(auth|login|signup|account|profile)(\/|$)/i.test(filePath))) { - actors.push(`Usuarios autenticados${toEvidence(sample(matchingFiles(discovery, /(^|\/)(auth|login|signup|account|profile)(\/|$)/i), 4))}`); - } - - const publicRouteFiles = uniqueSorted([ - ...matchingFiles(discovery, /(^|\/)(src\/)?app\/\(?public\)?\/.+\/page\.(ts|tsx|js|jsx|mdx)$/i, 4), - ...matchingFiles(discovery, /(^|\/)(src\/)?app\/\(?public\)?\/page\.(ts|tsx|js|jsx|mdx)$/i, 4), - ...matchingFiles(discovery, /(^|\/)(src\/)?app\/\(?marketing\)?\/.+\/page\.(ts|tsx|js|jsx|mdx)$/i, 4), - ...matchingFiles(discovery, /(^|\/)(src\/)?pages\/(index|public|marketing|landing).*\.(ts|tsx|js|jsx|mdx)$/i, 4) - ]); - - if (publicRouteFiles.length > 0) { - actors.push(`Usuarios públicos o tráfico anónimo${toEvidence(sample(publicRouteFiles, 4))}`); - } - - if (actors.length === 0) { - return { - confirmed: [], - pending: ["Los actores de negocio no quedan explícitos en rutas, módulos o docs; requieren confirmación manual."] - }; - } - - return { - confirmed: uniqueSorted(actors), - pending: [] - }; -} - -function detectDataSignals(discovery: DiscoveryResult, flatDependencies: string[]): string[] { - const signals: string[] = []; - const add = (label: string, evidence: string[]) => signals.push(`${label}${toEvidence(sample(evidence, 4))}`); - - const prismaSchemaFiles = matchingFiles(discovery, /(^|\/)prisma\/schema\.prisma$/i); - const prismaMigrationFiles = matchingFiles(discovery, /(^|\/)prisma\/migrations\//i); - const prismaSeedFiles = matchingFiles(discovery, /(^|\/)prisma\/(seed(\.[^/]+)?|seeds\/)/i); - if (prismaSchemaFiles.length > 0) { - add("Prisma schema define la fuente de verdad relacional", [ - ...prismaSchemaFiles, - ...prismaMigrationFiles, - ...prismaSeedFiles - ]); - } else if (prismaMigrationFiles.length > 0) { - add("Prisma migrations versiona la capa de persistencia relacional", [...prismaMigrationFiles, ...prismaSeedFiles]); - } - - const drizzleFiles = uniqueSorted([ - ...matchingFiles(discovery, /(^|\/)drizzle(\/|$)/i), - ...matchingFiles(discovery, /(^|\/)drizzle\.config\.(ts|js|mts|cts)$/i) - ]); - if (drizzleFiles.length > 0 || flatDependencies.some((dependency) => dependency.includes("drizzle-orm"))) { - add("Drizzle define una parte explícita de la capa de persistencia", [...drizzleFiles, ...matchingFiles(discovery, /\.sql$/i)]); - } - - const sqlMigrationFiles = matchingFiles(discovery, /(^|\/)(migrations?|db\/migrations)\//i).filter( - (filePath) => !/\/prisma\//i.test(filePath) - ); - if (sqlMigrationFiles.length > 0) { - add("Migraciones SQL versionadas sugieren una fuente de verdad relacional", [ - ...sqlMigrationFiles, - ...matchingFiles(discovery, /\.sql$/i).filter((filePath) => !/\/prisma\//i.test(filePath)) - ]); - } - - const sqlFiles = matchingFiles(discovery, /\.sql$/i).filter((filePath) => !/\/prisma\//i.test(filePath)); - if (sqlMigrationFiles.length === 0 && sqlFiles.length > 0) { - add("Dump o esquema SQL versionado sugiere la fuente de verdad relacional", sqlFiles); - } - - const dependencySignals = matchingDependencies(flatDependencies, DATA_DEPENDENCY_MAP); - for (const signal of dependencySignals) { - add(`${signal} está presente en dependencias`, discovery.manifests); - } - - return uniquePreserved(signals); -} - -function detectFrontendSignals(discovery: DiscoveryResult, flatDependencies: string[]): FrontendSignals { - const appRoutes = matchingFiles(discovery, /(^|\/)(src\/)?app\/.+\/page\.(ts|tsx|js|jsx|mdx)$/i, 120); - const appLayouts = matchingFiles(discovery, /(^|\/)(src\/)?app\/.+\/layout\.(ts|tsx|js|jsx|mdx)$/i, 80); - const rootAppRoutes = matchingFiles(discovery, /(^|\/)(src\/)?app\/page\.(ts|tsx|js|jsx|mdx)$/i, 8); - const pageRoutes = matchingFiles( - discovery, - /(^|\/)(src\/)?pages\/(?!api\/)(?!_app\.|_document\.|_error\.)[^/].+\.(ts|tsx|js|jsx|mdx)$/i, - 80 - ); - const componentRoots = uniqueSorted( - [ - discovery.files.some((filePath) => /^components\//.test(filePath)) ? "components/" : "", - discovery.files.some((filePath) => /^src\/components\//.test(filePath)) ? "src/components/" : "", - discovery.files.some((filePath) => /^app\/components\//.test(filePath)) ? "app/components/" : "", - discovery.files.some((filePath) => /^app\/src\/components\//.test(filePath)) ? "app/src/components/" : "", - discovery.files.some((filePath) => /^app\/src\/app\/components\//.test(filePath)) ? "app/src/app/components/" : "", - discovery.files.some((filePath) => /^src\/app\/components\//.test(filePath)) ? "src/app/components/" : "", - discovery.files.some((filePath) => /^src\/ui\//.test(filePath)) ? "src/ui/" : "", - discovery.files.some((filePath) => /^components\/ui\//.test(filePath)) ? "components/ui/" : "", - discovery.files.some((filePath) => /^app\/src\/components\/ui\//.test(filePath)) ? "app/src/components/ui/" : "", - discovery.files.some((filePath) => /^app\/src\/app\/components\/ui\//.test(filePath)) ? "app/src/app/components/ui/" : "" - ].filter(Boolean) - ); - const componentFiles = matchingFiles(discovery, /(^|\/)(components|ui)\//i, 12); - const i18nSignals = uniqueSorted([ - ...matchingDependencies(flatDependencies, I18N_DEPENDENCY_MAP), - ...matchingFiles(discovery, /(^|\/)(locales|messages|i18n)\//i, 6) - ]); - const stateSignals = matchingDependencies(flatDependencies, STATE_DEPENDENCY_MAP); - const dataFetchingSignals = matchingDependencies(flatDependencies, DATA_FETCHING_DEPENDENCY_MAP); - const uiSignals = uniqueSorted([ - ...matchingDependencies(flatDependencies, UI_DEPENDENCY_MAP), - ...matchingFiles(discovery, /(tailwind\.config|postcss\.config|components\/ui\/|src\/ui\/)/i, 6) - ]); - const adminSignals = matchingFiles(discovery, /(^|\/)(admin|backoffice|dashboard)(\/|$)/i, 8); - - let renderingStrategy: string | undefined; - if (appRoutes.length > 0 || rootAppRoutes.length > 0) { - renderingStrategy = `Next.js App Router${toEvidence(sample([...rootAppRoutes, ...appRoutes, ...appLayouts], 4))}`; - } else if (pageRoutes.length > 0) { - renderingStrategy = `Next.js Pages Router${toEvidence(sample(pageRoutes, 4))}`; - } else if ( - discovery.frameworks.includes("React") && - discovery.files.some((filePath) => /(^|\/)(src\/)?(main|index)\.(tsx|jsx)$/i.test(filePath)) - ) { - renderingStrategy = `Cliente SPA de React${toEvidence(sample(matchingFiles(discovery, /(^|\/)(src\/)?(main|index)\.(tsx|jsx)$/i, 4)))}`; - } - - return { - routeFiles: uniqueSorted([...rootAppRoutes, ...appRoutes, ...pageRoutes]), - layoutFiles: appLayouts, - pageFiles: uniqueSorted([...rootAppRoutes, ...appRoutes, ...pageRoutes]), - componentRoots, - componentFiles, - renderingStrategy, - i18nSignals, - stateSignals, - dataFetchingSignals, - uiSignals, - adminSignals - }; -} - -function detectBackendSignals(discovery: DiscoveryResult, flatDependencies: string[]): BackendSignals { - const routeFiles = uniqueSorted( - [ - ...matchingFiles(discovery, /(^|\/)(src\/)?app\/api\/.+\/route\.(ts|tsx|js|jsx)$/i, 160), - ...matchingFiles(discovery, /(^|\/)(src\/)?pages\/api\/.+\.(ts|tsx|js|jsx)$/i, 80), - ...matchingFiles(discovery, /(^|\/)(routes|controllers|api)\//i, 80), - ...matchingFiles(discovery, /(^|\/)(?:default\/)?app\/controllers\/[^/]+_controller\.php$/i, 120) - ] - ).slice(0, 120); - - const contractFiles = uniqueSorted([ - ...discovery.apiFiles, - ...matchingFiles(discovery, /(schema\.graphql|schema\.prisma|openapi|swagger)/i), - ...matchingFiles(discovery, /(^|\/)(?:default\/)?app\/config\/routes\.php$/i, 4), - ...matchingFiles(discovery, /\.sql$/i, 6), - ...matchingFiles(discovery, /(^|\/)(API|ARCHITECTURE|FLOWS|BUSINESS_RULES)\.(md|mdx|txt)$/i, 12), - ...matchingFiles(discovery, /(^|\/)app\/docs\/technical\/ACCESS_CONTROL\.(md|mdx|txt)$/i, 4) - ]).filter((filePath) => !/^AI_CONTEXT\//i.test(filePath)); - - const authSignals = uniqueSorted([ - ...matchingDependencies(flatDependencies, AUTH_DEPENDENCY_MAP), - ...matchingFiles(discovery, /(^|\/)(auth|middleware|guards?|permissions?|acl|rbac)\//i, 8) - ]); - - const validationSignals = uniqueSorted([ - ...matchingDependencies(flatDependencies, VALIDATION_DEPENDENCY_MAP), - ...matchingFiles(discovery, /(^|\/)(validators?|schemas?)\//i, 8) - ]); - - const dataSignals = detectDataSignals(discovery, flatDependencies); - const queueSignals = uniqueSorted([ - ...matchingDependencies(flatDependencies, QUEUE_DEPENDENCY_MAP), - ...matchingFiles(discovery, /(^|\/)(jobs|workers|queues?|cron|schedules?)\//i, 8) - ]); - const webhookSignals = matchingFiles(discovery, /(^|\/)(webhooks?|integrations?)\//i, 10); - const integrationSignals = uniqueSorted([ - ...matchingFiles(discovery, /(^|\/)(integrations?|clients?|adapters?)\//i, 10), - ...matchingFiles(discovery, /(stripe|slack|sendgrid|twilio|s3|aws|gcp|azure)/i, 10) - ]); - - return { - routeFiles, - endpointExamples: [], - contractFiles, - authSignals, - validationSignals, - dataSignals, - queueSignals, - webhookSignals, - integrationSignals - }; -} - -function pickPrimaryDataSignal(dataSignals: string[]): string | undefined { - const priorities = [ - /^Prisma schema define/i, - /^Prisma migrations versiona/i, - /^Drizzle define/i, - /^Migraciones SQL versionadas/i - ]; - - for (const pattern of priorities) { - const match = dataSignals.find((signal) => pattern.test(signal)); - if (match) { - return match; - } - } - - return dataSignals[0]; -} - -function inferEndpointGroup(entry: string): string { - const match = entry.match(/\b(\/api\/[^\s)]+)/i); - if (!match?.[1]) { - return entry; - } - - const segments = match[1].replace(/^\/api\//i, "").split("/").filter(Boolean); - if (segments.length === 0) { - return "api"; - } - - if (segments[0] === "admin" || segments[0] === "profile") { - return segments.slice(0, 2).join("/"); - } - - return segments[0]; -} - -function scoreEndpointExample(entry: string): number { - let score = 0; - - if (/\/api\/events\b/i.test(entry)) { - score += 20; - } - if (/\/api\/routes\b/i.test(entry)) { - score += 19; - } - if (/\/api\/profile\/business\b/i.test(entry)) { - score += 18; - } - if (/\/api\/reports\b/i.test(entry)) { - score += 17; - } - if (/\/api\/notifications\b/i.test(entry)) { - score += 16; - } - if (/\/api\/reviews\b/i.test(entry)) { - score += 15; - } - if (/\/api\/social\b/i.test(entry)) { - score += 14; - } - if (/\/api\/upload\b/i.test(entry)) { - score += 13; - } - if (/\/api\/admin\b/i.test(entry)) { - score += 12; - } - if (/\/api\/auth\b/i.test(entry)) { - score += 11; - } - - return score; -} - -function pickRepresentativeEndpointExamples(entries: string[], limit: number): string[] { - const sorted = uniqueSorted(entries).sort( - (left, right) => scoreEndpointExample(right) - scoreEndpointExample(left) || left.localeCompare(right) - ); - const selected: string[] = []; - const seenGroups = new Set(); - - for (const entry of sorted) { - const group = inferEndpointGroup(entry); - if (seenGroups.has(group)) { - continue; - } - selected.push(entry); - seenGroups.add(group); - if (selected.length >= limit) { - return selected; - } - } - - for (const entry of sorted) { - if (selected.includes(entry)) { - continue; - } - selected.push(entry); - if (selected.length >= limit) { - break; - } - } - - return selected; -} - -async function extractInlineEndpoints(context: ProjectContext, backend: BackendSignals): Promise { - const endpointExamples = new Set(); - const candidateFiles = uniqueSorted([ - ...backend.routeFiles, - ...matchingFiles(context.discovery, /(^|\/)src\/index\.(ts|js)$/i, 2), - ...matchingFiles(context.discovery, /(^|\/)server\//i, 6) - ]); - - for (const filePath of candidateFiles) { - const content = await readTextSafe(path.join(context.targetPath, filePath)); - if (!content) { - continue; - } - - for (const match of content.matchAll(/\b(?:app|router)\.(get|post|put|patch|delete)\(\s*["'`]([^"'`]+)["'`]/gi)) { - endpointExamples.add(`${String(match[1]).toUpperCase()} ${String(match[2])} (${filePath})`); - } - - for (const match of content.matchAll(/@(Get|Post|Put|Patch|Delete)\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) { - endpointExamples.add(`${String(match[1]).toUpperCase()} ${String(match[2])} (${filePath})`); - } - - if (/\/route\.(ts|tsx|js|jsx)$/i.test(filePath)) { - const routePath = filePath - .replace(/^(src\/)?app\/api\//i, "/api/") - .replace(/\/route\.(ts|tsx|js|jsx)$/i, "") - .replace(/\[([^\]]+)\]/g, ":$1"); - - for (const match of content.matchAll(/export\s+async\s+function\s+(GET|POST|PUT|PATCH|DELETE)\b/g)) { - endpointExamples.add(`${String(match[1]).toUpperCase()} ${routePath} (${filePath})`); - } - } - - if (/\/pages\/api\//i.test(filePath)) { - const routePath = filePath - .replace(/^(src\/)?pages\/api\//i, "/api/") - .replace(/\.(ts|tsx|js|jsx)$/i, "") - .replace(/\/index$/i, "") - .replace(/\[([^\]]+)\]/g, ":$1"); - - endpointExamples.add(`API handler ${routePath} (${filePath})`); - } - - const phpController = filePath.match(/(^|\/)(?:default\/)?app\/controllers\/([^/]+)_controller\.php$/i); - if (phpController?.[2]) { - const controllerRoute = normalizeDomainLabel(phpController[2]).replace(/-/g, "_"); - for (const match of content.matchAll(/public\s+function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g)) { - const action = String(match[1]); - if (/^(__construct|initialize|before_filter|after_filter)$/i.test(action)) { - continue; - } - endpointExamples.add(`Kumbia action /${controllerRoute}/${action} (${filePath})`); - } - } - } - - return pickRepresentativeEndpointExamples([...endpointExamples], 16); -} - -function detectModuleSignals(discovery: DiscoveryResult): ModuleSignal[] { - const moduleMap = new Map(); - - const push = (label: string, filePath: string, note: string, status: ModuleSignal["status"] = "active") => { - const existing = moduleMap.get(label); - if (existing) { - if (!existing.files.includes(filePath)) { - existing.files.push(filePath); - } - return; - } - - moduleMap.set(label, { - label, - files: [filePath], - status, - note - }); - }; - - for (const filePath of discovery.files) { - const normalized = filePath.replace(/\\/g, "/"); - const parts = normalized.split("/"); - const phpMvcDomain = inferPhpMvcDomain(normalized); - - if (phpMvcDomain) { - push(phpMvcDomain.label, normalized, `Módulo MVC PHP/Kumbia detectado por ${phpMvcDomain.role}.`); - continue; - } - - if (/^(src\/)?(features|domains|modules)\//i.test(normalized) && parts[2]) { - push(parts[2], normalized, "Módulo explícito bajo `features/`, `domains/` o `modules/`."); - continue; - } - - if (/^(src\/)?app\//i.test(normalized) && parts[parts.length - 1]?.match(/^(page|layout|route)\./)) { - const routeGroup = parts.slice(parts[0] === "src" ? 2 : 1, Math.max(parts.length - 1, parts[0] === "src" ? 3 : 2)).join("/"); - push(routeGroup || "root-route", normalized, "Superficie de ruta activa en App Router."); - continue; - } - - if (/^(src\/)?pages\//i.test(normalized) && !/\/api\//i.test(normalized) && !/_app\.|_document\.|_error\./i.test(normalized)) { - const routeGroup = parts.slice(parts[0] === "src" ? 2 : 1, Math.max(parts.length - 1, parts[0] === "src" ? 3 : 2)).join("/"); - push(routeGroup || "root-page", normalized, "Superficie de ruta activa en Pages Router."); - continue; - } - } - - if (moduleMap.size === 0) { - for (const topLevel of discovery.structure.topLevelDirectories) { - const relatedFiles = discovery.files.filter((filePath) => filePath.startsWith(`${topLevel}/`)).slice(0, 4); - const lower = topLevel.toLowerCase(); - const status = - /legacy|deprecated|old/.test(lower) ? "legacy" : /fallback|compat/.test(lower) ? "fallback" : "active"; - moduleMap.set(topLevel, { - label: topLevel, - files: relatedFiles, - status, - note: "Superficie estructural detectada en el escaneo del repo." - }); - } - } - - return [...moduleMap.values()] - .map((signal) => ({ - ...signal, - files: uniqueSorted(signal.files).slice(0, 4) - })) - .sort((left, right) => left.label.localeCompare(right.label)); -} - -function buildSystemOverview( - metadata: RepoMetadata, - discovery: DiscoveryResult, - frontend: FrontendSignals, - backend: BackendSignals, - documentation: DocumentationSignals, - navigation: NavigationSignals -): { content: string; summary: string[]; pending: string[] } { - const confirmed: string[] = []; - const pending: string[] = [...documentation.pending]; - const goal = inferSystemGoal(metadata, discovery); - const actors = detectActors(discovery); - - confirmed.push(`Repositorio: \`${discovery.repoName}\`${toEvidence(sample(discovery.manifests, 3))}`); - - if (goal.confirmed) { - confirmed.push(`Objetivo del sistema: ${goal.confirmed}`); - } - if (goal.pending) { - pending.push(goal.pending); - } - - confirmed.push(`Arquitectura operativa actual: ${inferProjectShape(discovery)} con ${discovery.frameworks.join(", ") || "runtime pendiente de confirmar"} y ${discovery.apis.join(", ") || "sin contratos confirmados"}${toEvidence(sampleSourceEvidence([...discovery.manifests, ...discovery.apiFiles, ...discovery.structure.sampleFiles], 5))}`); - confirmed.push(`Stack principal: lenguajes=${discovery.languages.join(", ") || "Pendiente de confirmar"}; frameworks=${discovery.frameworks.join(", ") || "Pendiente de confirmar"}; testing=${discovery.testing.join(", ") || "Pendiente de confirmar"}${toEvidence(sample([...discovery.manifests, ...discovery.files.filter((filePath) => /Dockerfile|\.github\/workflows\//.test(filePath))], 5))}`); - - for (const actor of actors.confirmed) { - confirmed.push(`Actor principal: ${actor}`); - } - pending.push(...actors.pending); - for (const actor of documentation.actorEntries.slice(0, 4)) { - confirmed.push(`Actor documentado: ${actor}`); - } - - const primaryDataSignal = pickPrimaryDataSignal(backend.dataSignals); - if (primaryDataSignal) { - confirmed.push(`Persistencia y fuente de verdad: ${primaryDataSignal}`); - } else if (backend.contractFiles.length > 0) { - confirmed.push(`Fuente de verdad parcial: contratos o esquemas versionados${toEvidence(sample(backend.contractFiles, 4))}`); - pending.push("La fuente de verdad de datos persistidos no queda explícita en ORM, migraciones o esquemas de base de datos."); - } else { - pending.push("No se confirmó una fuente de verdad de datos persistidos en código o manifests."); - } - - const restrictions: string[] = []; - if (metadata.scripts.length > 0) { - restrictions.push(`scripts raíz disponibles: ${metadata.scripts.join(", ")}${toEvidence(["package.json"])}`); - } - if (discovery.ci.providers.length > 0) { - restrictions.push(`CI detectado: ${discovery.ci.providers.join(", ")}${toEvidence(sample(discovery.files.filter((filePath) => filePath.startsWith(".github/workflows/")), 3))}`); - } - if (frontend.renderingStrategy) { - restrictions.push(`rendering strategy activa: ${frontend.renderingStrategy}`); - } - if (discovery.infrastructure.length > 0) { - restrictions.push(`superficie operativa: ${discovery.infrastructure.join(", ")}${toEvidence(sample(discovery.infraFiles, 4))}`); - } - if (restrictions.length > 0) { - confirmed.push(...restrictions.map((restriction) => `Restricción técnica permanente: ${restriction}`)); - } else { - pending.push("No hay restricciones técnicas permanentes documentadas de forma explícita; conviene confirmarlas manualmente."); - } - if (navigation.guardSummaries.length > 0) { - confirmed.push(`Restricción técnica permanente: guardas de navegación/acceso detectadas${toEvidence(sample(navigation.guardFiles, 4))}`); - } - if (documentation.authority.declaredCanonicalSources.length > 0) { - confirmed.push(`Fuentes canónicas declaradas por la documentación: ${documentation.authority.declaredCanonicalSources.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - if (documentation.authority.primarySources.length > 0) { - confirmed.push( - `${documentation.authority.declaredCanonicalSources.length > 0 ? "Documentos operativos de referencia" : "Fuentes primarias operativas"}: ${documentation.authority.primarySources - .map((filePath) => `\`${filePath}\``) - .join(", ")}` - ); - } - if (documentation.authority.secondarySources.length > 0) { - confirmed.push(`Fuentes secundarias de apoyo: ${documentation.authority.secondarySources.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - confirmed.push(...documentation.authority.authorityNotes.slice(0, 2)); - pending.push(...documentation.authority.ambiguousSources); - confirmed.push(...documentation.summary.slice(0, 2)); - - const summary = [ - `Proyecto: ${discovery.repoName}`, - `Forma actual: ${inferProjectShape(discovery)}`, - `Stack: ${discovery.languages.join(", ") || "pendiente"} / ${discovery.frameworks.join(", ") || "pendiente"}`, - `APIs o contratos: ${discovery.apis.join(", ") || "sin confirmar"}`, - `Fuente de verdad: ${primaryDataSignal ?? "pendiente de confirmar"}`, - `Frontend: ${frontend.renderingStrategy ?? "sin superficie frontend confirmada"}`, - `CI/testing: ${discovery.ci.providers.join(", ") || "sin CI detectado"} / ${discovery.testing.join(", ") || "sin testing confirmado"}` - ]; - - return { - content: `# System Overview - -## Estado actual - -${renderList(confirmed)} - -## Pendiente de confirmar - -${renderList(uniqueSorted(pending))} -`, - summary, - pending: uniqueSorted(pending) - }; -} - -function buildModulesMap( - discovery: DiscoveryResult, - frontend: FrontendSignals, - backend: BackendSignals, - documentation: DocumentationSignals, - navigation: NavigationSignals -): { content: string; pending: string[] } { - const modules = detectModuleSignals(discovery); - const confirmed: string[] = documentation.domainEntries.map((domainEntry) => { - const notes = [ - domainEntry.docFiles.length > 0 ? `docs=${domainEntry.docFiles.map((filePath) => `\`${filePath}\``).join(", ")}` : "", - domainEntry.codeFiles.length > 0 ? `superficies=${domainEntry.codeFiles.map((filePath) => `\`${filePath}\``).join(", ")}` : "", - domainEntry.highlights.length > 0 ? `señales=${domainEntry.highlights.join(" | ")}` : "" - ] - .filter(Boolean) - .join("; "); - - return `Dominio ${domainEntry.label}: ${notes}`; - }); - const highlightedModules = modules.slice(0, documentation.domainEntries.length > 0 ? 16 : 24); - confirmed.push(...highlightedModules.map((moduleSignal) => { - const status = moduleSignal.status === "legacy" ? "legacy" : moduleSignal.status === "fallback" ? "fallback" : "activo"; - return `Módulo ${moduleSignal.label}: ${moduleSignal.note}; estado=${status}${toEvidence(moduleSignal.files)}`; - })); - const pending: string[] = [...documentation.pending]; - - if (frontend.routeFiles.length > 0) { - confirmed.push(`Rutas clave: ${pickRepresentativeFiles(frontend.routeFiles, 8).map((filePath) => `\`${filePath}\``).join(", ")}`); - } else if (backend.routeFiles.length > 0 || backend.contractFiles.length > 0) { - confirmed.push(`Superficie de rutas/backend: ${sample([...backend.routeFiles, ...backend.contractFiles], 8).map((filePath) => `\`${filePath}\``).join(", ")}`); - } else { - pending.push("No se detectaron rutas clave en convenciones comunes (`app/`, `pages/`, `routes/`, `controllers/`)."); - } - - if (frontend.adminSignals.length > 0) { - confirmed.push(`Panel admin o backoffice detectado${toEvidence(sample(frontend.adminSignals, 4))}`); - } else { - pending.push("No se confirmó un panel admin/backoffice en rutas o carpetas con nombres explícitos."); - } - if (navigation.navFiles.length > 0) { - confirmed.push(`Superficies de navegación: ${pickRepresentativeFiles(navigation.navFiles, 8).map((filePath) => `\`${filePath}\``).join(", ")}`); - } - if (navigation.actorScopes.length > 0) { - confirmed.push(`Scopes o actores con navegación propia: ${navigation.actorScopes.map((scope) => `\`${scope}\``).join(", ")}`); - } - - const primaryDataSignal = pickPrimaryDataSignal(backend.dataSignals); - if (primaryDataSignal) { - confirmed.push(`Relación módulo-datos: ${primaryDataSignal}`); - } else { - pending.push("La relación entre módulos y la fuente de verdad de datos requiere confirmación manual."); - } - - if (!modules.some((moduleSignal) => moduleSignal.status !== "active")) { - pending.push("No se encontraron superficies marcadas como legacy o fallback por naming; si existen, requieren confirmación manual."); - } - if (modules.length > highlightedModules.length) { - confirmed.push(`Superficies adicionales detectadas: ${modules.length - highlightedModules.length} módulos/rutas más; priorizar dominios documentados para trabajo operativo.`); - } - - return { - content: `# Modules Map - -## Estado actual - -${renderList(confirmed)} - -## Pendiente de confirmar - -${renderList(uniqueSorted(pending))} -`, - pending: uniqueSorted(pending) - }; -} - -function buildFrontendArchitecture( - frontend: FrontendSignals, - discovery: DiscoveryResult, - navigation: NavigationSignals -): { content: string; pending: string[] } { - const confirmed: string[] = []; - const pending: string[] = []; - - if (frontend.renderingStrategy) { - confirmed.push(`Rendering strategy: ${frontend.renderingStrategy}`); - } else { - pending.push("No se confirmó una estrategia de rendering frontend en `app/`, `pages/`, `main.tsx` o `index.tsx`."); - } - - if (frontend.layoutFiles.length > 0 || frontend.pageFiles.length > 0) { - confirmed.push(`Layouts y páginas: ${pickRepresentativeFiles([...frontend.layoutFiles, ...frontend.pageFiles], 8).map((filePath) => `\`${filePath}\``).join(", ")}`); - } else { - pending.push("No se confirmó una estructura de layouts/páginas frontend."); - } - - if (frontend.componentRoots.length > 0) { - confirmed.push(`Raíces de componentes: ${frontend.componentRoots.map((root) => `\`${root}\``).join(", ")}${toEvidence(sample(frontend.componentFiles, 4))}`); - } else { - pending.push("No se detectó una raíz clara de componentes reutilizables."); - } - - if (navigation.navFiles.length > 0) { - confirmed.push( - `Navegación y shells: archivos=${pickRepresentativeFiles(navigation.navFiles, 8).map((filePath) => `\`${filePath}\``).join(", ")}${ - navigation.navLabels.length > 0 ? `; entradas=${navigation.navLabels.map((label) => `"${label}"`).join(", ")}` : "" - }${navigation.navSectionLabels.length > 0 ? `; secciones=${navigation.navSectionLabels.map((label) => `"${label}"`).join(", ")}` : ""}` - ); - } else if (discovery.frameworks.includes("React") || discovery.frameworks.includes("NextJS")) { - pending.push("No se detectaron sidebars, nav configs o shells de navegación por naming convencional."); - } - - if (frontend.i18nSignals.length > 0) { - confirmed.push(`i18n: ${frontend.i18nSignals.map((signal) => `\`${signal}\``).join(", ")}`); - } else { - pending.push("No se confirmaron librerías o carpetas de i18n/locales."); - } - - if (frontend.stateSignals.length > 0 || frontend.dataFetchingSignals.length > 0) { - confirmed.push( - `Estado y data fetching: ${[ - frontend.stateSignals.length > 0 ? `state=${frontend.stateSignals.join(", ")}` : "", - frontend.dataFetchingSignals.length > 0 ? `data=${frontend.dataFetchingSignals.join(", ")}` : "" - ] - .filter(Boolean) - .join("; ")}` - ); - } else if (discovery.frameworks.includes("React") || discovery.frameworks.includes("NextJS")) { - pending.push("No se confirmaron librerías explícitas de estado o fetching; puede existir uso directo de `fetch` o utilidades locales."); - } - - if (frontend.uiSignals.length > 0) { - confirmed.push(`Sistema visual y dependencias UI: ${frontend.uiSignals.map((signal) => `\`${signal}\``).join(", ")}`); - } else if (discovery.frameworks.includes("React") || discovery.frameworks.includes("NextJS")) { - pending.push("No se confirmaron dependencias visuales base; revisar CSS global y componentes raíz manualmente."); - } - - if (confirmed.length === 0) { - confirmed.push("No hay una superficie frontend principal confirmada; el repo parece orientado a backend o infraestructura."); - } - - return { - content: `# Frontend Architecture - -## Estado actual - -${renderList(confirmed)} - -## Pendiente de confirmar - -${renderList(uniqueSorted(pending))} -`, - pending: uniqueSorted(pending) - }; -} - -function buildBackendFlowsAndContracts( - backend: BackendSignals, - discovery: DiscoveryResult, - documentation: DocumentationSignals -): { content: string; pending: string[] } { - const confirmed: string[] = []; - const pending: string[] = []; - - if (backend.endpointExamples.length > 0) { - confirmed.push(`Endpoints reales detectados: ${backend.endpointExamples.map((entry) => `\`${entry}\``).join(", ")}`); - } else if (backend.contractFiles.length > 0) { - confirmed.push(`Contratos o superficies API: ${sample(backend.contractFiles, 8).map((filePath) => `\`${filePath}\``).join(", ")}`); - pending.push("Los handlers concretos de endpoints no se confirmaron en patrones inline (`app.get`, `router.get`, `route.ts`)."); - } else { - pending.push("No se confirmaron endpoints reales ni contratos versionados."); - } - - if (backend.authSignals.length > 0) { - confirmed.push(`Autenticación/autorización: ${backend.authSignals.map((signal) => `\`${signal}\``).join(", ")}`); - } else { - pending.push("No se confirmó una capa de auth/authz en dependencias o carpetas convencionales."); - } - - if (backend.validationSignals.length > 0) { - confirmed.push(`Validaciones: ${backend.validationSignals.map((signal) => `\`${signal}\``).join(", ")}`); - } else { - pending.push("No se confirmaron validadores o esquemas explícitos de entrada."); - } - - if (backend.dataSignals.length > 0) { - confirmed.push(`Acceso a datos: ${backend.dataSignals.join("; ")}`); - } else { - pending.push("No se confirmó una capa explícita de acceso a base de datos, migraciones o seeds."); - } - - if (backend.queueSignals.length > 0 || backend.webhookSignals.length > 0 || backend.integrationSignals.length > 0) { - confirmed.push( - `Jobs/webhooks/integraciones: ${[ - backend.queueSignals.length > 0 ? `jobs=${backend.queueSignals.join(", ")}` : "", - backend.webhookSignals.length > 0 ? `webhooks=${backend.webhookSignals.join(", ")}` : "", - backend.integrationSignals.length > 0 ? `integraciones=${backend.integrationSignals.join(", ")}` : "" - ] - .filter(Boolean) - .join("; ")}` - ); - } else { - pending.push("No se confirmaron colas, webhooks o integraciones externas en superficies convencionales."); - } - - if (backend.contractFiles.length > 0) { - confirmed.push(`Contratos vigentes frontend/backend: ${backend.contractFiles.map((filePath) => `\`${filePath}\``).join(", ")}`); - } else if (discovery.apis.length > 0) { - pending.push("Hay señales de API, pero no un contrato versionado claro entre frontend y backend."); - } - - if (documentation.authority.declaredCanonicalSources.length > 0) { - confirmed.push(`Fuentes canónicas declaradas para contratos/reglas: ${documentation.authority.declaredCanonicalSources.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - if (documentation.authority.primarySources.length > 0) { - confirmed.push( - `${documentation.authority.declaredCanonicalSources.length > 0 ? "Documentos operativos de referencia" : "Fuentes primarias para contratos/reglas"}: ${documentation.authority.primarySources - .map((filePath) => `\`${filePath}\``) - .join(", ")}` - ); - } - if (documentation.authority.secondarySources.length > 0) { - confirmed.push(`Fuentes secundarias relevantes: ${documentation.authority.secondarySources.map((filePath) => `\`${filePath}\``).join(", ")}`); - } - confirmed.push(...documentation.authority.authorityNotes.slice(0, 2)); - pending.push(...documentation.authority.ambiguousSources); - - return { - content: `# Backend Flows And Contracts - -## Estado actual - -${renderList(confirmed)} - -## Pendiente de confirmar - -${renderList(uniqueSorted(pending))} -`, - pending: uniqueSorted(pending) - }; -} - -function buildUiRules( - frontend: FrontendSignals, - discovery: DiscoveryResult, - navigation: NavigationSignals -): { content: string; pending: string[] } { - const confirmed: string[] = []; - const pending: string[] = []; - - if (frontend.uiSignals.length > 0) { - confirmed.push(`Dirección visual activa: ${frontend.uiSignals.map((signal) => `\`${signal}\``).join(", ")}`); - } else if (discovery.frameworks.includes("React") || discovery.frameworks.includes("NextJS")) { - pending.push("No se confirmó una dirección visual activa a partir de dependencias UI o config CSS."); - } - - if (frontend.componentRoots.length > 0) { - confirmed.push(`Componentes base a respetar: ${frontend.componentRoots.map((root) => `\`${root}\``).join(", ")}${toEvidence(sample(frontend.componentFiles, 5))}`); - } else { - pending.push("No se confirmó una base clara de componentes reutilizables."); - } - - if (frontend.renderingStrategy) { - confirmed.push(`Regla de implementación: respetar la estrategia de rutas/rendering ya activa (${frontend.renderingStrategy}).`); - } - if (navigation.navFiles.length > 0) { - confirmed.push( - `Navegación operacional: mantener shells y menús por actor${toEvidence(sample(navigation.navFiles, 5))}${ - navigation.actorScopes.length > 0 ? ` Scopes detectados: ${navigation.actorScopes.map((scope) => `\`${scope}\``).join(", ")}.` : "" - }` - ); - } - if (navigation.guardSummaries.length > 0) { - confirmed.push(`Permisos y guardas UI: ${navigation.guardSummaries.slice(0, 4).map((entry) => `\`${entry}\``).join(", ")}`); - } else { - pending.push("No se confirmaron guardas de navegación o permisos UI desde layouts, auth helpers o archivos de acceso."); - } - - if (frontend.uiSignals.some((signal) => signal.includes("Tailwind"))) { - confirmed.push("Responsive: la superficie parece utility-first; cambios visuales deben respetar clases y layout utilities existentes."); - } else { - pending.push("Las reglas de responsive necesitan confirmación manual si no hay un sistema CSS explícito detectado."); - } - - if (frontend.componentFiles.some((filePath) => /dialog|modal|form|table|button/i.test(filePath))) { - confirmed.push(`Patrones que no deben degradarse: formularios, tablas o acciones base detectadas${toEvidence(sample(frontend.componentFiles.filter((filePath) => /dialog|modal|form|table|button/i.test(filePath)), 5))}`); - } else { - pending.push("No se confirmaron patrones UI sensibles como forms, tablas o modales por naming explícito."); - } - - if (frontend.i18nSignals.length > 0) { - confirmed.push("Copy/i18n: respetar la infraestructura de traducción ya presente antes de introducir strings inline."); - } else { - pending.push("Accesibilidad y copy requieren revisión manual; no hay señales suficientes de tooling o reglas explícitas."); - } - - if (confirmed.length === 0) { - confirmed.push("No hay una UI principal confirmada; no deben imponerse reglas visuales imaginarias."); - } - - return { - content: `# UI Rules - -## Estado actual - -${renderList(confirmed)} - -## Pendiente de confirmar - -${renderList(uniqueSorted(pending))} -`, - pending: uniqueSorted(pending) - }; -} - -function buildGeneratedDecisions( - discovery: DiscoveryResult, - frontend: FrontendSignals, - backend: BackendSignals, - documentation: DocumentationSignals -): string { - const entries: string[] = []; - - for (const decision of documentation.decisionEntries.slice(0, 4)) { - entries.push(`Decisión documentada: ${decision}`); - } - - if (discovery.frameworks.length > 0) { - entries.push(`La base tecnológica actual gira alrededor de ${discovery.frameworks.join(", ")}${toEvidence(sample(discovery.manifests, 3))}; la justificación explícita no está documentada y requiere confirmación manual si importa para cambios grandes.`); - } - if (frontend.renderingStrategy) { - entries.push(`La estrategia de frontend activa es ${frontend.renderingStrategy}; futuros cambios deben respetar esa partición antes de mezclar routers o modos de rendering.`); - } - if (backend.contractFiles.length > 0) { - entries.push(`Los contratos versionados actuales pasan por ${backend.contractFiles.map((filePath) => `\`${filePath}\``).join(", ")}; cualquier cambio debe tratarlos como artefactos vigentes.`); - } - if (backend.dataSignals.length > 0) { - entries.push(`La persistencia o sus señales explícitas viven en ${backend.dataSignals.join("; ")}; no conviene asumir una segunda fuente de verdad sin confirmar ownership.`); - } - if (documentation.authority.declaredCanonicalSources.length > 0) { - entries.push(`La documentación vigente declara como canon ${documentation.authority.declaredCanonicalSources.map((filePath) => `\`${filePath}\``).join(", ")}; si un markdown resumido contradice eso, debe ganar el código o schema indicado.`); - } - if (documentation.authority.primarySources.length > 0) { - entries.push(`Los documentos operativos de referencia están en ${documentation.authority.primarySources.map((filePath) => `\`${filePath}\``).join(", ")}; sirven para orientar cambios, pero no sustituyen una fuente canónica declarada.`); - } - if (documentation.authority.ambiguousSources.length > 0) { - entries.push(`Hay fuentes ambiguas que no deben tomarse como canon único: ${documentation.authority.ambiguousSources.join(" | ")}`); - } - - return `## Generated snapshot ${discovery.scannedAt} - -${renderList(entries.length > 0 ? entries : ["No se pudieron inferir decisiones arquitectónicas fiables sin más evidencia del repo."])} -`; -} - -function buildGeneratedLearnings( - discovery: DiscoveryResult, - frontend: FrontendSignals, - backend: BackendSignals, - documentation: DocumentationSignals, - navigation: NavigationSignals -): string { - const entries: string[] = []; - - if (documentation.docFiles.length > 0) { - entries.push(`Prioriza la documentación operativa detectada (${documentation.docFiles.slice(0, 4).map((filePath) => `\`${filePath}\``).join(", ")}) antes de inferir reglas desde naming o estructura superficial.`); - } - if (documentation.pending.length > 0) { - entries.push(`Hay documentación que requiere validación manual: ${documentation.pending.slice(0, 2).join(" | ")}`); - } - if (navigation.guardSummaries.length > 0) { - entries.push(`La navegación y el acceso parecen condicionados por auth/roles; revisar ${navigation.guardSummaries.slice(0, 3).join(" | ")} antes de simplificar menús o layouts.`); - } - - if (backend.contractFiles.length > 0) { - entries.push(`Trata ${backend.contractFiles.map((filePath) => `\`${filePath}\``).join(", ")} como contratos vivos antes de cambiar rutas o payloads.`); - } - if (documentation.authority.declaredCanonicalSources.length > 0) { - entries.push(`La propia documentación declara como fuentes canónicas ${documentation.authority.declaredCanonicalSources.map((filePath) => `\`${filePath}\``).join(", ")}; no trates los markdown como sustituto del runtime real.`); - } - if (documentation.authority.primarySources.length > 0) { - entries.push(`Antes de inferir reglas desde naming o estructura superficial, revisa los documentos operativos ${documentation.authority.primarySources.map((filePath) => `\`${filePath}\``).join(", ")}.`); - } - if (documentation.authority.ambiguousSources.length > 0) { - entries.push(`Hay ambigüedades documentales activas: ${documentation.authority.ambiguousSources.join(" | ")}`); - } - if (!frontend.renderingStrategy) { - entries.push("No asumas una UI completa si no hay `app/`, `pages/` o entrypoints frontend confirmados."); - } - if (discovery.ci.providers.length === 0) { - entries.push("No hay CI confirmado; cualquier cambio debería validarse manualmente antes de darlo por seguro."); - } - if (backend.dataSignals.length === 0) { - entries.push("La fuente de verdad de datos no quedó explícita; futuros agentes deben confirmarla antes de tocar persistencia."); - } - if (entries.length === 0) { - entries.push("No se detectaron trampas críticas nuevas en esta pasada ligera; mantener el contexto alineado con código y contratos."); - } - - return `## Generated snapshot ${discovery.scannedAt} - -${renderList(entries)} -`; -} - -function buildGeneratedTasks( - discovery: DiscoveryResult, - frontend: FrontendSignals, - backend: BackendSignals, - documentation: DocumentationSignals, - navigation: NavigationSignals, - pendingQuestions: string[] -): string { - const tasks: string[] = []; - const pushTask = (priority: "high" | "medium" | "low", task: string, context: string, doneWhen: string, blocker = "none") => { - tasks.push(`[${priority}] ${task}; Contexto: ${context}; Criterio de cierre: ${doneWhen}; Bloqueo: ${blocker}.`); - }; - - if (backend.dataSignals.length === 0) { - pushTask("high", "Confirmar la fuente de verdad de datos", "No se detectaron ORM, migraciones o schemas de persistencia inequívocos.", "La documentación identifica el owner y la capa de persistencia real."); - } - - if (!frontend.renderingStrategy && (discovery.frameworks.includes("React") || discovery.frameworks.includes("NextJS"))) { - pushTask("medium", "Confirmar la superficie frontend activa", "Hay framework frontend, pero no se detectó una estructura de rutas/rendering concluyente.", "Quedan documentados router, layouts y entrypoints vigentes."); - } - - for (const pending of documentation.pending.slice(0, 2)) { - pushTask("medium", "Validar documentación marcada como pendiente o draft", pending, "La documentación queda confirmada, corregida o descartada con evidencia en código.", "requiere validación humana"); - } - - for (const domain of documentation.domainEntries.filter((entry) => entry.codeFiles.length === 0).slice(0, 2)) { - pushTask( - "medium", - "Confirmar superficie real del dominio documentado", - `El dominio \`${domain.label}\` tiene documentación, pero no se asociaron rutas o módulos activos.`, - "Quedan documentadas las superficies reales del dominio o se marca explícitamente como futuro/legacy.", - "requiere validación humana" - ); - } - - for (const note of documentation.authority.ambiguousSources.slice(0, 2)) { - pushTask( - "high", - "Resolver ambigüedad en fuentes de verdad", - note, - "La fuente queda resuelta o se documenta explícitamente qué artefacto manda para cambios futuros.", - "requiere validación humana" - ); - } - - if (documentation.docFiles.length === 0) { - pushTask("medium", "Crear documentación mínima de dominio", "No se detectaron `README` o `docs/` con señales operativas suficientes.", "Existe al menos una fuente breve y confiable de contexto por dominio principal."); - } - - if (navigation.navFiles.length === 0 && (discovery.frameworks.includes("React") || discovery.frameworks.includes("NextJS"))) { - pushTask("low", "Confirmar la navegación operativa", "No se detectaron archivos de navegación por naming convencional.", "Quedan documentados sidebars, nav configs o shells activos.", "necesita confirmación manual"); - } - - if (navigation.guardSummaries.length === 0 && navigation.permissionFiles.length === 0) { - pushTask("low", "Confirmar reglas de acceso y permisos UI", "No se detectaron guardas o archivos de permisos en superficies convencionales.", "Quedan documentadas las restricciones de acceso por actor o se marca como no aplicable.", "necesita confirmación manual"); - } - - for (const pending of pendingQuestions.slice(0, 3)) { - pushTask("low", "Resolver hueco de contexto", pending, "El hueco queda resuelto en docs o se etiqueta como no aplicable.", "necesita confirmación manual"); - } - - return `## Generated snapshot ${discovery.scannedAt} - -${renderList(uniqueSorted(tasks))} -`; -} - -async function upsertGeneratedSection(filePath: string, title: string, generatedContent: string): Promise { - const existing = await readTextSafe(filePath); - const generatedBlock = `${GENERATED_START}\n${generatedContent.trim()}\n${GENERATED_END}\n`; - - if (!existing) { - await writeFileEnsured(filePath, `# ${title}\n\n${generatedBlock}`); - return; - } - - if (existing.includes(GENERATED_START) && existing.includes(GENERATED_END)) { - const updated = existing.replace(new RegExp(`${GENERATED_START}[\\s\\S]*?${GENERATED_END}\\n?`, "m"), generatedBlock); - await writeFileEnsured(filePath, updated); - return; - } - - const needsTrailingNewline = existing.endsWith("\n") ? "" : "\n"; - await writeFileEnsured(filePath, `${existing}${needsTrailingNewline}\n${generatedBlock}`); -} - -function buildSummaryReport( - context: ProjectContext, - summary: string[], - openQuestions: string[], - artifactPaths: string[] -): string { - return `# Context Lite Summary - -- Repository: ${context.repoName} -- Output: ${context.outputPath} -- Generated at: ${context.scannedAt} - -## Real State - -${renderList(summary)} - -## Requires Manual Confirmation - -${renderList(openQuestions)} - -## Artifacts - -${renderList(artifactPaths.map((filePath) => `\`${path.relative(context.outputPath, filePath) || path.basename(filePath)}\``))} -`; -} - -function formatInlinePaths(paths: string[], limit = 6): string { - return paths.length > 0 ? paths.slice(0, limit).map((filePath) => `\`${filePath}\``).join(", ") : "Pendiente de confirmar"; -} - -function buildMasterContextPrompt( - context: ProjectContext, - summary: string[], - openQuestions: string[], - documentation: DocumentationSignals, - frontend: FrontendSignals, - backend: BackendSignals, - navigation: NavigationSignals -): string { - const repoShape = inferProjectShape(context.discovery); - const primaryDataSignal = pickPrimaryDataSignal(backend.dataSignals) ?? "Pendiente de confirmar"; - const coreDomains = documentation.domainEntries.slice(0, 5).map((entry) => { - const docNotes = entry.docFiles.length > 0 ? `docs=${formatInlinePaths(entry.docFiles, 2)}` : ""; - const codeNotes = entry.codeFiles.length > 0 ? `superficies=${formatInlinePaths(entry.codeFiles, 3)}` : ""; - const notes = [docNotes, codeNotes].filter(Boolean).join("; "); - return notes ? `\`${entry.label}\`: ${notes}` : `\`${entry.label}\``; - }); - const canonicalSources = documentation.authority.declaredCanonicalSources; - const operationalDocs = documentation.authority.primarySources; - const secondarySources = documentation.authority.secondarySources; - const ambiguityNotes = uniqueSorted([ - ...documentation.authority.ambiguousSources, - ...openQuestions - ]).slice(0, 6); - const stackSnapshot = [ - `Lenguajes: ${context.discovery.languages.join(", ") || "Pendiente de confirmar"}`, - `Frameworks: ${context.discovery.frameworks.join(", ") || "Pendiente de confirmar"}`, - `APIs/contratos: ${context.discovery.apis.join(", ") || "Pendiente de confirmar"}`, - `Rendering/UI: ${frontend.renderingStrategy ?? "Pendiente de confirmar"} / ${frontend.uiSignals.join(", ") || "sin sistema visual confirmado"}`, - `Auth/validación: ${backend.authSignals.join(", ") || "sin auth confirmada"} / ${backend.validationSignals.join(", ") || "sin validadores confirmados"}`, - `Actores/navegación: ${navigation.actorScopes.join(", ") || "sin scopes confirmados"} / ${navigation.navFiles.length > 0 ? `${navigation.navFiles.length} archivos de navegación` : "sin navegación confirmada"}` - ]; - const activeSurfaces = [ - `Contratos y schemas confirmados: ${formatInlinePaths(backend.contractFiles, 6)}`, - `Endpoints o handlers representativos: ${backend.endpointExamples.slice(0, 4).join(", ") || "Pendiente de confirmar"}`, - `Guardas y permisos detectados: ${navigation.guardSummaries.slice(0, 4).join(" | ") || formatInlinePaths(navigation.guardFiles, 4)}`, - `Testing/CI detectado: ${context.discovery.testing.join(", ") || "sin testing confirmado"} / ${context.discovery.ci.providers.join(", ") || "sin CI detectado"}` - ]; - - return `# Master Context Prompt - -Generado por \`project-brain context-lite\` el ${context.scannedAt} para \`${context.repoName}\`. - -Usa este prompt cuando necesites crear o refrescar el \`AI_CONTEXT/\` de este mismo proyecto sin empezar desde cero. El objetivo es continuar desde el análisis ya confirmado en esta corrida, no volver a inventar el proyecto. - -## Contexto confirmado en esta corrida - -- Repositorio objetivo: \`${context.targetPath}\` -- Forma detectada: ${repoShape} -- Fuente de verdad/persistencia prioritaria: ${primaryDataSignal} -${renderList(summary.slice(0, 6))} - -## Señales operativas ya detectadas - -${renderList(stackSnapshot)} - -## Fuentes que debes priorizar - -- Fuentes canónicas declaradas por esta corrida: ${formatInlinePaths(canonicalSources)} -- Documentos operativos de referencia: ${formatInlinePaths(operationalDocs)} -- Fuentes secundarias útiles: ${formatInlinePaths(secondarySources)} - -## Dominios detectados en esta corrida - -${renderList(coreDomains)} - -## Superficies activas ya confirmadas - -${renderList(activeSurfaces)} - -## Huecos o ambigüedades que siguen abiertas - -${renderList(ambiguityNotes)} - -## Prompt - -Actua como analista tecnico del repositorio actual. -Inspecciona este proyecto y crea o actualiza la carpeta \`AI_CONTEXT/\` para que otros agentes AI puedan trabajar aqui sin inventar arquitectura, flujos, reglas ni contratos. - -Objetivo: -Mantener un contexto operativo, breve y confiable del proyecto real. - -Reglas: -1. Inspecciona el repo completo antes de escribir: - - \`README*\` - - \`package.json\`, lockfiles y scripts - - estructura de \`src/\`, \`app/\`, \`pages/\`, \`components/\`, \`lib/\`, \`api/\` - - configuracion (\`env\`, auth, db, build, CI) - - migraciones, schemas, seeds, contratos, tests y docs existentes - - \`docs/\`, \`app/docs/\` y docs raiz como \`API.md\`, \`ARCHITECTURE.md\`, \`BUSINESS_RULES.md\`, \`FLOWS.md\` -2. No inventes nada. - - Si algo no esta confirmado en codigo o docs, marcalo como \`Pendiente de confirmar\`. - - Si hay contradiccion entre docs y codigo, prioriza el codigo, runtime o schema vigente y documenta la contradiccion. - - Usa primero las fuentes canónicas y operativas listadas arriba; no sustituyas esas fuentes por resúmenes viejos o markdown secundarios. -3. Usa rutas reales del repositorio. -4. Cada hallazgo importante debe incluir evidencia breve: - - \`Evidencia: ruta[:linea]\` -5. Si \`AI_CONTEXT/\` ya existe: - - actualiza solo lo impactado - - no borres notas manuales fuera de bloques generados - - conserva decisiones y learnings manuales si no fueron invalidados por evidencia nueva -6. No propongas refactors imaginarios. - - \`TASKS.md\` debe salir de evidencia real del repo. -7. Revisa primero los dominios ya detectados arriba y solo agrega dominios nuevos si aparecen confirmados en código o docs activos. -8. Si un hueco listado arriba sigue abierto tras la inspección, déjalo explícito y no lo cierres por inferencia. - -Archivos a crear o actualizar: -- \`AI_CONTEXT/system_overview.md\` -- \`AI_CONTEXT/domain_inventory.md\` -- \`AI_CONTEXT/modules_map.md\` -- \`AI_CONTEXT/frontend_architecture.md\` -- \`AI_CONTEXT/backend_flows_and_contracts.md\` -- \`AI_CONTEXT/ui_rules.md\` -- \`AI_CONTEXT/DECISIONS.md\` -- \`AI_CONTEXT/LEARNINGS.md\` -- \`AI_CONTEXT/TASKS.md\` - -Entrega final: -1. Crea o actualiza los archivos. -2. Resume en 10-15 lineas el estado real del proyecto. -3. Lista supuestos, huecos o zonas que requieren confirmacion manual. - -## Uso recomendado - -- Ejecuta este prompt dentro del repo: \`${context.targetPath}\` -- Usa como punto de partida el \`AI_CONTEXT/\` ya existente en este proyecto. -- Toma como base el snapshot confirmado de esta corrida: -${renderList(summary.slice(0, 6))} -- Si cuentas con output de \`project-brain context-lite\`, tratalo como base inicial y refresca solo lo que haya cambiado. -`; -} - -async function buildDocuments(context: ProjectContext): Promise { - const metadata = await loadRepoMetadata(context); - const flatDependencies = flattenLowerDependencies(context.discovery); - const frontend = detectFrontendSignals(context.discovery, flatDependencies); - const backend = detectBackendSignals(context.discovery, flatDependencies); - const documentation = await buildDocumentationSignals(context); - const navigation = await detectNavigationSignals(context); - backend.endpointExamples = await extractInlineEndpoints(context, backend); - - const systemOverview = buildSystemOverview(metadata, context.discovery, frontend, backend, documentation, navigation); - const domainInventory = buildDomainInventoryDocument(documentation); - const modulesMap = buildModulesMap(context.discovery, frontend, backend, documentation, navigation); - const frontendArchitecture = buildFrontendArchitecture(frontend, context.discovery, navigation); - const backendFlowsAndContracts = buildBackendFlowsAndContracts(backend, context.discovery, documentation); - const uiRules = buildUiRules(frontend, context.discovery, navigation); - - const openQuestions = uniqueSorted([ - ...systemOverview.pending, - ...domainInventory.pending, - ...modulesMap.pending, - ...frontendArchitecture.pending, - ...backendFlowsAndContracts.pending, - ...uiRules.pending - ]).slice(0, 12); - - const summary = uniqueSorted([ - ...systemOverview.summary, - `Dominios detectados: ${pickRepresentativeDomainLabels(documentation.domainEntries, 6).join(", ") || "sin dominios confirmados"}`, - `Módulos detectados: ${detectModuleSignals(context.discovery) - .slice(0, 5) - .map((signal) => signal.label) - .join(", ") || "pendiente de confirmar"}`, - `Navegación/actores: ${navigation.actorScopes.join(", ") || "sin scopes de navegación confirmados"} / ${navigation.navFiles.length > 0 ? `${navigation.navFiles.length} archivos de navegación` : "sin navegación confirmada"}`, - `Auth/validación: ${backend.authSignals.join(", ") || "sin auth confirmada"} / ${backend.validationSignals.join(", ") || "sin validadores confirmados"}`, - `Contratos backend: ${backend.contractFiles.join(", ") || "sin contratos confirmados"}`, - `UI base: ${frontend.uiSignals.join(", ") || "sin sistema visual confirmado"}`, - `Huecos principales: ${openQuestions.slice(0, 2).join(" | ") || "sin huecos críticos detectados"}` - ]).slice(0, 12); - const masterContextPrompt = buildMasterContextPrompt( - context, - summary, - openQuestions, - documentation, - frontend, - backend, - navigation - ); - - return { - systemOverview: systemOverview.content, - domainInventory: domainInventory.content, - modulesMap: modulesMap.content, - frontendArchitecture: frontendArchitecture.content, - backendFlowsAndContracts: backendFlowsAndContracts.content, - uiRules: uiRules.content, - masterContextPrompt, - decisionsBlock: buildGeneratedDecisions(context.discovery, frontend, backend, documentation), - learningsBlock: buildGeneratedLearnings(context.discovery, frontend, backend, documentation, navigation), - tasksBlock: buildGeneratedTasks(context.discovery, frontend, backend, documentation, navigation, openQuestions), - summary, - openQuestions - }; -} - -export async function writeContextLiteArtifacts(context: ProjectContext): Promise { - const documents = await buildDocuments(context); - const memoryDir = context.memoryDir; - const reportPath = path.join(context.reportsDir, "context_lite.md"); - const artifactPaths = [ - path.join(memoryDir, "system_overview.md"), - path.join(memoryDir, "domain_inventory.md"), - path.join(memoryDir, "modules_map.md"), - path.join(memoryDir, "frontend_architecture.md"), - path.join(memoryDir, "backend_flows_and_contracts.md"), - path.join(memoryDir, "ui_rules.md"), - path.join(memoryDir, "MASTER_CONTEXT_PROMPT.md"), - path.join(memoryDir, "DECISIONS.md"), - path.join(memoryDir, "LEARNINGS.md"), - path.join(memoryDir, "TASKS.md") - ]; - - await writeFileEnsured(path.join(memoryDir, "system_overview.md"), documents.systemOverview); - await writeFileEnsured(path.join(memoryDir, "domain_inventory.md"), documents.domainInventory); - await writeFileEnsured(path.join(memoryDir, "modules_map.md"), documents.modulesMap); - await writeFileEnsured(path.join(memoryDir, "frontend_architecture.md"), documents.frontendArchitecture); - await writeFileEnsured(path.join(memoryDir, "backend_flows_and_contracts.md"), documents.backendFlowsAndContracts); - await writeFileEnsured(path.join(memoryDir, "ui_rules.md"), documents.uiRules); - await writeFileEnsured(path.join(memoryDir, "MASTER_CONTEXT_PROMPT.md"), documents.masterContextPrompt); - await upsertGeneratedSection(path.join(memoryDir, "DECISIONS.md"), "DECISIONS", documents.decisionsBlock); - await upsertGeneratedSection(path.join(memoryDir, "LEARNINGS.md"), "LEARNINGS", documents.learningsBlock); - await upsertGeneratedSection(path.join(memoryDir, "TASKS.md"), "TASKS", documents.tasksBlock); - await writeFileEnsured(reportPath, buildSummaryReport(context, documents.summary, documents.openQuestions, artifactPaths)); - - return { - context, - reportPath, - artifactPaths, - summary: documents.summary, - openQuestions: documents.openQuestions - }; -} diff --git a/core/deepagents_swarm/index.ts b/core/deepagents_swarm/index.ts deleted file mode 100644 index a2a3008..0000000 --- a/core/deepagents_swarm/index.ts +++ /dev/null @@ -1,864 +0,0 @@ -import { promises as fs } from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -/* - * Experimental Deep Agents engine for repository analysis. - * - * Use `deepagents` when a run needs a richer scratch workspace, repository tools, - * and subagent-style exploration. Prefer the bounded engine for default CLI runs, - * deterministic budgeting, predictable retries, and cheaper local execution. - * - * External requirements: Ollama must be reachable, at least one configured local - * model must be installed, and the `deepagents`, LangChain, and ChatOllama - * packages must be available at runtime. - * - * Known limitations: this engine is analysis-only, exposes less queue/retry - * control than the bounded runtime, and normalizes unstructured Deep Agents - * responses into conservative `SwarmRunResult` fallbacks. - */ - -import { ChatOllama } from "@langchain/ollama"; -import { FilesystemBackend, createDeepAgent, type SubAgent } from "deepagents"; -import { tool, toolStrategy } from "langchain"; -import { z } from "zod"; - -import { buildRepoSummary } from "../../agents/ai-support"; -import { ensureDir, readTextSafe, toPosixPath, walkDirectory, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ProjectContext, SwarmPlanTask, SwarmRunResult, SwarmWorkerResult } from "../../shared/types"; -import type { ModelInventory } from "../ai_router/router"; -import type { TokenPreset } from "../token_policy"; - -interface DeepAgentsAssistant { - listModels?: () => Promise; -} - -interface DeepAgentsSwarmOptions { - parallelism?: number; - chunkSize?: number; - preset?: TokenPreset; - taskTimeoutMs?: number; - maxRetries?: number; - plannerTimeoutMs?: number; - synthesisTimeoutMs?: number; - runTimeoutMs?: number; - maxQueuedTasks?: number; - scopeBias?: SwarmRunResult["chunking"]["scopeBias"]; -} - -const SWARM_TASK_PROFILES = ["worker", "reviewer", "reasoning", "planner", "synthesizer"] as const; -const INSPECTABLE_EXTENSIONS = new Set([ - ".ts", - ".tsx", - ".js", - ".jsx", - ".mjs", - ".cjs", - ".json", - ".md", - ".yml", - ".yaml", - ".toml", - ".txt", - ".py", - ".go", - ".rs", - ".java", - ".rb", - ".php", - ".sh", - ".sql" -]); -const INSPECTABLE_BASENAMES = new Set([ - "Dockerfile", - "package.json", - "tsconfig.json", - "README.md", - "Makefile", - ".env.example" -]); - -const subagentSummarySchema = z.object({ - title: z.string(), - profile: z.enum(SWARM_TASK_PROFILES).default("reasoning"), - summary: z.string(), - findings: z.array(z.string()).default([]), - recommendations: z.array(z.string()).default([]) -}); - -const deepAgentsSwarmResponseSchema = z.object({ - headline: z.string(), - summary: z.string(), - findings: z.array(z.string()).default([]), - priorities: z.array(z.string()).default([]), - next_steps: z.array(z.string()).default([]), - task_summaries: z.array(subagentSummarySchema).default([]) -}); - -type DeepAgentsSwarmResponse = z.infer; - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function unique(values: string[]): string[] { - return [...new Set(values.filter((value) => value.trim().length > 0))]; -} - -function isInspectableFile(filePath: string): boolean { - const baseName = path.basename(filePath); - return INSPECTABLE_BASENAMES.has(baseName) || INSPECTABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase()); -} - -function resolveWithinRoot(rootPath: string, requestedPath: string): string { - const normalizedRequest = requestedPath.trim() || "."; - const resolvedRoot = path.resolve(rootPath); - const candidate = path.resolve(rootPath, normalizedRequest); - - if (candidate !== resolvedRoot && !candidate.startsWith(`${resolvedRoot}${path.sep}`)) { - throw new Error(`Path escapes the allowed root: ${requestedPath}`); - } - - return candidate; -} - -function relativeFrom(rootPath: string, absolutePath: string): string { - return toPosixPath(path.relative(rootPath, absolutePath) || "."); -} - -function clampLineWindow(startLine: number, endLine: number, maxSpan = 250): { startLine: number; endLine: number } { - const safeStart = Number.isFinite(startLine) ? Math.max(1, Math.trunc(startLine)) : 1; - const safeEnd = Number.isFinite(endLine) ? Math.max(safeStart, Math.trunc(endLine)) : safeStart + maxSpan - 1; - - if (safeEnd - safeStart + 1 > maxSpan) { - return { - startLine: safeStart, - endLine: safeStart + maxSpan - 1 - }; - } - - return { - startLine: safeStart, - endLine: safeEnd - }; -} - -function excerptLines(content: string, startLine: number, endLine: number): string { - const { startLine: boundedStart, endLine: boundedEnd } = clampLineWindow(startLine, endLine); - const lines = content.split(/\r?\n/); - const selected = lines.slice(boundedStart - 1, boundedEnd); - - if (selected.length === 0) { - return "No content available in the requested line range."; - } - - return selected.map((line, index) => `${boundedStart + index}: ${line}`).join("\n"); -} - -function extractJsonObject(input: string): Record | undefined { - const trimmed = input.trim(); - const candidate = trimmed.startsWith("```") - ? trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "") - : trimmed; - - try { - const parsed = JSON.parse(candidate) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : undefined; - } catch { - const start = candidate.indexOf("{"); - const end = candidate.lastIndexOf("}"); - if (start < 0 || end <= start) { - return undefined; - } - - try { - const parsed = JSON.parse(candidate.slice(start, end + 1)) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : undefined; - } catch { - return undefined; - } - } -} - -function extractMessageText(content: unknown): string { - if (typeof content === "string") { - return content; - } - - if (!Array.isArray(content)) { - return ""; - } - - return content - .map((entry) => { - if (typeof entry === "string") { - return entry; - } - - if (!entry || typeof entry !== "object") { - return ""; - } - - const block = entry as Record; - if (typeof block.text === "string") { - return block.text; - } - if (typeof block.content === "string") { - return block.content; - } - - return ""; - }) - .filter(Boolean) - .join("\n"); -} - -function normalizeDeepAgentsResponse(raw: unknown, intent: string): DeepAgentsSwarmResponse { - if (raw && typeof raw === "object" && !Array.isArray(raw)) { - const record = raw as Record; - const structured = record.structuredResponse; - const structuredParsed = deepAgentsSwarmResponseSchema.safeParse(structured); - if (structuredParsed.success) { - return structuredParsed.data; - } - - if (Array.isArray(record.messages) && record.messages.length > 0) { - const lastMessage = record.messages[record.messages.length - 1]; - if (lastMessage && typeof lastMessage === "object") { - const text = extractMessageText((lastMessage as Record).content); - const extracted = text ? extractJsonObject(text) : undefined; - const extractedParsed = deepAgentsSwarmResponseSchema.safeParse(extracted); - if (extractedParsed.success) { - return extractedParsed.data; - } - } - } - } - - return { - headline: `The deepagents swarm completed a partial review for: ${intent}`, - summary: "Deep Agents returned an unstructured answer, so project-brain preserved a conservative fallback summary.", - findings: ["The experimental deepagents engine did not return the expected structured payload."], - priorities: ["Tighten the response schema or system prompt if this engine is promoted."], - next_steps: ["Inspect the deepagents workspace artifacts and rerun with a narrower intent."], - task_summaries: [] - }; -} - -function buildRawResultPreview(raw: unknown): Record | undefined { - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - return undefined; - } - - const record = raw as Record; - const preview: Record = {}; - - if (record.structuredResponse && typeof record.structuredResponse === "object") { - preview.structuredResponse = record.structuredResponse; - } - - if (Array.isArray(record.messages)) { - preview.messageCount = record.messages.length; - const lastMessage = record.messages[record.messages.length - 1]; - if (lastMessage && typeof lastMessage === "object") { - const finalMessage = extractMessageText((lastMessage as Record).content).trim(); - if (finalMessage) { - preview.finalMessage = finalMessage.slice(0, 4000); - } - } - } - - return Object.keys(preview).length > 0 ? preview : undefined; -} - -function derivePressure(cpuCount: number, loadAverage1m: number, freeMemoryMb: number): SwarmRunResult["parallelism"]["pressure"] { - if (loadAverage1m >= Math.max(cpuCount * 0.75, 6) || freeMemoryMb < 1024) { - return "high"; - } - - if (loadAverage1m >= Math.max(cpuCount * 0.45, 3) || freeMemoryMb < 2048) { - return "medium"; - } - - return "low"; -} - -function resolveScopeHints(intent: string): string[] { - const matches = intent.match(/[A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+/g) ?? []; - return unique(matches.map((match) => match.replace(/^\.\/+/, "").trim())).slice(0, 5); -} - -function defaultTaskPlan(intent: string): SwarmPlanTask[] { - return [ - { - taskId: "deep-map", - title: "Map repository shape", - goal: `Identify stack, boundaries, and hotspots for: ${intent}`, - profile: "reviewer", - deliverable: "Repo map with relevant hotspots." - }, - { - taskId: "deep-risk-review", - title: "Review critical risks", - goal: "Surface grounded architectural, testing, and delivery risks from repo evidence.", - profile: "reasoning", - deliverable: "Evidence-backed risk review." - }, - { - taskId: "deep-priorities", - title: "Prioritize next steps", - goal: "Turn the findings into a short, high-signal action plan.", - profile: "planner", - deliverable: "Ordered next steps." - } - ]; -} - -function renderDeepAgentsReport( - context: ProjectContext, - intent: string, - workspacePath: string, - result: DeepAgentsSwarmResponse, - tasks: SwarmPlanTask[], - workerResults: SwarmWorkerResult[], - model: { - provider: string; - model: string; - residency: string; - } -): string { - return `# DeepAgents Swarm Run - -- Engine: deepagents -- Repository: ${context.repoName} -- Intent: ${intent} -- Model: ${model.model} -- Provider: ${model.provider} -- Residency: ${model.residency} -- Workspace: ${workspacePath} - -## Headline - -${result.headline} - -## Summary - -${result.summary} - -## Findings - -${renderList(result.findings)} - -## Priorities - -${renderList(result.priorities)} - -## Next Steps - -${renderList(result.next_steps)} - -## Planned Tasks - -${tasks - .map((task) => `- ${task.title} [${task.profile}] -> ${task.deliverable}`) - .join("\n")} - -## Task Outputs - -${workerResults - .map( - (worker) => `### ${worker.title} - -- Profile: ${worker.profile} -- Status: ${worker.status} -- Model: ${worker.model} -- Scope: ${worker.scopePaths.join(", ") || "."} -- Summary: ${worker.summary} - -Findings: -${renderList(worker.findings)} - -Recommendations: -${renderList(worker.recommendations)}` - ) - .join("\n\n")} -`; -} - -function createRepoTools(context: ProjectContext) { - const repoRoot = context.targetPath; - const outputRoot = context.outputPath; - - const repoOverview = tool( - async () => - [ - buildRepoSummary(context), - `Target path: ${repoRoot}`, - `Output path: ${outputRoot}`, - `Top-level directories: ${context.discovery.structure.topLevelDirectories.join(", ") || "None"}`, - `Sample files: ${context.discovery.structure.sampleFiles.slice(0, 20).join(", ") || "None"}` - ].join("\n"), - { - name: "get_repo_overview", - description: "Return a compact overview of the repository, stack, and discovered hotspots.", - schema: z.object({}) - } - ); - - const repoDirectory = tool( - async ({ dir = ".", maxEntries = 80 }: { dir?: string; maxEntries?: number }) => { - const absoluteDir = resolveWithinRoot(repoRoot, dir); - const entries = await fs.readdir(absoluteDir, { withFileTypes: true }); - - return entries - .sort((left, right) => left.name.localeCompare(right.name)) - .slice(0, maxEntries) - .map((entry) => `${entry.isDirectory() ? "[dir]" : "[file]"} ${toPosixPath(path.join(dir, entry.name))}`) - .join("\n") || "Directory is empty."; - }, - { - name: "list_repo_directory", - description: "List files and directories under a repository path. Paths are relative to the repo root.", - schema: z.object({ - dir: z.string().optional().default("."), - maxEntries: z.number().int().min(1).max(200).optional().default(80) - }) - } - ); - - const repoFile = tool( - async ({ - filePath, - startLine = 1, - endLine = 200 - }: { - filePath: string; - startLine?: number; - endLine?: number; - }) => { - const absolutePath = resolveWithinRoot(repoRoot, filePath); - const content = await readTextSafe(absolutePath); - if (!content) { - return `No readable content found for ${filePath}.`; - } - - return excerptLines(content, startLine, endLine); - }, - { - name: "read_repo_file", - description: "Read a file from the repository. Use relative paths from the repo root and bounded line ranges.", - schema: z.object({ - filePath: z.string(), - startLine: z.number().int().min(1).optional().default(1), - endLine: z.number().int().min(1).optional().default(200) - }) - } - ); - - const repoSearch = tool( - async ({ - query, - scope = ".", - maxResults = 40 - }: { - query: string; - scope?: string; - maxResults?: number; - }) => { - const absoluteScope = resolveWithinRoot(repoRoot, scope); - const stats = await fs.stat(absoluteScope); - const candidates = stats.isDirectory() - ? await walkDirectory(absoluteScope, 1500) - : [path.basename(absoluteScope)]; - const results: string[] = []; - const normalizedQuery = query.toLowerCase(); - - for (const relativePath of candidates) { - if (results.length >= maxResults) { - break; - } - - const absolutePath = stats.isDirectory() ? path.join(absoluteScope, relativePath) : absoluteScope; - const repoRelativePath = stats.isDirectory() - ? relativeFrom(repoRoot, absolutePath) - : relativeFrom(repoRoot, absoluteScope); - - if (!isInspectableFile(repoRelativePath)) { - continue; - } - - const content = await readTextSafe(absolutePath); - if (!content) { - continue; - } - - const lines = content.split(/\r?\n/); - for (let index = 0; index < lines.length; index += 1) { - if (lines[index].toLowerCase().includes(normalizedQuery)) { - results.push(`${repoRelativePath}:${index + 1}: ${lines[index].trim()}`); - if (results.length >= maxResults) { - break; - } - } - } - } - - return results.length > 0 ? results.join("\n") : `No matches found for "${query}".`; - }, - { - name: "search_repo", - description: "Search for a literal string across repository files. Use this to locate symbols, configs, and implementation hotspots.", - schema: z.object({ - query: z.string(), - scope: z.string().optional().default("."), - maxResults: z.number().int().min(1).max(100).optional().default(40) - }) - } - ); - - const outputArtifact = tool( - async ({ - filePath, - startLine = 1, - endLine = 200 - }: { - filePath: string; - startLine?: number; - endLine?: number; - }) => { - const absolutePath = resolveWithinRoot(outputRoot, filePath); - const content = await readTextSafe(absolutePath); - if (!content) { - return `No readable artifact found for ${filePath}.`; - } - - return excerptLines(content, startLine, endLine); - }, - { - name: "read_output_artifact", - description: "Read a generated project-brain artifact from the output directory, such as reports or AI_CONTEXT files.", - schema: z.object({ - filePath: z.string(), - startLine: z.number().int().min(1).optional().default(1), - endLine: z.number().int().min(1).optional().default(200) - }) - } - ); - - return [repoOverview, repoDirectory, repoFile, repoSearch, outputArtifact]; -} - -function buildSubagents(): SubAgent[] { - return [ - { - name: "repo_mapper", - description: "Maps stack, directory boundaries, entrypoints, and likely hotspots.", - systemPrompt: - "Map the repository shape with evidence. Focus on stack, boundaries, entrypoints, and the most relevant code areas tied to the user intent.", - responseFormat: toolStrategy(subagentSummarySchema) - }, - { - name: "risk_reviewer", - description: "Reviews architecture, testing, operability, and delivery risks grounded in repo evidence.", - systemPrompt: - "Review critical technical risks. Prioritize grounded findings over speculation and tie every concern to repo evidence.", - responseFormat: toolStrategy(subagentSummarySchema) - }, - { - name: "delivery_planner", - description: "Turns evidence into a short, prioritized implementation plan.", - systemPrompt: - "Convert the mapped evidence into a pragmatic, short, prioritized next-step plan. Prefer small, high-leverage changes.", - responseFormat: toolStrategy(subagentSummarySchema) - } - ]; -} - -function resolveModelSelection(inventory: ModelInventory): { - provider: string; - model: string; - residency: string; - offlineCapable: boolean; -} { - const candidates = unique([ - inventory.resolvedProfiles.planner, - inventory.resolvedProfiles.reasoning, - inventory.resolvedProfiles.reviewer, - inventory.localConfigured, - inventory.fallbackConfigured - ]); - - for (const candidate of candidates) { - const descriptor = inventory.availableModels.find((model) => model.name === candidate); - if (descriptor) { - return { - provider: "ollama", - model: descriptor.name, - residency: descriptor.residency, - offlineCapable: descriptor.offlineCapable - }; - } - } - - const fallback = inventory.availableModels[0]; - if (fallback) { - return { - provider: "ollama", - model: fallback.name, - residency: fallback.residency, - offlineCapable: fallback.offlineCapable - }; - } - - throw new Error("Deep Agents swarm requires at least one Ollama model. Run `project-brain models` to verify availability."); -} - -function resolvePresetParallelism(preset: TokenPreset | undefined): number { - if (preset === "cheap") { - return 1; - } - if (preset === "thorough") { - return 3; - } - return 2; -} - -function clampParallelism(value: number): number { - return Math.min(4, Math.max(1, Math.trunc(value))); -} - -export async function runDeepAgentsSwarm( - context: ProjectContext, - intent: string, - assistant: DeepAgentsAssistant, - options: DeepAgentsSwarmOptions = {} -): Promise { - if (!intent.trim()) { - throw new Error("Deep Agents swarm requires a non-empty intent."); - } - if (!assistant.listModels) { - throw new Error("Deep Agents swarm requires model inventory support from the AI router."); - } - - const inventory = await assistant.listModels(); - const modelSelection = resolveModelSelection(inventory); - const workspacePath = path.join(context.memoryDir, "swarm", "deepagents_workspace"); - const reportPath = path.join(context.reportsDir, "swarm_run.md"); - const memoryPath = path.join(context.memoryDir, "swarm", "swarm_run.json"); - const scopeHints = resolveScopeHints(intent); - const cpuCount = Math.max(1, os.cpus().length); - const loadAverage1m = os.loadavg()[0] ?? 0; - const freeMemoryMb = Math.round(os.freemem() / (1024 * 1024)); - const totalMemoryMb = Math.round(os.totalmem() / (1024 * 1024)); - const runTimeoutMs = options.runTimeoutMs ?? 90_000; - const selectedParallelism = clampParallelism(options.parallelism ?? resolvePresetParallelism(options.preset)); - - await ensureDir(workspacePath); - - const backend = new FilesystemBackend({ - rootDir: workspacePath, - virtualMode: true, - maxFileSizeMb: 2 - }); - - const model = new ChatOllama({ - model: modelSelection.model, - baseUrl: process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434", - temperature: 0, - think: false - }); - - const agent = createDeepAgent({ - name: "project-brain-deepagents-swarm", - model, - backend, - tools: createRepoTools(context), - subagents: buildSubagents(), - responseFormat: toolStrategy(deepAgentsSwarmResponseSchema), - systemPrompt: [ - "You are the experimental Deep Agents swarm engine for project-brain.", - "Your job is to inspect a repository in read-only mode, keep working notes in the isolated workspace, delegate when useful, and return grounded implementation guidance.", - "Constraints:", - "- Never modify files inside the target repository.", - "- Never propose automatic application of patches.", - "- Use repository tools to inspect code and generated project-brain artifacts.", - "- Use built-in filesystem tools only for scratch notes inside the isolated deepagents workspace.", - "- Prefer evidence over speculation and keep recommendations concrete.", - "- For non-trivial tasks, use write_todos and delegate to at least one specialist subagent before finalizing.", - scopeHints.length > 0 ? `Scope hints from the user intent: ${scopeHints.join(", ")}` : "No explicit scope hints were present in the user intent.", - `Repository summary:\n${buildRepoSummary(context)}` - ].join("\n") - }); - - let normalized: DeepAgentsSwarmResponse; - let rawResult: unknown; - let runTimedOut = false; - - try { - rawResult = await agent.invoke( - { - messages: [ - { - role: "user", - content: [ - `Intent: ${intent}`, - "", - "Analyze this repository the way project-brain needs:", - "- map the repo shape and the most relevant areas", - "- surface concrete technical risks tied to evidence", - "- prioritize a short set of high-leverage next steps", - "", - "Return structured JSON only." - ].join("\n") - } - ] - }, - { - signal: AbortSignal.timeout(runTimeoutMs) - } - ); - - normalized = normalizeDeepAgentsResponse(rawResult, intent); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (!/abort|timeout/i.test(message)) { - throw error; - } - - runTimedOut = true; - normalized = { - headline: `The deepagents swarm hit its time budget for: ${intent}`, - summary: "The experimental deepagents run timed out before producing a complete structured result.", - findings: ["The deepagents engine exceeded the configured global timeout."], - priorities: ["Tighten the intent or increase the run timeout for deep analysis."], - next_steps: ["Retry with a narrower scope or a larger timeout budget."], - task_summaries: [] - }; - } - - const tasks = normalized.task_summaries.length > 0 - ? normalized.task_summaries.map((summary, index): SwarmPlanTask => ({ - taskId: `deepagents-task-${index + 1}`, - title: summary.title, - goal: summary.summary, - profile: summary.profile, - deliverable: summary.recommendations[0] ?? "Grounded repo analysis." - })) - : defaultTaskPlan(intent); - - const workerResults = (normalized.task_summaries.length > 0 ? normalized.task_summaries : [ - { - title: "Deep Agents synthesis", - profile: "reasoning" as const, - summary: normalized.summary, - findings: normalized.findings, - recommendations: [...normalized.priorities, ...normalized.next_steps] - } - ]).map((summary, index): SwarmWorkerResult => ({ - taskId: `deepagents-task-${index + 1}`, - parentTaskId: `deepagents-task-${index + 1}`, - chunkId: "deepagents-workspace", - attempt: 1, - status: runTimedOut ? "timed_out" : "completed", - title: summary.title, - profile: summary.profile, - scopePaths: scopeHints.length > 0 ? scopeHints : ["."], - provider: modelSelection.provider, - model: modelSelection.model, - residency: modelSelection.residency, - summary: summary.summary, - findings: summary.findings, - recommendations: summary.recommendations, - error: runTimedOut ? "Deep Agents run timeout exceeded." : undefined - })); - - const resilience: SwarmRunResult["resilience"] = { - runTimeoutMs, - requestedRunTimeoutMs: options.runTimeoutMs, - plannerTimeoutMs: options.plannerTimeoutMs ?? runTimeoutMs, - requestedPlannerTimeoutMs: options.plannerTimeoutMs, - synthesisTimeoutMs: options.synthesisTimeoutMs ?? runTimeoutMs, - requestedSynthesisTimeoutMs: options.synthesisTimeoutMs, - taskTimeoutMs: options.taskTimeoutMs ?? runTimeoutMs, - requestedTaskTimeoutMs: options.taskTimeoutMs, - maxRetries: options.maxRetries ?? 0, - queueBudget: options.maxQueuedTasks ?? tasks.length, - requestedQueueBudget: options.maxQueuedTasks, - plannerTimedOut: false, - synthesisTimedOut: false, - runTimedOut, - timedOutTasks: runTimedOut ? workerResults.length : 0, - retriedTasks: 0, - splitTasks: 0, - failedTasks: 0, - droppedTasks: 0, - localBudgetMode: modelSelection.offlineCapable, - adaptiveQueueBudget: false - }; - - const chunking: SwarmRunResult["chunking"] = { - selectedChunkSize: options.chunkSize ?? 1, - requestedChunkSize: options.chunkSize, - scopeUnits: Math.max(context.discovery.structure.topLevelDirectories.length, 1), - scopeChunks: 1, - queuedTasks: tasks.length, - queueStrategy: "round-robin", - scopeBias: options.scopeBias ?? "source-first", - scopeHints - }; - - const parallelism: SwarmRunResult["parallelism"] = { - selected: selectedParallelism, - requested: options.parallelism, - cpuCount, - loadAverage1m, - freeMemoryMb, - totalMemoryMb, - pressure: derivePressure(cpuCount, loadAverage1m, freeMemoryMb) - }; - - await writeFileEnsured( - reportPath, - renderDeepAgentsReport(context, intent, workspacePath, normalized, tasks, workerResults, modelSelection) - ); - await writeJsonEnsured(memoryPath, { - engine: "deepagents", - repoName: context.repoName, - intent, - workspacePath, - model: modelSelection, - resilience, - chunking, - parallelism, - structuredResponse: normalized, - resultPreview: buildRawResultPreview(rawResult) - }); - - return { - engine: "deepagents", - context, - intent, - reportPath, - memoryPath, - resilience, - chunking, - parallelism, - planner: { - provider: modelSelection.provider, - model: modelSelection.model, - residency: modelSelection.residency, - overview: normalized.summary - }, - tasks, - workerResults, - synthesis: { - provider: modelSelection.provider, - model: modelSelection.model, - residency: modelSelection.residency, - headline: normalized.headline, - summary: normalized.summary, - priorities: unique([...normalized.priorities, ...normalized.findings]).slice(0, 8), - nextSteps: normalized.next_steps - } - }; -} diff --git a/core/discovery_engine/index.ts b/core/discovery_engine/index.ts deleted file mode 100644 index 7c9fa6e..0000000 --- a/core/discovery_engine/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { scanApis } from "../../analysis/api_scanner"; -import { scanDependencies } from "../../analysis/dependency_scanner"; -import { scanInfrastructure } from "../../analysis/infra_scanner"; -import { scanRepositoryStructure } from "../../analysis/repo_scanner"; -import { detectCi } from "../../integrations/ci"; -import { detectGitIntegration } from "../../integrations/git"; -import { detectLogging } from "../../integrations/logs"; -import { detectMetrics } from "../../integrations/metrics"; -import { StructuredLogger } from "../../shared/logger"; -import { uniqueSorted } from "../../shared/fs-utils"; -import type { DiscoveryResult } from "../../shared/types"; - -function buildRecommendations(discovery: Omit): string[] { - const recommendations: string[] = []; - - if (discovery.ci.providers.length === 0) { - recommendations.push("Add a CI workflow to run validation on every change."); - } - - if (discovery.testing.length === 0) { - recommendations.push("Establish an automated testing baseline for the primary runtime."); - } - - if (discovery.apis.includes("REST") && !discovery.apis.includes("OpenAPI")) { - recommendations.push("Publish an OpenAPI contract for the main API surface."); - } - - if (!discovery.logging.structured) { - recommendations.push("Adopt structured logging for traceable operational diagnostics."); - } - - if (discovery.metrics.tools.length === 0) { - recommendations.push("Add metrics or tracing instrumentation for critical execution paths."); - } - - return uniqueSorted(recommendations); -} - -function detectStructuralFrameworks(files: string[]): string[] { - const frameworks: string[] = []; - - if ( - files.some((filePath) => /(^|\/)core\/kumbia\//i.test(filePath)) || - files.some((filePath) => /(^|\/)(?:default\/)?app\/config\/config\.php$/i.test(filePath)) - ) { - frameworks.push("KumbiaPHP"); - } - - return uniqueSorted(frameworks); -} - -export class DiscoveryEngine { - private readonly logger = new StructuredLogger("discovery-engine"); - - async analyze(targetPath: string, options?: { excludePaths?: string[] }): Promise { - this.logger.info("Scanning repository", { - component: "discovery", - action: "scan_start", - targetPath, - excludePaths: options?.excludePaths ?? [] - }); - - const repoScan = await scanRepositoryStructure(targetPath, options?.excludePaths ?? []); - const dependencyScan = await scanDependencies(targetPath, repoScan.files); - const frameworks = uniqueSorted([...dependencyScan.frameworks, ...detectStructuralFrameworks(repoScan.files)]); - const apiScan = scanApis(repoScan.files, dependencyScan.dependencies, frameworks); - const infraScan = await scanInfrastructure(targetPath, repoScan.files); - const git = detectGitIntegration(targetPath, repoScan.structure.submodules.length > 0); - const ci = detectCi(repoScan.files); - const logging = detectLogging(repoScan.files, dependencyScan.dependencies); - const metrics = detectMetrics(repoScan.files, dependencyScan.dependencies); - - const discoveryBase: Omit = { - repoName: repoScan.repoName, - targetPath, - scannedAt: repoScan.scannedAt, - files: repoScan.files, - structure: repoScan.structure, - languages: repoScan.languages, - frameworks, - apis: apiScan.apis, - infrastructure: infraScan.infrastructure, - testing: dependencyScan.testing, - dependencies: dependencyScan.dependencies, - manifests: dependencyScan.manifests, - apiFiles: apiScan.apiFiles, - infraFiles: infraScan.infraFiles, - dockerStageCount: infraScan.dockerStageCount, - git, - ci, - logging, - metrics - }; - - const discovery = { - ...discoveryBase, - recommendations: buildRecommendations(discoveryBase) - }; - - this.logger.info("Repository scan completed", { - component: "discovery", - action: "scan_complete", - repoName: discovery.repoName, - files: discovery.structure.fileCount, - frameworks: discovery.frameworks - }); - - return discovery; - } -} diff --git a/core/doctor/index.ts b/core/doctor/index.ts deleted file mode 100644 index d21ec94..0000000 --- a/core/doctor/index.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { spawn } from "node:child_process"; -import path from "node:path"; -import { existsSync, readFileSync } from "node:fs"; - -import type { ModelInventory } from "../ai_router/router"; -import { deriveDoctorSuggestions } from "../reaction_engine"; -import { ensureDir, fileExists, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - DoctorCheck, - DoctorCheckStatus, - DoctorResult, - DoctorSetupItem, - ProjectContext, - SuggestedAction -} from "../../shared/types"; - -interface DoctorAssistant { - listModels?: () => Promise; -} - -interface CommandProbeResult { - ok: boolean; - exitCode: number | null; - stdout: string; - stderr: string; -} - -interface DoctorDeps { - runCommand?: (command: string, args: string[], options?: { cwd?: string; timeoutMs?: number }) => Promise; - projectRoot?: string; -} - -interface RuntimeToolchainSpec { - id: string; - label: string; - summary: string; - installHint: string; - command: string; - args: string[]; - appliesToTarget: boolean; - detectedBy: string[]; - required?: boolean; -} - -function statusRank(status: DoctorCheckStatus): number { - if (status === "fail") { - return 3; - } - if (status === "warn") { - return 2; - } - return 1; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderSuggestions(suggestions: SuggestedAction[]): string { - return suggestions.length > 0 - ? suggestions - .map( - (suggestion) => `### ${suggestion.label} - -- Priority: ${suggestion.priority.toUpperCase()} -- Command: \`${suggestion.command}\` -- Rationale: ${suggestion.rationale}` - ) - .join("\n\n") - : "No immediate follow-up actions suggested."; -} - -function renderSetupItems(setupItems: DoctorSetupItem[]): string { - const sections: Array<{ title: string; items: DoctorSetupItem[] }> = [ - { title: "Required Local Runtime", items: setupItems.filter((item) => item.tier === "required") }, - { title: "Recommended For This Target", items: setupItems.filter((item) => item.tier === "recommended") }, - { title: "Optional Open-Source Expansion", items: setupItems.filter((item) => item.tier === "optional") } - ]; - - return sections - .filter((section) => section.items.length > 0) - .map( - (section) => `### ${section.title} - -${section.items - .map( - (item) => `#### ${item.label} - -- Status: ${item.status.toUpperCase()} -- Summary: ${item.summary} -- Install / enable: ${item.installHint} - -Details: -${renderList(item.details)}` - ) - .join("\n\n")}` - ) - .join("\n\n"); -} - -function withDefaultTag(model: string): string { - return model.includes(":") ? model : `${model}:latest`; -} - -function matchesModel(candidate: string, available: string[]): boolean { - const normalized = withDefaultTag(candidate); - return available.some((model) => model === candidate || model === normalized || model.startsWith(`${candidate}:`)); -} - -function buildCheck(id: string, label: string, status: DoctorCheckStatus, summary: string, details: string[] = []): DoctorCheck { - return { id, label, status, summary, details }; -} - -function countByStatus(checks: DoctorCheck[], status: DoctorCheckStatus): number { - return checks.filter((check) => check.status === status).length; -} - -function buildHeadline(checks: DoctorCheck[]): string { - const failed = countByStatus(checks, "fail"); - const warnings = countByStatus(checks, "warn"); - - if (failed > 0) { - return `Doctor found ${failed} failing checks and ${warnings} warnings.`; - } - - if (warnings > 0) { - return `Doctor found ${warnings} warnings and no failing checks.`; - } - - return "Doctor found no failing checks."; -} - -function resolveProjectBrainRoot(startDir: string): string { - let current = startDir; - - while (true) { - const candidate = path.join(current, "package.json"); - if (existsSync(candidate)) { - try { - const parsed = JSON.parse(readFileSync(candidate, "utf8")) as { name?: string }; - if (parsed.name === "project-brain") { - return current; - } - } catch { - // Keep walking. - } - } - - const parent = path.dirname(current); - if (parent === current) { - return process.cwd(); - } - current = parent; - } -} - -async function defaultRunCommand( - command: string, - args: string[], - options: { cwd?: string; timeoutMs?: number } = {} -): Promise { - return new Promise((resolve) => { - const child = spawn(command, args, { - cwd: options.cwd, - stdio: ["ignore", "pipe", "pipe"] - }); - - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - let timedOut = false; - - child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk))); - child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk))); - - const timeout = options.timeoutMs - ? setTimeout(() => { - timedOut = true; - child.kill("SIGTERM"); - }, options.timeoutMs) - : undefined; - - child.on("error", (error) => { - if (timeout) { - clearTimeout(timeout); - } - resolve({ - ok: false, - exitCode: null, - stdout: "", - stderr: error.message - }); - }); - - child.on("close", (exitCode) => { - if (timeout) { - clearTimeout(timeout); - } - resolve({ - ok: !timedOut && exitCode === 0, - exitCode, - stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(), - stderr: timedOut ? "Command timed out." : Buffer.concat(stderrChunks).toString("utf8").trim() - }); - }); - }); -} - -function buildDoctorReport( - context: ProjectContext, - summary: DoctorResult["summary"], - checks: DoctorCheck[], - setupItems: DoctorSetupItem[], - suggestions: SuggestedAction[] -): string { - return `# Doctor - -## Summary - -- Repository: ${context.repoName} -- Target: ${context.targetPath} -- Output: ${context.outputPath} -- Passed: ${summary.passed} -- Warnings: ${summary.warnings} -- Failed: ${summary.failed} -- Headline: ${summary.headline} - -## Checks - -${checks - .map( - (check) => `### ${check.label} - -- Status: ${check.status.toUpperCase()} -- Summary: ${check.summary} - -Details: -${renderList(check.details)}` - ) - .join("\n\n")} - -## Runtime Setup - -${renderSetupItems(setupItems)} - -## Suggested Actions - -${renderSuggestions(suggestions)} -`; -} - -function normalizeSet(values: string[]): Set { - return new Set(values.map((value) => value.toLowerCase())); -} - -function hasMatchingFile(files: string[], matchers: Array): boolean { - return files.some((filePath) => - matchers.some((matcher) => (typeof matcher === "string" ? filePath === matcher : matcher.test(filePath))) - ); -} - -function buildRuntimeToolchainSpecs(context: ProjectContext): RuntimeToolchainSpec[] { - const languages = normalizeSet(context.discovery.languages); - const files = context.discovery.files; - const manifests = context.discovery.manifests; - - const nodeDetected = - languages.has("javascript") || - languages.has("typescript") || - hasMatchingFile(files, ["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"]); - const pythonDetected = - languages.has("python") || hasMatchingFile(files, ["requirements.txt", "pyproject.toml", "uv.lock", "Pipfile"]); - const goDetected = languages.has("go") || hasMatchingFile(files, ["go.mod"]); - const rustDetected = languages.has("rust") || hasMatchingFile(files, ["Cargo.toml"]); - const javaDetected = - languages.has("java") || hasMatchingFile(files, ["pom.xml", "build.gradle", "build.gradle.kts"]); - const phpDetected = languages.has("php") || hasMatchingFile(files, ["composer.json"]); - const rubyDetected = languages.has("ruby") || hasMatchingFile(files, ["Gemfile"]); - const dotnetDetected = - languages.has("c#") || languages.has("csharp") || hasMatchingFile(files, [/\.csproj$/i, /\.sln$/i, /Directory\.Build\.props$/i]); - - const manifestDetails = manifests.length > 0 ? manifests.slice(0, 3) : []; - - return [ - { - id: "ollama-local-runtime", - label: "Ollama Local Runtime", - summary: "Habilita ejecucion local, offline y barata para `ask`, `swarm`, `review-delta` y `security-audit`.", - installHint: "Instala Ollama y descarga al menos un modelo local definido en `config/models.json`.", - command: "ollama", - args: ["--version"], - appliesToTarget: true, - detectedBy: ["project-brain local-first runtime"], - required: true - }, - { - id: "node-open-source-toolchain", - label: "Node.js / npm", - summary: "Permite ejecutar build, lint, tests y tooling real sobre repos JavaScript/TypeScript.", - installHint: "Instala Node.js 18+ con npm; agrega pnpm o yarn si el repo lo requiere.", - command: "npm", - args: ["--version"], - appliesToTarget: nodeDetected, - detectedBy: nodeDetected ? ["TypeScript/JavaScript detected", ...manifestDetails] : ["optional stack expansion"] - }, - { - id: "python-open-source-toolchain", - label: "Python 3 / uv", - summary: "Permite validar scripts, servicios y tests Python sin salir del flujo governado.", - installHint: "Instala Python 3.11+ y `uv` o `pip` para repos Python.", - command: "python3", - args: ["--version"], - appliesToTarget: pythonDetected, - detectedBy: pythonDetected ? ["Python detected", ...manifestDetails] : ["optional stack expansion"] - }, - { - id: "go-open-source-toolchain", - label: "Go Toolchain", - summary: "Permite ejecutar `go test`, validar modulos y revisar proyectos Go con comandos reales.", - installHint: "Instala el toolchain oficial de Go y habilita `go` en PATH.", - command: "go", - args: ["version"], - appliesToTarget: goDetected, - detectedBy: goDetected ? ["Go detected", ...manifestDetails] : ["optional stack expansion"] - }, - { - id: "rust-open-source-toolchain", - label: "Rust / Cargo", - summary: "Permite compilar, testear y revisar crates con evidencia real en lugar de solo analisis estatico.", - installHint: "Instala `rustup`, `rustc` y `cargo`.", - command: "cargo", - args: ["--version"], - appliesToTarget: rustDetected, - detectedBy: rustDetected ? ["Rust detected", ...manifestDetails] : ["optional stack expansion"] - }, - { - id: "java-open-source-toolchain", - label: "OpenJDK / Maven", - summary: "Permite revisar servicios Java con builds y tests reales en stacks JVM open source.", - installHint: "Instala OpenJDK LTS y Maven o Gradle.", - command: "java", - args: ["-version"], - appliesToTarget: javaDetected, - detectedBy: javaDetected ? ["Java detected", ...manifestDetails] : ["optional stack expansion"] - }, - { - id: "php-open-source-toolchain", - label: "PHP / Composer", - summary: "Permite validar apps PHP y dependencias Composer dentro de `project-brain`.", - installHint: "Instala PHP 8+ y Composer.", - command: "php", - args: ["--version"], - appliesToTarget: phpDetected, - detectedBy: phpDetected ? ["PHP detected", ...manifestDetails] : ["optional stack expansion"] - }, - { - id: "ruby-open-source-toolchain", - label: "Ruby / Bundler", - summary: "Permite correr tests y checks reales sobre repos Ruby y Rails.", - installHint: "Instala Ruby y Bundler.", - command: "ruby", - args: ["--version"], - appliesToTarget: rubyDetected, - detectedBy: rubyDetected ? ["Ruby detected", ...manifestDetails] : ["optional stack expansion"] - }, - { - id: "dotnet-open-source-toolchain", - label: ".NET SDK", - summary: "Permite build, restore y tests en repos C#/.NET sin depender solo del analisis textual.", - installHint: "Instala .NET SDK LTS.", - command: "dotnet", - args: ["--version"], - appliesToTarget: dotnetDetected, - detectedBy: dotnetDetected ? ["C#/.NET detected", ...manifestDetails] : ["optional stack expansion"] - } - ]; -} - -async function buildRuntimeSetupItems( - context: ProjectContext, - runCommand: (command: string, args: string[], options?: { cwd?: string; timeoutMs?: number }) => Promise -): Promise { - const specs = buildRuntimeToolchainSpecs(context); - const items: DoctorSetupItem[] = []; - - for (const spec of specs) { - const probe = await runCommand(spec.command, spec.args, { timeoutMs: 5_000 }); - const tier = spec.required ? "required" : spec.appliesToTarget ? "recommended" : "optional"; - const probeOutput = [probe.stdout, probe.stderr].find((value) => value && value.trim().length > 0)?.trim(); - - items.push({ - id: spec.id, - label: spec.label, - tier, - status: probe.ok ? "installed" : "missing", - summary: spec.summary, - installHint: spec.installHint, - details: [ - `Applies to current target: ${spec.appliesToTarget ? "yes" : "optional expansion"}`, - `Detected by: ${spec.detectedBy.join(", ")}`, - probe.ok - ? `Probe: ${probeOutput ?? `${spec.command} ${spec.args.join(" ")}`}` - : `Probe failed: ${probeOutput ?? `missing command ${spec.command}`}` - ] - }); - } - - return items; -} - -export async function runDoctor( - context: ProjectContext, - assistant: DoctorAssistant, - deps: DoctorDeps = {} -): Promise { - const runCommand = deps.runCommand ?? defaultRunCommand; - const projectRoot = deps.projectRoot ?? resolveProjectBrainRoot(__dirname); - const checks: DoctorCheck[] = []; - - const nodeMajor = Number(process.versions.node.split(".")[0] ?? 0); - checks.push( - buildCheck( - "node-runtime", - "Node Runtime", - nodeMajor >= 18 ? "pass" : "fail", - `Node ${process.version} detected.`, - nodeMajor >= 18 ? ["Global fetch and AbortSignal.timeout are available."] : ["project-brain expects Node 18+."] - ) - ); - - const gitBinary = await runCommand("git", ["--version"], { timeoutMs: 5_000 }); - checks.push( - buildCheck( - "git-binary", - "Git Binary", - gitBinary.ok ? "pass" : "fail", - gitBinary.ok ? gitBinary.stdout || "Git is available." : "Git is not available in PATH.", - gitBinary.ok ? [] : [gitBinary.stderr || "Install git and retry."] - ) - ); - - const gitRepo = await runCommand("git", ["-C", context.targetPath, "rev-parse", "--is-inside-work-tree"], { timeoutMs: 5_000 }); - if (!gitBinary.ok) { - checks.push(buildCheck("git-repository", "Target Git Repository", "warn", "Skipped because git is unavailable.", [])); - } else { - const branch = gitRepo.ok - ? await runCommand("git", ["-C", context.targetPath, "branch", "--show-current"], { timeoutMs: 5_000 }) - : undefined; - checks.push( - buildCheck( - "git-repository", - "Target Git Repository", - gitRepo.ok ? "pass" : "warn", - gitRepo.ok ? "Target path is a git repository." : "Target path is not a git repository.", - gitRepo.ok && branch?.stdout ? [`Branch: ${branch.stdout}`] : [] - ) - ); - } - - const ollamaBinary = await runCommand("ollama", ["--version"], { timeoutMs: 5_000 }); - checks.push( - buildCheck( - "ollama-binary", - "Ollama Binary", - ollamaBinary.ok ? "pass" : "warn", - ollamaBinary.ok ? ollamaBinary.stdout || "Ollama is available." : "Ollama is not available in PATH.", - ollamaBinary.ok ? [] : [ollamaBinary.stderr || "Install Ollama if you want local model execution."] - ) - ); - - let inventory: ModelInventory | undefined; - if (assistant.listModels) { - try { - inventory = await assistant.listModels(); - } catch (error) { - checks.push( - buildCheck( - "ollama-api", - "Ollama API", - "warn", - "Could not query the Ollama API through the model router.", - [error instanceof Error ? error.message : String(error)] - ) - ); - } - } else { - checks.push(buildCheck("ollama-api", "Ollama API", "warn", "Model inventory is unavailable from the current AI router.", [])); - } - - if (inventory) { - checks.push( - buildCheck( - "ollama-api", - "Ollama API", - inventory.availableModels.length > 0 ? "pass" : ollamaBinary.ok ? "warn" : "fail", - inventory.availableModels.length > 0 - ? `Detected ${inventory.availableModels.length} Ollama model(s).` - : "No Ollama models were detected.", - inventory.availableModels.map((model) => `${model.name} (${model.residency}, offline=${model.offlineCapable ? "yes" : "no"})`) - ) - ); - - const availableNames = inventory.availableModels.map((model) => model.name); - const profileChecks = Object.entries(inventory.resolvedProfiles).map(([profile, model]) => ({ - profile, - model, - available: matchesModel(model, availableNames) - })); - const missingCriticalProfiles = profileChecks.filter((entry) => ["worker", "reviewer", "reasoning"].includes(entry.profile) && !entry.available); - const missingOptionalProfiles = profileChecks.filter((entry) => !["worker", "reviewer", "reasoning"].includes(entry.profile) && !entry.available); - checks.push( - buildCheck( - "model-profiles", - "Model Profiles", - missingCriticalProfiles.length > 0 ? "fail" : missingOptionalProfiles.length > 0 ? "warn" : "pass", - missingCriticalProfiles.length > 0 - ? "One or more critical local model profiles are unavailable." - : missingOptionalProfiles.length > 0 - ? "Some non-critical model profiles are unavailable, but local fallbacks exist." - : "All configured model profiles are available.", - profileChecks.map((entry) => `${entry.profile}: ${entry.model} (${entry.available ? "available" : "missing"})`) - ) - ); - - checks.push( - buildCheck( - "swarm-local-readiness", - "Swarm Local Readiness", - inventory.offlineReady ? "pass" : "warn", - inventory.offlineReady ? "Local-first swarm runs are supported." : "Offline/local swarm readiness is incomplete.", - [ - `Offline mode: ${inventory.offlineMode ? "yes" : "no"}`, - `Remote Ollama allowed: ${inventory.remoteOllamaAllowed ? "yes" : "no"}`, - `Offline ready: ${inventory.offlineReady ? "yes" : "no"}` - ] - ) - ); - } - - const configPath = path.join(projectRoot, "config", "models.json"); - const configExists = await fileExists(configPath); - checks.push( - buildCheck( - "model-config", - "Model Config", - configExists ? "pass" : "fail", - configExists ? "Model config file is present." : "config/models.json is missing.", - [configPath] - ) - ); - - const distCliPath = path.join(projectRoot, "dist", "cli", "project-brain.js"); - const distCliExists = await fileExists(distCliPath); - checks.push( - buildCheck( - "cli-build", - "CLI Build Artifact", - distCliExists ? "pass" : "warn", - distCliExists ? "Built CLI artifact is present." : "Built CLI artifact is missing.", - [distCliPath] - ) - ); - - try { - await ensureDir(context.outputPath); - await ensureDir(context.reportsDir); - await ensureDir(path.join(context.memoryDir, "doctor")); - checks.push( - buildCheck( - "output-path", - "Output Path", - "pass", - "Output directories are writable.", - [context.outputPath, context.reportsDir, path.join(context.memoryDir, "doctor")] - ) - ); - } catch (error) { - checks.push( - buildCheck( - "output-path", - "Output Path", - "fail", - "Could not create or access the output directories.", - [error instanceof Error ? error.message : String(error)] - ) - ); - } - - const setupItems = await buildRuntimeSetupItems(context, runCommand); - - const orderedChecks = checks.sort((left, right) => { - const rankDelta = statusRank(right.status) - statusRank(left.status); - return rankDelta !== 0 ? rankDelta : left.label.localeCompare(right.label); - }); - const summary = { - passed: countByStatus(orderedChecks, "pass"), - warnings: countByStatus(orderedChecks, "warn"), - failed: countByStatus(orderedChecks, "fail"), - headline: buildHeadline(orderedChecks) - }; - const suggestions = deriveDoctorSuggestions({ - context, - checks: orderedChecks, - summary - }); - - const reportPath = path.join(context.reportsDir, "doctor.md"); - const memoryPath = path.join(context.memoryDir, "doctor", "doctor.json"); - await writeFileEnsured(reportPath, buildDoctorReport(context, summary, orderedChecks, setupItems, suggestions)); - await writeJsonEnsured(memoryPath, { - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - projectRoot, - summary, - checks: orderedChecks, - setupItems, - suggestions - }); - - return { - context, - reportPath, - memoryPath, - summary, - checks: orderedChecks, - setupItems, - suggestions - }; -} diff --git a/core/intent_router/index.ts b/core/intent_router/index.ts deleted file mode 100644 index 0ff5664..0000000 --- a/core/intent_router/index.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type { AskRoute, GovernanceTrigger } from "../../shared/types"; - -function includesAny(value: string, patterns: RegExp[]): boolean { - return patterns.some((pattern) => pattern.test(value)); -} - -function followUpsFor(workflow: AskRoute["workflow"]): string[] { - if (workflow === "resume-project") { - return [ - "project-brain resume .", - "project-brain status .", - 'project-brain ask "dime que le falta criticamente"', - 'project-brain ask "revisa los cambios recientes"' - ]; - } - - if (workflow === "discover-project") { - return [ - 'project-brain ask "dime que le falta criticamente"', - 'project-brain swarm "ayudame a mejorar este repo"', - "project-brain plan-improvements .", - 'project-brain ask "revisa los cambios recientes"', - 'project-brain ask "inspecciona el firewall y aprobaciones"' - ]; - } - - if (workflow === "critical-gaps") { - return [ - 'project-brain swarm "ayudame a priorizar y mejorar este repo"', - "project-brain plan-improvements .", - 'project-brain ask "revisa los cambios recientes"', - 'project-brain ask "inspecciona el firewall y aprobaciones"' - ]; - } - - if (workflow === "security-audit") { - return [ - "project-brain security-audit .", - 'project-brain ask "revisa los cambios recientes"', - 'project-brain ask "inspecciona el firewall y aprobaciones"', - 'project-brain swarm "prioriza remediaciones reales de seguridad para este repo"' - ]; - } - - if (workflow === "review-latest-changes") { - return [ - 'project-brain ask "dime si el riesgo de estos cambios es alto"', - 'project-brain ask "muestrame el grafo de impacto"', - 'project-brain swarm "dame una segunda opinion sobre este repo"', - 'project-brain ask "dime que le falta criticamente"' - ]; - } - - if (workflow === "inspect-firewall") { - return [ - 'project-brain ask "identifica este proyecto"', - 'project-brain ask "dime que le falta criticamente"', - 'project-brain swarm "proponme mejoras para este repo"', - 'project-brain ask "revisa los cambios recientes"' - ]; - } - - return [ - 'project-brain ask "revisa los cambios recientes"', - 'project-brain ask "dime que le falta criticamente"', - 'project-brain swarm "ayudame a mejorar este repo"', - 'project-brain ask "inspecciona el firewall y aprobaciones"' - ]; -} - -function inferTrigger(normalizedIntent: string, fallback: GovernanceTrigger): GovernanceTrigger { - if (/advisory|cve|vuln|vulnerabil/i.test(normalizedIntent)) { - return "security-advisory"; - } - - if (/security|seguridad|secret|dependency|dependenc/i.test(normalizedIntent)) { - return "security-audit"; - } - - if (/architecture|arquitectura|structural|refactor|boundary/i.test(normalizedIntent)) { - return "architecture-review"; - } - - if (/incident|outage|falla|caida|degrad/i.test(normalizedIntent)) { - return "incident-detection"; - } - - if (/change|cambio|diff|commit|pull request|pr\b|latest/i.test(normalizedIntent)) { - return "repository-change"; - } - - return fallback; -} - -export function routeIntent(intent: string): AskRoute { - const normalizedIntent = intent.trim().toLowerCase(); - - if ( - includesAny(normalizedIntent, [ - /\bresume\b/, - /\bcontinue\b/, - /\bretoma\b/, - /\bcontinua\b/, - /\bcontinuar\b/, - /\bseguir\b/, - /\bseguimos\b/, - /where.*left off/, - /donde nos quedamos/, - /en que nos quedamos/ - ]) - ) { - return { - workflow: "resume-project", - reason: "The request is about continuing from the latest saved project state.", - trigger: "manual", - followUps: followUpsFor("resume-project") - }; - } - - if ( - includesAny(normalizedIntent, [ - /firewall/, - /policy/, - /approval/, - /permissions?/, - /permisos?/, - /riesgo operativ/, - /tool matrix/, - /safe mode/, - /aprobaciones?/ - ]) - ) { - return { - workflow: "inspect-firewall", - reason: "The request is about approvals, permissions, or execution boundaries.", - trigger: inferTrigger(normalizedIntent, "repository-change"), - followUps: followUpsFor("inspect-firewall") - }; - } - - if ( - includesAny(normalizedIntent, [ - /review/, - /revisa/, - /latest changes?/, - /ultimos? cambios?/, - /ultimo commit/, - /diff/, - /pull request/, - /\bpr\b/, - /delta/ - ]) - ) { - return { - workflow: "review-latest-changes", - reason: "The request focuses on recent changes or bounded review context.", - trigger: "repository-change", - followUps: followUpsFor("review-latest-changes") - }; - } - - if ( - includesAny(normalizedIntent, [ - /graph/, - /grafo/, - /dependencies?/, - /dependencias/, - /impact/, - /blast radius/, - /callers?/, - /callees?/ - ]) - ) { - return { - workflow: "build-code-graph", - reason: "The request asks for structural code relationships or impact context.", - trigger: inferTrigger(normalizedIntent, "repository-change"), - followUps: followUpsFor("build-code-graph") - }; - } - - if ( - includesAny(normalizedIntent, [ - /security audit/, - /auditoria de seguridad/, - /auditoría de seguridad/, - /critical/, - /critic/, - /que le falta/, - /what.*missing/, - /missing/, - /riesgo/, - /risk/, - /security/, - /seguridad/, - /owasp/, - /cwe/, - /auth/, - /csrf/, - /xss/, - /idor/, - /documentation/, - /documentacion/, - /deuda tecnica/, - /technical debt/ - ]) - ) { - return { - workflow: /security|seguridad|owasp|cwe|auth|csrf|xss|idor/i.test(normalizedIntent) ? "security-audit" : "critical-gaps", - reason: /security|seguridad|owasp|cwe|auth|csrf|xss|idor/i.test(normalizedIntent) - ? "The request asks for a security-focused audit with vulnerabilities, auth, or exploitability concerns." - : "The request asks for weaknesses, risks, or missing capabilities.", - trigger: inferTrigger(normalizedIntent, "manual"), - followUps: followUpsFor(/security|seguridad|owasp|cwe|auth|csrf|xss|idor/i.test(normalizedIntent) ? "security-audit" : "critical-gaps") - }; - } - - return { - workflow: "discover-project", - reason: "Defaulting to repository discovery because the request is exploratory or introductory.", - trigger: "manual", - followUps: followUpsFor("discover-project") - }; -} diff --git a/core/orchestrator/chief-agent.ts b/core/orchestrator/chief-agent.ts deleted file mode 100644 index 80c6b4f..0000000 --- a/core/orchestrator/chief-agent.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { buildAgentCatalog, type AgentCatalogEntry } from "../../agents/catalog"; -import { StructuredLogger } from "../../shared/logger"; -import type { AgentReport, ProjectContext } from "../../shared/types"; - -export class ChiefAgent { - private readonly logger = new StructuredLogger("chief-agent"); - private readonly catalog = buildAgentCatalog(); - private readonly agents = this.catalog.map((entry) => entry.agent); - - listCatalog(): AgentCatalogEntry[] { - return [...this.catalog]; - } - - async run(context: ProjectContext): Promise { - this.logger.info("Running specialist agents", { - repoName: context.repoName, - agents: this.agents.map((agent) => agent.agentId) - }); - - const reports: AgentReport[] = []; - - for (const agent of this.agents) { - reports.push(await agent.run(context)); - } - - return reports; - } -} diff --git a/core/orchestrator/main.ts b/core/orchestrator/main.ts deleted file mode 100644 index b5a45aa..0000000 --- a/core/orchestrator/main.ts +++ /dev/null @@ -1,1779 +0,0 @@ -import path from "node:path"; - -import { buildOrUpdateCodeGraphV2 } from "../../analysis/code_graph_v2"; -import { analyzeImpactRadius } from "../../analysis/impact_radius"; -import { MetricsCollector } from "../../analysis/metrics/metrics_collector"; -import { buildRepositoryFactGraph } from "../../analysis/repository_fact_graph"; -import { discoverRepositoryTargets, uniqueRepositoryNames } from "../../analysis/workspace_discovery"; -import { AIRouter, type AIRouterRequest, type ModelInventory, type ModelSelection } from "../ai_router/router"; -import { routeIntent } from "../intent_router"; -import { writeCodebaseMapArtifacts } from "../codebase_map"; -import { runDoctor } from "../doctor"; -import { buildResume } from "../resume"; -import { buildStatus } from "../status"; -import { buildWorkflowRuntimeDefinitions, type WorkflowRuntimeDefinition } from "../workflow_registry"; -import { ContextBuilder } from "../context_builder"; -import { writeContextLiteArtifacts } from "../context_lite"; -import { WeeklyScheduler } from "../scheduler"; -import { DiscoveryEngine } from "../discovery_engine"; -import { runDeepAgentsSwarm } from "../deepagents_swarm"; -import { runSecurityAudit } from "../security_audit"; -import { runSwarm } from "../swarm_runtime"; -import { AgentSelfGovernanceSystem } from "../../governance/self-governance-system"; -import { buildKnowledgeGraphArtifacts } from "../../memory/knowledge_graph"; -import { recordLearningArtifacts, recordSwarmLearningArtifacts } from "../../memory/learning_store"; -import { runFactQuery } from "../../memory/fact_query"; -import { preflightFacts } from "../../memory/preflight_facts"; -import { writeMemoryBriefArtifacts } from "../../memory/memory_brief"; -import { assessMemoryReadiness } from "../../memory/readiness"; -import { updateContext } from "../../memory/session_log"; -import { runHarnessAudit } from "../../operations/harness_audit"; -import { clearContextAnnotation, listContextAnnotations, readContextAnnotation, writeContextAnnotation } from "../../memory/annotations"; -import { getContextRegistryEntry, listContextSources, searchContextRegistry } from "../../memory/context_registry"; -import { runEcosystemRadar } from "../../memory/context_registry/ecosystem_radar"; -import { updatePersistentMemory } from "../../memory/context_store"; -import { writeImprovementPlanArtifacts } from "../../planning/improvement_plan"; -import { writeArchitecturePlanArtifacts } from "../../planning/architecture_plan"; -import { writeProjectSeedArtifacts } from "../../planning/project_seed"; -import { buildRunbook } from "../../planning/runbook"; -import { createCycleId, StructuredLogger, withLogContext } from "../../shared/logger"; -import { ensureDir, readJsonSafe, readTextSafe, toPosixPath, uniqueSorted, walkDirectory, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - AgentReport, - AskArtifact, - AskResult, - AskWorkflow, - CodeGraphBuildResult, - CodebaseMapResult, - ContextLiteResult, - FactQueryResult, - HarnessAuditResult, - ContextGetResult, - ContextAnnotation, - ContextSearchResult, - ContextSourcesResult, - EcosystemRadarResult, - ImpactAnalysisResult, - EcosystemCodebaseMapResult, - EcosystemAnalysisResult, - EcosystemRepositoryResult, - FirewallInspectionResult, - GovernanceTrigger, - ImprovementPlanResult, - ArchitecturePlanResult, - OrchestrationResult, - ProjectContext, - ProjectSeedInput, - ProjectSeedResult, - ReportManifest, - RepositoryTarget, - DoctorResult, - ResumeResult, - SecurityAuditResult, - StartResult, - StartStep, - StatusResult, - RunbookResult, - SwarmRunResult -} from "../../shared/types"; -import type { TokenPreset } from "../token_policy"; - -function highestRisk(agentReports: AgentReport[]): "low" | "medium" | "high" { - if (agentReports.some((report) => report.riskLevel === "high")) { - return "high"; - } - if (agentReports.some((report) => report.riskLevel === "medium")) { - return "medium"; - } - return "low"; -} - -function clampScore(value: number): number { - return Math.max(0, Math.min(1, Number(value.toFixed(2)))); -} - -function containsPathLikeEvidence(value: string): boolean { - return /`[^`]+\.[a-z0-9]+`|(?:^|[\s(])(?:src|app|lib|components|pages|routes|controllers|tests|docs|config)\/[^\s,;:()]+/i.test(value); -} - -function collectGroundedFiles(context: ProjectContext, text: string): string[] { - return context.discovery.files.filter((filePath) => text.includes(filePath)).slice(0, 8); -} - -function detectGenericSignals(entries: string[]): string[] { - return uniqueSorted( - entries.filter( - (entry) => - GENERIC_REPORT_PATTERNS.some((pattern) => pattern.test(entry)) && - !containsPathLikeEvidence(entry) - ) - ).slice(0, 4); -} - -async function assessAgentReportQuality( - context: ProjectContext, - report: AgentReport -): Promise { - const reportContent = await readTextSafe(report.outputPath); - const evidenceText = [report.summary, ...report.findings, ...report.recommendations, reportContent].join("\n"); - const groundedFiles = collectGroundedFiles(context, evidenceText); - const genericSignals = detectGenericSignals([...report.findings, ...report.recommendations]); - const notes: string[] = []; - let score = 1; - - if (report.findings.length === 0 && report.recommendations.length === 0) { - score -= 0.4; - notes.push("No contiene findings ni recomendaciones accionables."); - } - - if (!reportContent.trim()) { - score -= 0.15; - notes.push("El artefacto escrito del agente quedó vacío o no se pudo leer."); - } - - if (groundedFiles.length === 0 && !containsPathLikeEvidence(evidenceText)) { - score -= 0.45; - notes.push("No cita archivos o superficies confirmadas del repositorio."); - } - - if (genericSignals.length > 0 && groundedFiles.length === 0) { - score -= 0.2; - notes.push(`Las recomendaciones parecen genéricas: ${genericSignals.join(" | ")}`); - } - - if (report.riskLevel !== "low" && report.findings.length === 0) { - score -= 0.1; - notes.push("Marca riesgo medio/alto sin findings concretos."); - } - - return { - report, - score: clampScore(score), - status: score >= 0.65 ? "accepted" : "review-required", - notes: notes.length > 0 ? notes : ["El reporte cita evidencia suficiente para entrar al resumen operativo."], - groundedFiles, - genericSignals - }; -} - -function buildReportQualityContent( - context: ProjectContext, - assessments: AgentReportQualityAssessment[], - effectiveReports: AgentReport[], - fellBackToRawReports: boolean -): string { - const accepted = assessments.filter((assessment) => assessment.status === "accepted"); - const reviewRequired = assessments.filter((assessment) => assessment.status === "review-required"); - - return `# Report Quality - -## Summary - -- Repository: ${context.repoName} -- Accepted reports: ${accepted.length} -- Review-required reports: ${reviewRequired.length} -- Effective reports used downstream: ${effectiveReports.length} -- Fallback to raw reports: ${fellBackToRawReports ? "yes" : "no"} - -## Assessments - -${assessments - .map( - (assessment) => `### ${assessment.report.title} - -- Agent: ${assessment.report.agentId} -- Status: ${assessment.status} -- Score: ${assessment.score} -- Grounded files: ${assessment.groundedFiles.join(", ") || "None"} -- Notes: -${renderList(assessment.notes)} -` - ) - .join("\n")} -`; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderArtifactList(artifacts: AskArtifact[]): string { - return artifacts.length > 0 - ? artifacts.map((artifact) => `- ${artifact.label}: ${artifact.path}`).join("\n") - : "- None"; -} - -interface AskAssistant { - ask(input: AIRouterRequest): Promise; - selectModel(input: AIRouterRequest): Promise; - listModels?: () => Promise; -} - -interface AskAIEnhancement { - headline?: string; - summary: string[]; - followUps: string[]; - suggestedWorkflow?: AskWorkflow; - modelSelection: ModelSelection; -} - -interface AskGuidedExecution { - label: string; - command: string; - headline: string; - summary: string[]; - artifacts: AskArtifact[]; - followUps: string[]; -} - -interface ProjectBrainOrchestratorOptions { - aiRouter?: AskAssistant; -} - -interface AgentReportQualityAssessment { - report: AgentReport; - score: number; - status: "accepted" | "review-required"; - notes: string[]; - groundedFiles: string[]; - genericSignals: string[]; -} - -const GENERIC_REPORT_PATTERNS = [ - /\bimprove (?:the )?(?:ux|ui|architecture|performance|security|reliability)\b/i, - /\badd (?:more )?(?:tests|logging|monitoring|documentation)\b/i, - /\brefactor (?:the )?(?:codebase|workflow|module|architecture)\b/i, - /\benhance (?:the )?(?:workflow|platform|experience|quality)\b/i, - /\boptimi[sz]e (?:the )?(?:app|application|system|performance)\b/i -]; - -function extractJsonObject(input: string): Record | undefined { - const trimmed = input.trim(); - const candidate = trimmed.startsWith("```") - ? trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "") - : trimmed; - - try { - const parsed = JSON.parse(candidate) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : undefined; - } catch { - const start = candidate.indexOf("{"); - const end = candidate.lastIndexOf("}"); - if (start < 0 || end <= start) { - return undefined; - } - - try { - const parsed = JSON.parse(candidate.slice(start, end + 1)) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : undefined; - } catch { - return undefined; - } - } -} - -function normalizeStringList(value: unknown): string[] { - return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) : []; -} - -function mergeUniqueStrings(...groups: string[][]): string[] { - return [...new Set(groups.flat().filter((item) => item.trim().length > 0))]; -} - -function normalizeSuggestedWorkflow(value: unknown): AskWorkflow | undefined { - if (typeof value !== "string") { - return undefined; - } - - const allowed: AskWorkflow[] = [ - "resume-project", - "discover-project", - "security-audit", - "critical-gaps", - "review-latest-changes", - "inspect-firewall", - "build-code-graph" - ]; - - return allowed.includes(value as AskWorkflow) ? (value as AskWorkflow) : undefined; -} - -function shouldUseAIAskAssist(intent: string, workflow: AskWorkflow): boolean { - if (workflow === "security-audit") { - return false; - } - - const strategic = /estrateg|strategy|roadmap|stack|tecnolog|deploy|alcance|scope|arquitect|architecture|producto|product|idea|greenfield/i.test( - intent - ); - const exploratory = /quiero|ayudame|help me|necesito|define|definir|como seguimos|what should/i.test(intent); - - if (workflow === "discover-project") { - return strategic || exploratory; - } - - if (workflow === "resume-project") { - return exploratory; - } - - return strategic; -} - -function hasAskArtifact(artifacts: AskArtifact[], label: string): boolean { - return artifacts.some((artifact) => artifact.label === label); -} - -function shouldAutoContinueAsk(intent: string, workflow: AskWorkflow): boolean { - if (workflow !== "resume-project") { - return false; - } - - return /\b(resume|continue|retoma|continua|continuar|seguir|seguimos|donde nos quedamos|where.*left off)\b/i.test(intent); -} - -export class ProjectBrainOrchestrator { - private readonly logger = new StructuredLogger("orchestrator"); - private readonly discoveryEngine = new DiscoveryEngine(); - private readonly contextBuilder = new ContextBuilder(); - private readonly selfGovernance = new AgentSelfGovernanceSystem(); - private readonly scheduler = new WeeklyScheduler(); - private readonly metricsCollector = new MetricsCollector(); - private readonly aiRouter: AskAssistant; - - constructor(options: ProjectBrainOrchestratorOptions = {}) { - this.aiRouter = options.aiRouter ?? new AIRouter(); - } - - private discoveryExclusions(targetPath: string, outputPath: string): string[] { - const relativeOutput = toPosixPath(path.relative(targetPath, outputPath)); - - if (!relativeOutput || relativeOutput === "." || relativeOutput.startsWith("../")) { - return []; - } - - return [relativeOutput]; - } - - async initTarget(targetPath: string, outputPath = targetPath): Promise { - const discovery = await this.discoveryEngine.analyze(targetPath, { - excludePaths: this.discoveryExclusions(targetPath, outputPath) - }); - const context = await this.contextBuilder.build(discovery, outputPath); - await writeMemoryBriefArtifacts(context); - return context; - } - - async mapTarget(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - const artifact = await writeCodebaseMapArtifacts(context); - await writeMemoryBriefArtifacts(context); - - return { - context, - ...artifact - }; - } - - async analyzeImpact( - targetPath: string, - outputPath = targetPath, - options?: { - files?: string[]; - baseRef?: string; - headRef?: string; - } - ): Promise { - const context = await this.initTarget(targetPath, outputPath); - return analyzeImpactRadius(context, options); - } - - async buildCodeGraph(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - const codeGraph = await buildOrUpdateCodeGraphV2(context); - const factGraph = await buildRepositoryFactGraph(context, codeGraph.graph); - await writeMemoryBriefArtifacts(context); - - return { - ...codeGraph, - factGraphPath: factGraph.graphPath, - factReportPath: factGraph.reportPath, - factGraph: factGraph.graph - }; - } - - async contextLite(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - const result = await writeContextLiteArtifacts(context); - await writeMemoryBriefArtifacts(context); - return result; - } - - async factQuery(targetPath: string, outputPath = targetPath, query: string): Promise { - const context = await this.initTarget(targetPath, outputPath); - const result = await runFactQuery(context, query); - await writeMemoryBriefArtifacts(context); - return result; - } - - async runbook(targetPath: string, outputPath = targetPath, intent: string): Promise { - const context = await this.initTarget(targetPath, outputPath); - const result = await buildRunbook(context, intent); - await writeMemoryBriefArtifacts(context); - return result; - } - - async harnessAudit(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - const result = await runHarnessAudit(context); - await writeMemoryBriefArtifacts(context); - return result; - } - - async start( - targetPath: string, - outputPath = targetPath, - intent = "optimize analysis and cost", - options: { withSwarm?: boolean } = {} - ): Promise { - const context = await this.initTarget(targetPath, outputPath); - const executedSteps: StartStep[] = []; - let status = await buildStatus(context); - const hasArtifact = (label: string): boolean => status.artifacts.some((artifact) => artifact.label === label && artifact.exists); - const workflowHasArtifact = (workflow: WorkflowRuntimeDefinition): boolean => - workflow.artifactLabels.some((label) => hasArtifact(label)); - const refreshStatus = async (): Promise => { - status = await buildStatus(context); - }; - const addWorkflowStep = ( - workflow: WorkflowRuntimeDefinition, - stepStatus: StartStep["status"], - summary: string - ): void => { - executedSteps.push({ - id: workflow.workflowId, - label: workflow.commandLabel, - status: stepStatus, - command: workflow.command, - summary - }); - }; - const workflows = buildWorkflowRuntimeDefinitions(context, intent).sort( - (left, right) => left.resumePriority - right.resumePriority || left.workflowId.localeCompare(right.workflowId) - ); - const cheapWorkflows = workflows.filter((workflow) => workflow.cheap); - const modelWorkflow = workflows.find((workflow) => workflow.usesModel); - - for (const workflow of cheapWorkflows) { - switch (workflow.workflowId) { - case "memory-brief": - addWorkflowStep(workflow, workflowHasArtifact(workflow) ? "skipped" : "done", "MEMORY_BRIEF se actualizo durante initTarget."); - break; - case "start": - addWorkflowStep(workflow, "done", "Este workflow esta en ejecucion."); - break; - case "doctor": - if (status.summary.doctorStatus === "unknown" || status.summary.doctorStatus === "fail") { - await this.doctor(targetPath, outputPath); - addWorkflowStep(workflow, "done", "Se actualizo el diagnostico local."); - await refreshStatus(); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe un diagnostico utilizable."); - } - break; - case "map-codebase": - if (!workflowHasArtifact(workflow)) { - await writeCodebaseMapArtifacts(context); - addWorkflowStep(workflow, "done", "Se genero el mapa estructural."); - await refreshStatus(); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe mapa estructural."); - } - break; - case "code-graph": - if (!workflowHasArtifact(workflow)) { - await this.buildCodeGraph(targetPath, outputPath); - addWorkflowStep(workflow, "done", "Se genero el grafo factual."); - await refreshStatus(); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe grafo factual."); - } - break; - case "fact-query": - if (!workflowHasArtifact(workflow)) { - await this.factQuery(targetPath, outputPath, `${context.repoName} ${intent}`); - addWorkflowStep(workflow, "done", "Se filtro memoria relevante sin usar modelo."); - await refreshStatus(); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe consulta factual reutilizable."); - } - break; - case "runbook": - if (!workflowHasArtifact(workflow)) { - await this.runbook(targetPath, outputPath, intent); - addWorkflowStep(workflow, "done", "Se genero un runbook token-aware."); - await refreshStatus(); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe runbook."); - } - break; - case "harness-audit": - if (!workflowHasArtifact(workflow)) { - await this.harnessAudit(targetPath, outputPath); - addWorkflowStep(workflow, "done", "Se audito preparacion de memoria/costos."); - await refreshStatus(); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe harness audit."); - } - break; - case "firewall": - if (!workflowHasArtifact(workflow)) { - await this.inspectFirewall(targetPath, outputPath, "repository-change"); - addWorkflowStep(workflow, "done", "Se genero snapshot de firewall."); - await refreshStatus(); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe firewall."); - } - break; - case "plan-improvements": - if (hasArtifact("Swarm") && !workflowHasArtifact(workflow)) { - await this.planImprovements(targetPath, outputPath, "repository-change"); - addWorkflowStep(workflow, "done", "Se convirtieron findings existentes en plan persistente."); - await refreshStatus(); - } else if (!hasArtifact("Swarm")) { - addWorkflowStep(workflow, "skipped", "Requiere un swarm previo; queda como paso posterior al contexto."); - } else { - addWorkflowStep(workflow, "skipped", "Ya existe plan de mejoras."); - } - break; - case "resume": - case "ask": - case "review-delta": - addWorkflowStep(workflow, "skipped", workflow.rationale); - break; - } - } - - if (!hasArtifact("Swarm") && options.withSwarm) { - await this.swarm(targetPath, outputPath, intent, { - engine: "bounded", - preset: "cheap", - chunkSize: 1, - parallelism: 2, - taskTimeoutMs: 90_000, - plannerTimeoutMs: 60_000, - synthesisTimeoutMs: 60_000, - runTimeoutMs: 120_000, - maxQueuedTasks: 4, - maxRetries: 0 - }); - await refreshStatus(); - if (modelWorkflow) { - executedSteps.push({ - id: modelWorkflow.workflowId, - label: modelWorkflow.commandLabel, - status: "done", - command: modelWorkflow.command, - summary: "Se ejecuto swarm porque se pidio --with-swarm." - }); - } - if (!hasArtifact("Improvement Plan")) { - await this.planImprovements(targetPath, outputPath, "repository-change"); - await refreshStatus(); - } - } - - const suggestedStep = executedSteps.find((step) => step.status === "suggested"); - const nextCommand = - suggestedStep?.command ?? - (!hasArtifact("Swarm") ? modelWorkflow?.command : undefined) ?? - status.suggestions[0]?.command; - const result: StartResult = { - context, - intent, - reportPath: path.join(context.reportsDir, "start.md"), - memoryPath: path.join(context.memoryDir, "start", "start.json"), - headline: nextCommand ? "Start complete: base context is ready; one next action remains." : "Start complete: project-brain context is ready.", - memoryReadiness: await assessMemoryReadiness(context), - executiveSummary: status.executiveSummary, - executedSteps, - nextCommand, - artifacts: status.artifacts, - suggestions: status.suggestions - }; - - await writeFileEnsured( - result.reportPath, - `# Start - -## Summary - -- Repository: ${context.repoName} -- Intent: ${intent} -- Headline: ${result.headline} -- Memory readiness: ${result.memoryReadiness.status} (${result.memoryReadiness.reason}) -- Executive summary: ${result.executiveSummary.reportPath} -- Next command: ${nextCommand ?? "None"} - -## Steps - -${executedSteps.map((step) => `- [${step.status}] ${step.label}: \`${step.command}\` - ${step.summary}`).join("\n")} -` - ); - await writeJsonEnsured(result.memoryPath, { - repoName: context.repoName, - targetPath, - outputPath, - intent, - headline: result.headline, - memoryReadiness: result.memoryReadiness, - executiveSummary: { - reportPath: result.executiveSummary.reportPath, - memoryPath: result.executiveSummary.memoryPath, - status: result.executiveSummary.status - }, - executedSteps, - nextCommand, - suggestions: status.suggestions - }); - const lastDoneCommand = [...executedSteps].reverse().find((step) => step.status === "done")?.command ?? "project-brain start"; - await updateContext(context, { - detected_stack: [ - `Languages: ${context.discovery.languages.join(", ") || "UNKNOWN"}`, - `Frameworks: ${context.discovery.frameworks.join(", ") || "UNKNOWN"}`, - `Testing: ${context.discovery.testing.join(", ") || "UNKNOWN"}`, - `Infrastructure: ${context.discovery.infrastructure.join(", ") || "UNKNOWN"}` - ], - entrypoints: context.discovery.structure.sampleFiles.slice(0, 12), - last_command: lastDoneCommand, - updated_at: new Date().toISOString() - }); - await writeMemoryBriefArtifacts(context); - - return result; - } - - async securityAudit( - targetPath: string, - outputPath = targetPath, - trigger: GovernanceTrigger = "security-audit" - ): Promise { - const scope = await discoverRepositoryTargets(targetPath, outputPath); - const firstRepository = scope.repositories[0]; - const primaryTargetPath = firstRepository?.targetPath ?? targetPath; - const primaryOutputPath = - scope.mode === "workspace" && firstRepository - ? this.workspaceRepoOutputPath(outputPath, firstRepository) - : outputPath; - const scopeNote = - scope.mode === "workspace" && firstRepository - ? `Workspace detectado; la auditoría se ejecutó sobre el primer repositorio materializado: ${firstRepository.repoName} (${firstRepository.relativePath}).` - : undefined; - const context = await this.initTarget(primaryTargetPath, primaryOutputPath); - const contextLite = await writeContextLiteArtifacts(context); - const governanceRun = await this.selfGovernance.run(context, trigger); - - return runSecurityAudit(context, governanceRun, contextLite, { - trigger, - scopeNote - }); - } - - async doctor(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - return runDoctor(context, this.aiRouter); - } - - async status(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - return buildStatus(context); - } - - async resume(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - return buildResume(context); - } - - async reviewDelta( - targetPath: string, - outputPath = targetPath, - options?: { - baseRef?: string; - headRef?: string; - } - ): Promise { - return this.analyzeImpact(targetPath, outputPath, { - baseRef: options?.baseRef, - headRef: options?.headRef - }); - } - - async inspectFirewall( - targetPath: string, - outputPath = targetPath, - trigger: GovernanceTrigger = "manual" - ): Promise { - const context = await this.initTarget(targetPath, outputPath); - const firewall = await this.selfGovernance.inspectFirewall(context, trigger); - - return { - context, - firewall - }; - } - - private async buildAskAIEnhancement( - intent: string, - workflow: AskWorkflow, - routingReason: string, - scopeMode: "repository" | "workspace" - ): Promise { - if (!shouldUseAIAskAssist(intent, workflow)) { - return undefined; - } - - const request: AIRouterRequest = { - task: "intent-routing", - profile: "planner", - allowRemote: true, - prompt: [ - "You are refining a user intent for project-brain.", - "Do not invent repository facts.", - "Interpret the request and improve the next step selection.", - "Return JSON only with this shape:", - '{ "headline": string, "summary": string[], "follow_ups": string[], "suggested_workflow": string | null }', - `Intent: ${intent}`, - `Current workflow: ${workflow}`, - `Routing reason: ${routingReason}`, - `Scope mode: ${scopeMode}` - ].join("\n") - }; - - try { - const modelSelection = await this.aiRouter.selectModel(request); - const response = await this.aiRouter.ask(request); - const parsed = extractJsonObject(response); - if (!parsed) { - return undefined; - } - - return { - headline: typeof parsed.headline === "string" ? parsed.headline : undefined, - summary: normalizeStringList(parsed.summary), - followUps: normalizeStringList(parsed.follow_ups), - suggestedWorkflow: normalizeSuggestedWorkflow(parsed.suggested_workflow), - modelSelection - }; - } catch (error) { - this.logger.warn("Ask AI enhancement unavailable", { - action: "ask_ai_assist_unavailable", - intent, - workflow, - error: error instanceof Error ? error.message : String(error) - }); - return undefined; - } - } - - private async buildGuidedResumeExecution( - targetPath: string, - outputPath: string, - stage: ResumeResult["summary"]["stage"], - artifacts: AskArtifact[] - ): Promise { - if (stage === "bootstrap") { - const result = await this.doctor(targetPath, outputPath); - return { - label: "Doctor", - command: `project-brain doctor . --output "${outputPath}"`, - headline: "Continued from bootstrap into Doctor.", - summary: [ - result.summary.headline, - `Checks: passed=${result.summary.passed}, warnings=${result.summary.warnings}, failed=${result.summary.failed}` - ], - artifacts: [{ label: "Doctor report", path: result.reportPath }], - followUps: result.suggestions.map((suggestion) => suggestion.command) - }; - } - - if (stage === "doctor" && !hasAskArtifact(artifacts, "Codebase map summary")) { - const result = await this.mapTarget(targetPath, outputPath); - return { - label: "Codebase Map", - command: `project-brain map-codebase . --output "${outputPath}"`, - headline: "Continued from Doctor into Codebase Map.", - summary: [ - `Languages: ${result.context.discovery.languages.join(", ") || "Unknown"}`, - `Frameworks: ${result.context.discovery.frameworks.join(", ") || "Unknown"}`, - "Generated the repository map as the next structural step." - ], - artifacts: [ - { label: "Codebase map summary", path: result.summaryPath }, - { label: "Codebase map directory", path: result.codebaseMapDir } - ], - followUps: [ - 'project-brain ask "dime que le falta criticamente"', - 'project-brain swarm "ayudame a mejorar este repo"', - `project-brain status . --output "${outputPath}"` - ] - }; - } - - if (stage === "swarm" && !hasAskArtifact(artifacts, "Improvement plan summary")) { - const result = await this.planImprovements(targetPath, outputPath, "manual"); - return { - label: "Improvement Plan", - command: `project-brain plan-improvements . --output "${outputPath}"`, - headline: "Continued from Swarm into Improvement Plan.", - summary: [ - "Converted the latest bounded analysis into a persistent roadmap.", - `Plan summary: ${result.summaryPath}`, - `Roadmap: ${result.roadmapPath}` - ], - artifacts: [ - { label: "Improvement plan summary", path: result.summaryPath }, - { label: "Improvement roadmap", path: result.roadmapPath } - ], - followUps: [ - `project-brain review-delta . --output "${outputPath}"`, - `project-brain status . --output "${outputPath}"`, - 'project-brain ask "dime que le falta criticamente"' - ] - }; - } - - if (stage === "plan-improvements" && !hasAskArtifact(artifacts, "Impact report")) { - const result = await this.reviewDelta(targetPath, outputPath, { - baseRef: "HEAD~1", - headRef: "HEAD" - }); - return { - label: "Review Delta", - command: `project-brain review-delta . --output "${outputPath}"`, - headline: "Continued from Improvement Plan into Review Delta.", - summary: [ - `Changed files: ${result.changedFiles.join(", ") || "None"}`, - `Review set size: ${result.reviewFiles.length}`, - `Related tests: ${result.impactedTests.join(", ") || "None"}` - ], - artifacts: [ - { label: "Impact report", path: result.reportPath }, - { label: "Code graph", path: result.graphPath } - ], - followUps: [ - `project-brain status . --output "${outputPath}"`, - 'project-brain ask "dime que le falta criticamente"', - 'project-brain ask "inspecciona el firewall y aprobaciones"' - ] - }; - } - - return undefined; - } - - async ask(targetPath: string, outputPath = targetPath, intent: string): Promise { - const scope = await discoverRepositoryTargets(targetPath, outputPath); - let route = routeIntent(intent); - const briefPath = path.join(outputPath, "reports", "ask_brief.md"); - const firstRepository = scope.repositories[0]; - const primaryTargetPath = firstRepository?.targetPath ?? targetPath; - const primaryOutputPath = - scope.mode === "workspace" && firstRepository - ? this.workspaceRepoOutputPath(outputPath, firstRepository) - : outputPath; - const scopeNote = - scope.mode === "workspace" && firstRepository - ? `The intent was run against the first repository in the workspace: ${firstRepository.repoName} (${firstRepository.relativePath}).` - : undefined; - const preflightContext = await this.initTarget(primaryTargetPath, primaryOutputPath); - const preflight = await preflightFacts(preflightContext, intent, { - scope: scope.mode === "workspace" ? firstRepository?.relativePath : "." - }); - const aiEnhancement = await this.buildAskAIEnhancement( - intent, - route.workflow, - route.reason, - scope.mode === "workspace" ? "workspace" : "repository" - ); - - if (route.workflow === "discover-project" && aiEnhancement?.suggestedWorkflow) { - const suggestedRoute = routeIntent(aiEnhancement.suggestedWorkflow.replace(/-/g, " ")); - route = { - ...suggestedRoute, - reason: `${route.reason} AI planner refinement suggested ${aiEnhancement.suggestedWorkflow}.` - }; - } - - let headline = ""; - let summary: string[] = []; - let artifacts: AskArtifact[] = []; - let guidedExecution: AskGuidedExecution | undefined; - - if (route.workflow === "resume-project") { - const result = await this.resume(primaryTargetPath, primaryOutputPath); - headline = result.summary.headline; - summary = [ - scopeNote ?? `Target path: ${primaryTargetPath}`, - `Recovered stage: ${result.summary.stage}`, - ...result.notes - ].filter(Boolean); - artifacts = [ - { label: "Resume report", path: result.reportPath }, - ...(result.latestArtifact ? [{ label: `Latest artifact (${result.latestArtifact.label})`, path: result.latestArtifact.path }] : []) - ]; - route.followUps = mergeUniqueStrings(route.followUps, result.suggestions.map((suggestion) => suggestion.command)).slice(0, 6); - - if (shouldAutoContinueAsk(intent, route.workflow)) { - guidedExecution = await this.buildGuidedResumeExecution( - primaryTargetPath, - primaryOutputPath, - result.summary.stage, - artifacts - ); - - if (guidedExecution) { - headline = guidedExecution.headline; - summary = mergeUniqueStrings(summary, guidedExecution.summary); - artifacts = [...artifacts, ...guidedExecution.artifacts]; - route.followUps = mergeUniqueStrings(route.followUps, guidedExecution.followUps).slice(0, 6); - route.followUps = route.followUps.filter((followUp) => followUp !== guidedExecution?.command); - } - } - } - - if (route.workflow === "discover-project") { - if (scope.mode === "workspace") { - const result = await this.mapWorkspace(targetPath, outputPath, scope.repositories); - headline = `Detected a workspace with ${result.repositories.length} repositories.`; - summary = [ - `Root path: ${result.rootPath}`, - `Repositories: ${result.repositories.map((repository) => repository.repoName).join(", ") || "None"}`, - "Discovery completed and codebase maps were generated for each repository." - ]; - artifacts = [ - { label: "Workspace codebase map", path: result.summaryPath }, - ...result.repositories.slice(0, 3).map((repository) => ({ - label: `${repository.repoName} summary`, - path: repository.summaryPath - })) - ]; - } else { - const result = await this.mapTarget(primaryTargetPath, primaryOutputPath); - headline = `Detected ${result.context.repoName} and generated its repository map.`; - summary = [ - `Languages: ${result.context.discovery.languages.join(", ") || "Unknown"}`, - `Frameworks: ${result.context.discovery.frameworks.join(", ") || "Unknown"}`, - `Testing: ${result.context.discovery.testing.join(", ") || "Not detected"}`, - `Infrastructure: ${result.context.discovery.infrastructure.join(", ") || "Not detected"}` - ]; - artifacts = [ - { label: "Codebase map summary", path: result.summaryPath }, - { label: "Codebase map directory", path: result.codebaseMapDir } - ]; - } - } - - if (route.workflow === "critical-gaps") { - if (scope.mode === "workspace") { - const result = await this.analyzeWorkspace(targetPath, outputPath, route.trigger, scope.repositories); - headline = `Analyzed ${result.repositories.length} repositories for critical gaps.`; - summary = [ - `Trigger used: ${route.trigger}`, - `Repositories: ${result.repositories.map((repository) => repository.repoName).join(", ") || "None"}`, - `Cross-repo intelligence artifacts were generated for the workspace.` - ]; - artifacts = [ - { label: "Ecosystem report", path: result.ecosystemReportPath }, - { label: "Knowledge graph", path: result.knowledgeGraphPath }, - { label: "Runtime observability", path: result.runtimeObservabilityPath } - ]; - } else { - const result = await this.analyzeTarget(primaryTargetPath, primaryOutputPath, route.trigger); - const approved = result.governanceSummary?.proposals.filter((proposal) => proposal.status === "APPROVED").length ?? 0; - const review = result.governanceSummary?.proposals.filter((proposal) => proposal.status === "REQUIRES_HUMAN_REVIEW").length ?? 0; - headline = `Analyzed ${result.context.repoName} for critical gaps and governance findings.`; - summary = [ - `Languages: ${result.context.discovery.languages.join(", ") || "Unknown"}`, - `Frameworks: ${result.context.discovery.frameworks.join(", ") || "Unknown"}`, - `Agent reports: ${result.agentReports.length}`, - `Proposals: approved=${approved}, review=${review}` - ]; - artifacts = [ - { label: "Risk report", path: result.riskReportPath }, - { label: "Weekly system report", path: result.weeklyReportPath }, - { label: "Improvement proposals", path: result.governanceSummary?.improvementReportPath ?? path.join(primaryOutputPath, "reports", "improvement_proposals.md") } - ]; - } - } - - if (route.workflow === "security-audit") { - const result = await this.securityAudit(primaryTargetPath, primaryOutputPath, route.trigger); - const severityCounts = result.findings.reduce>((accumulator, finding) => { - accumulator[finding.severity] = (accumulator[finding.severity] ?? 0) + 1; - return accumulator; - }, {}); - headline = result.headline; - summary = [ - scopeNote ?? `Target path: ${primaryTargetPath}`, - `Verdict: ${result.verdict}`, - `Findings: critical=${severityCounts.critical ?? 0}, high=${severityCounts.high ?? 0}, medium=${severityCounts.medium ?? 0}, low=${severityCounts.low ?? 0}, info=${severityCounts.info ?? 0}`, - `Context gaps: ${result.verifiedContext.contextGaps.slice(0, 3).join(" | ") || "None"}` - ].filter(Boolean); - artifacts = [ - { label: "Security audit report", path: result.reportPath }, - { label: "Security audit memory", path: result.memoryPath }, - ...(result.contextLiteReportPath ? [{ label: "Context-lite report", path: result.contextLiteReportPath }] : []) - ]; - } - - if (route.workflow === "review-latest-changes") { - const result = await this.reviewDelta(primaryTargetPath, primaryOutputPath, { - baseRef: "HEAD~1", - headRef: "HEAD" - }); - headline = `Built a bounded review set for the latest repository changes.`; - summary = [ - scopeNote ?? `Target path: ${primaryTargetPath}`, - `Changed files: ${result.changedFiles.join(", ") || "None"}`, - `Review set size: ${result.reviewFiles.length}`, - `Related tests: ${result.impactedTests.join(", ") || "None"}` - ].filter(Boolean); - artifacts = [ - { label: "Impact report", path: result.reportPath }, - { label: "Code graph", path: result.graphPath } - ]; - } - - if (route.workflow === "inspect-firewall") { - const result = await this.inspectFirewall(primaryTargetPath, primaryOutputPath, route.trigger); - headline = `Inspected the current agent policy and approval model.`; - summary = [ - scopeNote ?? `Target path: ${primaryTargetPath}`, - `Allowed tasks: ${result.firewall.stats.allowed}`, - `Review-required tasks: ${result.firewall.stats.reviewRequired}`, - `Blocked tasks: ${result.firewall.stats.blocked}` - ].filter(Boolean); - artifacts = [ - { label: "Firewall report", path: result.firewall.reportPath }, - { label: "Firewall policy JSON", path: result.firewall.policyPath }, - { label: "Task packet directory", path: result.firewall.packetDir } - ]; - } - - if (route.workflow === "build-code-graph") { - const result = await this.buildCodeGraph(primaryTargetPath, primaryOutputPath); - headline = `Built or refreshed the structural code graph and factual repository graph.`; - summary = [ - scopeNote ?? `Target path: ${primaryTargetPath}`, - `Build mode: ${result.graph.build.mode}`, - `Files: ${result.graph.stats.files}`, - `Symbols: ${result.graph.stats.symbols}`, - `Edges: ${result.graph.stats.edges}`, - result.factGraph ? `Fact graph nodes: ${result.factGraph.stats.nodes}` : undefined, - result.factGraph ? `Fact graph edges: ${result.factGraph.stats.edges}` : undefined - ].filter((entry): entry is string => Boolean(entry)); - artifacts = [ - { label: "Code graph", path: result.graphPath }, - ...(result.factGraphPath ? [{ label: "Repository fact graph", path: result.factGraphPath }] : []), - ...(result.factReportPath ? [{ label: "Repository fact graph report", path: result.factReportPath }] : []) - ]; - } - - if (aiEnhancement) { - headline = aiEnhancement.headline ?? headline; - summary = mergeUniqueStrings(summary, aiEnhancement.summary); - route.followUps = mergeUniqueStrings(route.followUps, aiEnhancement.followUps).slice(0, 6); - } - - await ensureDir(path.dirname(briefPath)); - await writeFileEnsured( - briefPath, - `# Ask Brief - -## Request - -- Intent: ${intent} -- Workflow: ${route.workflow} -- Scope mode: ${scope.mode} -- Routing reason: ${route.reason} - -## Headline - -${headline} - -## Summary - -${renderList(summary)} - -## Artifacts - -${renderArtifactList(artifacts)} - -## Preflight Facts - -- Confidence: ${preflight.confidence} -- Facts found: ${preflight.factsFound ? "yes" : "no"} -- Recommended next action: ${preflight.recommendedNextAction} -- Fresh scopes: ${preflight.freshness.freshScopes.join(", ") || "None"} -- Stale scopes ignored: ${preflight.freshness.staleScopes.join(", ") || "None"} - -Facts: -${renderList(preflight.facts)} - -Evidence: -${renderList(preflight.evidence)} - -Unknowns: -${renderList(preflight.unknowns)} - -## Guided continuation - -${guidedExecution - ? renderList([ - `Step: ${guidedExecution.label}`, - `Command: ${guidedExecution.command}`, - `Headline: ${guidedExecution.headline}`, - ...guidedExecution.summary - ]) - : "- Not used"} - -## AI Assist - -${aiEnhancement - ? renderList([ - `Model: ${aiEnhancement.modelSelection.model}`, - `Provider: ${aiEnhancement.modelSelection.provider}`, - `Profile: ${aiEnhancement.modelSelection.profile}`, - `Residency: ${aiEnhancement.modelSelection.residency}`, - ...(aiEnhancement.suggestedWorkflow ? [`Suggested workflow: ${aiEnhancement.suggestedWorkflow}`] : []) - ]) - : "- Not used"} - -## Suggested next prompts - -${renderList(route.followUps)} -` - ); - - return { - intent, - workflow: route.workflow, - targetPath, - outputPath, - scopeMode: scope.mode === "workspace" ? "workspace" : "repository", - briefPath, - headline, - summary, - artifacts, - followUps: route.followUps, - routingReason: route.reason, - preflightFacts: preflight, - guidedExecution: guidedExecution - ? { - label: guidedExecution.label, - command: guidedExecution.command, - headline: guidedExecution.headline, - summary: guidedExecution.summary, - artifacts: guidedExecution.artifacts - } - : undefined, - aiAssistance: aiEnhancement - ? { - provider: aiEnhancement.modelSelection.provider, - model: aiEnhancement.modelSelection.model, - profile: aiEnhancement.modelSelection.profile, - residency: aiEnhancement.modelSelection.residency, - summary: aiEnhancement.summary, - suggestedWorkflow: aiEnhancement.suggestedWorkflow - } - : undefined - }; - } - - async swarm( - targetPath: string, - outputPath = targetPath, - intent: string, - options?: { - engine?: "bounded" | "deepagents"; - parallelism?: number; - chunkSize?: number; - taskTimeoutMs?: number; - maxRetries?: number; - plannerTimeoutMs?: number; - synthesisTimeoutMs?: number; - runTimeoutMs?: number; - maxQueuedTasks?: number; - scopeBias?: "balanced" | "source-first"; - preset?: TokenPreset; - } - ): Promise { - const context = await this.initTarget(targetPath, outputPath); - await writeMemoryBriefArtifacts(context); - const memoryReadiness = await assessMemoryReadiness(context); - if (memoryReadiness.status !== "ready") { - throw new Error(`MEMORY_BRIEF is not ready (${memoryReadiness.status}): ${memoryReadiness.reason}`); - } - let result: SwarmRunResult; - if (options?.engine === "deepagents") { - result = await runDeepAgentsSwarm(context, intent, this.aiRouter, options); - } else { - result = await runSwarm(context, intent, this.aiRouter, options); - } - await recordSwarmLearningArtifacts(context.memoryDir, result); - await writeMemoryBriefArtifacts(context); - return result; - } - - async selfImprove( - targetPath: string, - outputPath = targetPath, - intent = "identify high-value improvements" - ): Promise { - await this.start(targetPath, outputPath, "analiza y mejora project-brain"); - const result = await this.swarm(targetPath, outputPath, intent, { - preset: "cheap", - parallelism: 2, - chunkSize: 1, - taskTimeoutMs: 90_000, - plannerTimeoutMs: 60_000, - synthesisTimeoutMs: 60_000, - runTimeoutMs: 120_000, - maxQueuedTasks: 4, - maxRetries: 0, - scopeBias: "source-first" - }); - await this.planImprovements(targetPath, outputPath, "architecture-review"); - return result; - } - - async architecturePlan(targetPath: string, outputPath = targetPath): Promise { - const scope = await discoverRepositoryTargets(targetPath, outputPath); - const firstRepository = scope.repositories[0]; - const primaryTargetPath = firstRepository?.targetPath ?? targetPath; - const primaryOutputPath = - scope.mode === "workspace" && firstRepository - ? this.workspaceRepoOutputPath(outputPath, firstRepository) - : outputPath; - const context = await this.initTarget(primaryTargetPath, primaryOutputPath); - const result = await writeArchitecturePlanArtifacts(context); - await writeMemoryBriefArtifacts(context); - return result; - } - - async scaffoldProject(targetPath: string, input: ProjectSeedInput): Promise { - return writeProjectSeedArtifacts(path.resolve(targetPath), input); - } - - async planImprovements( - targetPath: string, - outputPath = targetPath, - trigger: GovernanceTrigger = "manual" - ): Promise { - const scope = await discoverRepositoryTargets(targetPath, outputPath); - const firstRepository = scope.repositories[0]; - const primaryTargetPath = firstRepository?.targetPath ?? targetPath; - const primaryOutputPath = - scope.mode === "workspace" && firstRepository - ? this.workspaceRepoOutputPath(outputPath, firstRepository) - : outputPath; - const analysis = await this.analyzeTarget(primaryTargetPath, primaryOutputPath, trigger); - const annotations = await listContextAnnotations(primaryOutputPath); - - const result = await writeImprovementPlanArtifacts( - analysis.context, - analysis.agentReports, - analysis.governanceSummary!, - annotations - ); - await writeMemoryBriefArtifacts(analysis.context); - return result; - } - - async contextSearch( - targetPath: string, - outputPath = targetPath, - query = "", - trust?: "official" | "maintainer" | "community" - ): Promise { - const context = await this.initTarget(targetPath, outputPath); - return searchContextRegistry(context, query, trust); - } - - async contextGet(targetPath: string, outputPath = targetPath, id = ""): Promise { - const context = await this.initTarget(targetPath, outputPath); - return getContextRegistryEntry(context, id); - } - - async contextSources(targetPath: string, outputPath = targetPath): Promise { - const context = await this.initTarget(targetPath, outputPath); - return listContextSources(context); - } - - async ecosystemRadar( - targetPath: string, - outputPath = targetPath, - options: { - limit?: number; - bucketId?: string; - seedOnly?: boolean; - } = {} - ): Promise { - const context = await this.initTarget(targetPath, outputPath); - return runEcosystemRadar(context, options); - } - - async analyzeTarget( - targetPath: string, - outputPath = targetPath, - trigger: GovernanceTrigger = "manual" - ): Promise { - const cycleId = createCycleId(trigger); - const span = this.metricsCollector.startCycle(trigger, cycleId); - - return withLogContext({ cycleId }, async () => { - this.logger.info("Cycle started", { - action: "cycle_start", - cycleType: trigger, - targetPath, - outputPath - }); - - const discovery = await this.discoveryEngine.analyze(targetPath, { - excludePaths: this.discoveryExclusions(targetPath, outputPath) - }); - const context = await this.contextBuilder.build(discovery, outputPath); - const governanceRun = await this.selfGovernance.run(context, trigger); - const agentReports = governanceRun.agentReports; - const reportAssessments = await Promise.all(agentReports.map((report) => assessAgentReportQuality(context, report))); - const acceptedReports = reportAssessments - .filter((assessment) => assessment.status === "accepted") - .map((assessment) => assessment.report); - const effectiveReports = acceptedReports.length > 0 ? acceptedReports : agentReports; - const fellBackToRawReports = acceptedReports.length === 0 && agentReports.length > 0; - const reportQualityPath = path.join(context.reportsDir, "report_quality.md"); - - await writeFileEnsured( - reportQualityPath, - buildReportQualityContent(context, reportAssessments, effectiveReports, fellBackToRawReports) - ); - - for (const record of governanceRun.summary.executionRecords) { - this.logger.info("Agent execution observed", { - agent: record.agentId, - action: - record.status === "completed" - ? "agent_complete" - : record.status === "failed" - ? "agent_failed" - : "agent_start", - taskId: record.taskId, - startedAt: record.startedAt, - completedAt: record.completedAt ?? null, - status: record.status - }); - } - - for (const assessment of reportAssessments.filter((entry) => entry.status === "review-required")) { - this.logger.warn("Agent report marked for manual review", { - action: "report_quality_review_required", - agent: assessment.report.agentId, - score: assessment.score, - notes: assessment.notes, - reportPath: assessment.report.outputPath - }); - } - - await updatePersistentMemory(context, effectiveReports); - await recordLearningArtifacts(context.memoryDir, effectiveReports); - await writeMemoryBriefArtifacts(context); - - if (governanceRun.summary.proposals.length > 0) { - this.logger.info("Improvement proposals generated", { - action: "proposal_generated", - proposalsGenerated: governanceRun.summary.proposals.length, - approved: governanceRun.summary.proposals.filter((proposal) => proposal.status === "APPROVED").length, - review: governanceRun.summary.proposals.filter((proposal) => proposal.status === "REQUIRES_HUMAN_REVIEW").length, - rejected: governanceRun.summary.proposals.filter((proposal) => proposal.status === "REJECTED").length - }); - } - - const weeklyReportPath = await this.writeWeeklySystemReport(context, effectiveReports); - this.logger.info("Weekly report generated", { - action: "report_generated", - report: "weekly_system_report", - reportPath: weeklyReportPath - }); - const riskReportPath = await this.writeRiskReport(context, effectiveReports); - this.logger.info("Risk report generated", { - action: "report_generated", - report: "risk_report", - reportPath: riskReportPath - }); - - const telemetry = this.metricsCollector.completeCycle(span, context.repoName, effectiveReports, governanceRun.summary); - const telemetryPath = await this.metricsCollector.persistCycleTelemetry(context, telemetry); - const runtimeObservabilityPath = await this.metricsCollector.writeRuntimeObservabilityReport(context.reportsDir); - - this.logger.info("Runtime observability updated", { - action: "report_generated", - report: "runtime_observability", - reportPath: runtimeObservabilityPath, - telemetryPath - }); - - this.logger.info("Cycle completed", { - action: "cycle_complete", - repoName: context.repoName, - outputPath, - highestRisk: highestRisk(effectiveReports), - cycleDuration: telemetry.cycleDuration, - agentsExecuted: telemetry.agentsExecuted, - risksDetected: telemetry.risksDetected - }); - - return { - context, - agentReports, - weeklyReportPath, - riskReportPath, - reportQualityPath, - governanceSummary: governanceRun.summary - }; - }); - } - - async runAgents( - targetPath: string, - outputPath = targetPath, - trigger: GovernanceTrigger = "manual" - ): Promise { - const result = await this.analyzeTarget(targetPath, outputPath, trigger); - return result.agentReports; - } - - async generateWeekly(targetPath: string, outputPath = targetPath): Promise { - return this.analyzeTarget(targetPath, outputPath, "weekly-review"); - } - - async analyzeScope( - targetPath: string, - outputPath = targetPath, - trigger: GovernanceTrigger = "manual" - ): Promise { - const scope = await discoverRepositoryTargets(targetPath, outputPath); - - if (scope.mode === "workspace") { - return this.analyzeWorkspace(targetPath, outputPath, trigger, scope.repositories); - } - - return this.analyzeTarget(scope.repositories[0]?.targetPath ?? targetPath, outputPath, trigger); - } - - async mapScope(targetPath: string, outputPath = targetPath): Promise { - const scope = await discoverRepositoryTargets(targetPath, outputPath); - - if (scope.mode === "workspace") { - return this.mapWorkspace(targetPath, outputPath, scope.repositories); - } - - return this.mapTarget(scope.repositories[0]?.targetPath ?? targetPath, outputPath); - } - - async generateWeeklyScope(targetPath: string, outputPath = targetPath): Promise { - return this.analyzeScope(targetPath, outputPath, "weekly-review"); - } - - async recordFeedback( - targetPath: string, - outputPath: string, - input: Parameters[1] - ) { - const discovery = await this.discoveryEngine.analyze(targetPath, { - excludePaths: this.discoveryExclusions(targetPath, outputPath) - }); - const context = await this.contextBuilder.build(discovery, outputPath); - return this.selfGovernance.recordFeedback(context, input); - } - - async annotateTarget( - targetPath: string, - outputPath: string, - input: { - scope: string; - note: string; - } - ): Promise { - await this.initTarget(targetPath, outputPath); - return writeContextAnnotation(outputPath, input.scope, input.note); - } - - async readAnnotation(targetPath: string, outputPath: string, scope: string): Promise { - await this.initTarget(targetPath, outputPath); - return readContextAnnotation(outputPath, scope); - } - - async listAnnotations(targetPath: string, outputPath: string): Promise { - await this.initTarget(targetPath, outputPath); - return listContextAnnotations(outputPath); - } - - async clearAnnotation(targetPath: string, outputPath: string, scope: string): Promise { - await this.initTarget(targetPath, outputPath); - return clearContextAnnotation(outputPath, scope); - } - - async collectReportManifest(outputPath: string): Promise { - const files = await walkDirectory(outputPath, 8000, [], { includeGeneratedArtifacts: true }); - return { - memoryFiles: files.filter((file) => file.startsWith("AI_CONTEXT/")), - reportFiles: files.filter((file) => file.startsWith("reports/")), - docFiles: files.filter((file) => file.startsWith("docs/")), - learningFiles: files.filter((file) => file.startsWith("memory/learnings/")), - swarmFiles: files.filter((file) => file.startsWith("memory/swarm/") || file === "reports/swarm_run.md"), - firewallFiles: files.filter((file) => file.startsWith("memory/firewall/")), - securityFiles: files.filter((file) => file.startsWith("memory/security/") || file === "reports/security_audit.md"), - knowledgeFiles: files.filter((file) => file.startsWith("memory/knowledge_graph/")), - contextRegistryFiles: files.filter((file) => file.startsWith("memory/context_registry/") || file.startsWith("AI_CONTEXT/EXTERNAL_CONTEXT/")), - taskFiles: files.filter((file) => file.startsWith("tasks/")), - patchProposalFiles: files.filter((file) => file.startsWith("patch_proposals/")), - proposalFiles: files.filter( - (file) => file.startsWith("docs/proposals/") || file.startsWith("proposal/") - ) - }; - } - - private workspaceRepoOutputPath(outputPath: string, repository: RepositoryTarget): string { - const slug = repository.relativePath - .replace(/[^a-zA-Z0-9/_-]+/g, "_") - .replace(/\//g, "_") - .replace(/^_+|_+$/g, ""); - - return path.join(outputPath, "ecosystem", slug || repository.repoName); - } - - private async readRepositoryTelemetry( - repository: EcosystemRepositoryResult - ): Promise[1] | undefined> { - const telemetryFiles = await walkDirectory(path.join(repository.outputPath, "reports", "telemetry")); - const latestFile = telemetryFiles - .filter((file) => file.startsWith("cycle_") && file.endsWith(".json")) - .sort((left, right) => right.localeCompare(left))[0]; - - if (!latestFile) { - return undefined; - } - - return readJsonSafe(path.join(repository.outputPath, "reports", "telemetry", latestFile)); - } - - private async analyzeWorkspace( - rootPath: string, - outputPath: string, - trigger: GovernanceTrigger, - repositories: RepositoryTarget[] - ): Promise { - const cycleId = createCycleId(`ecosystem_${trigger}`); - const span = this.metricsCollector.startCycle(trigger, cycleId); - - await ensureDir(outputPath); - - return withLogContext({ cycleId }, async () => { - this.logger.info("Workspace analysis started", { - component: "orchestrator", - action: "cycle_start", - cycleType: trigger, - targetPath: rootPath, - outputPath, - repositories: uniqueRepositoryNames(repositories) - }); - - const ecosystemResults = await Promise.all( - repositories.map(async (repository) => { - const repositoryOutputPath = this.workspaceRepoOutputPath(outputPath, repository); - const result = await this.analyzeTarget(repository.targetPath, repositoryOutputPath, trigger); - return { - repoName: repository.repoName, - relativePath: repository.relativePath, - targetPath: repository.targetPath, - outputPath: repositoryOutputPath, - result - } satisfies EcosystemRepositoryResult; - }) - ); - - const { knowledgeGraphPath, proposalPaths, ecosystemReportPath } = await buildKnowledgeGraphArtifacts( - outputPath, - ecosystemResults - ); - const rootReportsDir = path.join(outputPath, "reports"); - const repositoryTelemetries = ( - await Promise.all(ecosystemResults.map((repository) => this.readRepositoryTelemetry(repository))) - ).filter(Boolean) as Array[1]>; - - await Promise.all( - repositoryTelemetries.map((telemetry) => this.metricsCollector.persistTelemetry(rootReportsDir, telemetry)) - ); - const ecosystemTelemetry = this.metricsCollector.completeCycle( - span, - "ecosystem", - ecosystemResults.flatMap((repository) => repository.result.agentReports), - { - trigger, - tasks: ecosystemResults.flatMap((repository) => repository.result.governanceSummary?.tasks ?? []), - messages: ecosystemResults.flatMap((repository) => repository.result.governanceSummary?.messages ?? []), - evaluations: ecosystemResults.flatMap((repository) => repository.result.governanceSummary?.evaluations ?? []), - learnings: ecosystemResults.flatMap((repository) => repository.result.governanceSummary?.learnings ?? []), - proposals: ecosystemResults.flatMap((repository) => repository.result.governanceSummary?.proposals ?? []), - executionRecords: ecosystemResults.flatMap( - (repository) => repository.result.governanceSummary?.executionRecords ?? [] - ), - agentActivityReportPath: ecosystemResults.map((repository) => repository.result.governanceSummary?.agentActivityReportPath).filter(Boolean).join(", "), - improvementReportPath: ecosystemResults.map((repository) => repository.result.governanceSummary?.improvementReportPath).filter(Boolean).join(", ") - } - ); - const telemetryPath = await this.metricsCollector.persistTelemetry(rootReportsDir, { - ...ecosystemTelemetry, - agentIds: ecosystemResults.flatMap((repository) => - repository.result.governanceSummary?.tasks.map((task) => `${repository.repoName}:${task.agentId}`) ?? [] - ), - riskTypes: ecosystemResults.flatMap((repository) => - repository.result.agentReports - .filter((report) => report.findings.length > 0) - .map((report) => `${repository.repoName}:${report.riskLevel}`) - ) - }); - const runtimeObservabilityPath = await this.metricsCollector.writeRuntimeObservabilityReport(rootReportsDir); - - this.logger.info("Workspace analysis completed", { - component: "orchestrator", - action: "cycle_complete", - cycleType: trigger, - cycleId, - repositories: ecosystemResults.map((repository) => repository.repoName), - knowledgeGraphPath, - ecosystemReportPath, - telemetryPath - }); - - return { - rootPath, - outputPath, - trigger, - repositories: ecosystemResults, - knowledgeGraphPath, - ecosystemReportPath, - telemetryPath, - runtimeObservabilityPath, - proposalPaths - }; - }); - } - - private async mapWorkspace( - rootPath: string, - outputPath: string, - repositories: RepositoryTarget[] - ): Promise { - await ensureDir(outputPath); - - const results = await Promise.all( - repositories.map(async (repository) => { - const repositoryOutputPath = this.workspaceRepoOutputPath(outputPath, repository); - const result = await this.mapTarget(repository.targetPath, repositoryOutputPath); - - return { - repoName: repository.repoName, - relativePath: repository.relativePath, - targetPath: repository.targetPath, - outputPath: repositoryOutputPath, - codebaseMapDir: result.codebaseMapDir, - files: result.files, - summaryPath: result.summaryPath - }; - }) - ); - - const summaryPath = await this.writeWorkspaceCodebaseMapSummary(rootPath, outputPath, results); - - return { - rootPath, - outputPath, - repositories: results, - summaryPath - }; - } - - private async writeWorkspaceCodebaseMapSummary( - rootPath: string, - outputPath: string, - repositories: EcosystemCodebaseMapResult["repositories"] - ): Promise { - const summaryPath = path.join(outputPath, "docs", "ecosystem_codebase_map.md"); - const content = `# Ecosystem Codebase Map - -- Root path: ${rootPath} -- Repositories mapped: ${repositories.length} -- Repository names: ${uniqueRepositoryNames(repositories).join(", ") || "None"} - -## Repository outputs - -${renderList( - repositories.map( - (repository) => - `${repository.repoName} | relative path: ${repository.relativePath} | codebase map: ${repository.codebaseMapDir}` - ) -)} -`; - - await writeFileEnsured(summaryPath, content); - return summaryPath; - } - - private async writeWeeklySystemReport(context: ProjectContext, agentReports: AgentReport[]): Promise { - const schedule = this.scheduler.describeWindow(new Date(context.scannedAt)); - const outputPath = path.join(context.reportsDir, "weekly_system_report.md"); - const content = `# Weekly System Report - -## Executive Summary - -- Repository: ${context.repoName} -- Window: ${schedule.label} -- Overall risk: ${highestRisk(agentReports)} -- Next suggested run: ${schedule.nextRun} - -## Agent summaries - -${agentReports.map((report) => `### ${report.title}\n\n- Risk: ${report.riskLevel}\n- Summary: ${report.summary}`).join("\n\n")} - -## Recommended actions - -${renderList(agentReports.flatMap((report) => report.recommendations))} -`; - await writeFileEnsured(outputPath, content); - return outputPath; - } - - private async writeRiskReport(context: ProjectContext, agentReports: AgentReport[]): Promise { - const outputPath = path.join(context.reportsDir, "risk_report.md"); - const prioritizedFindings = agentReports - .filter((report) => report.findings.length > 0) - .sort((left, right) => { - const priority = { high: 3, medium: 2, low: 1 }; - return priority[right.riskLevel] - priority[left.riskLevel]; - }) - .flatMap((report) => report.findings.map((finding) => `[${report.title}] ${finding}`)); - - const content = `# Risk Report - -## Highest Risks - -${renderList(prioritizedFindings)} - -## Follow-up - -${renderList(agentReports.flatMap((report) => report.recommendations))} -`; - await writeFileEnsured(outputPath, content); - return outputPath; - } -} diff --git a/core/orchestrator/scheduler.ts b/core/orchestrator/scheduler.ts deleted file mode 100644 index 08fd9a1..0000000 --- a/core/orchestrator/scheduler.ts +++ /dev/null @@ -1 +0,0 @@ -export { WeeklyScheduler } from "../scheduler"; diff --git a/core/reaction_engine/index.ts b/core/reaction_engine/index.ts deleted file mode 100644 index 36d78f8..0000000 --- a/core/reaction_engine/index.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { buildWorkflowRuntimeDefinitions } from "../workflow_registry"; -import type { DoctorCheck, DoctorResult, ProjectContext, ResumeResult, StatusArtifactSummary, StatusResult, SuggestedAction } from "../../shared/types"; - -function uniqueSuggestions(actions: SuggestedAction[]): SuggestedAction[] { - const seen = new Set(); - const priorityRank: Record = { - high: 3, - medium: 2, - low: 1 - }; - - return actions - .filter((action) => { - const key = action.command.trim() || action.label.trim(); - if (seen.has(key)) { - return false; - } - seen.add(key); - return true; - }) - .sort((left, right) => { - const rankDelta = priorityRank[right.priority] - priorityRank[left.priority]; - return rankDelta !== 0 ? rankDelta : left.label.localeCompare(right.label); - }); -} - -function outputFlag(context: ProjectContext): string { - return `--output "${context.outputPath}"`; -} - -function hasArtifact(artifacts: StatusArtifactSummary[], label: string): boolean { - return artifacts.some((artifact) => artifact.label === label && artifact.exists); -} - -function failedChecks(checks: DoctorCheck[]): DoctorCheck[] { - return checks.filter((check) => check.status === "fail"); -} - -function warningChecks(checks: DoctorCheck[]): DoctorCheck[] { - return checks.filter((check) => check.status === "warn"); -} - -export function deriveDoctorSuggestions(result: Pick): SuggestedAction[] { - const actions: SuggestedAction[] = []; - const failed = failedChecks(result.checks); - const warnings = warningChecks(result.checks); - const output = outputFlag(result.context); - - if (failed.some((check) => check.id === "cli-build")) { - actions.push({ - label: "Build CLI", - command: "npm run build", - rationale: "The built CLI artifact is missing, so runtime commands may fail.", - priority: "high" - }); - } - - if (failed.some((check) => check.id === "model-config")) { - actions.push({ - label: "Restore Model Config", - command: `project-brain models`, - rationale: "The model config file is missing or unreadable.", - priority: "high" - }); - } - - if (failed.some((check) => check.id === "model-profiles")) { - actions.push({ - label: "Repair Local Models", - command: "project-brain models", - rationale: "One or more critical model profiles are unavailable.", - priority: "high" - }); - } - - if (warnings.some((check) => check.id === "ollama-binary" || check.id === "ollama-api")) { - actions.push({ - label: "Inspect Ollama", - command: "project-brain models", - rationale: "Local model execution is degraded or unavailable.", - priority: "medium" - }); - } - - if (warnings.some((check) => check.id === "git-repository")) { - actions.push({ - label: "Map Outside Git", - command: `project-brain map-codebase . ${output}`, - rationale: "The target is not a git repo, so static mapping is the best next step.", - priority: "medium" - }); - } - - if (failed.length === 0) { - actions.push({ - label: "Inspect Operational Status", - command: `project-brain status . ${output}`, - rationale: "The environment is healthy enough to inspect current artifacts and next steps.", - priority: warnings.length > 0 ? "medium" : "low" - }); - } - - return uniqueSuggestions(actions); -} - -export function deriveStatusSuggestions(result: Pick): SuggestedAction[] { - const actions: SuggestedAction[] = []; - const output = outputFlag(result.context); - const workflows = buildWorkflowRuntimeDefinitions(result.context); - const workflowById = new Map(workflows.map((workflow) => [workflow.workflowId, workflow])); - - if (result.summary.doctorStatus === "unknown") { - actions.push({ - label: "Run Doctor", - command: `project-brain doctor . ${output}`, - rationale: "There is no doctor snapshot for this output path yet.", - priority: "high" - }); - } else if (result.summary.doctorStatus === "fail" || result.summary.doctorStatus === "warn") { - actions.push({ - label: "Re-run Doctor", - command: `project-brain doctor . ${output}`, - rationale: "The latest doctor snapshot found issues or warnings that should be rechecked.", - priority: result.summary.doctorStatus === "fail" ? "high" : "medium" - }); - } - - if (!hasArtifact(result.artifacts, "Codebase Map")) { - const workflow = workflowById.get("map-codebase"); - actions.push({ - label: workflow?.commandLabel ?? "Generate Codebase Map", - command: workflow?.command ?? `project-brain map-codebase . ${output}`, - rationale: workflow?.rationale ?? "The output path does not have a current structural map yet.", - priority: workflow?.priority ?? "high" - }); - } - - if (!hasArtifact(result.artifacts, "Memory Brief")) { - actions.push({ - label: "Refresh Memory Brief", - command: `project-brain status . ${output}`, - rationale: "The compact memory handoff is missing; status refreshes project memory before deeper analysis.", - priority: "high" - }); - } - - if (!hasArtifact(result.artifacts, "Repository Fact Graph")) { - const workflow = workflowById.get("code-graph"); - actions.push({ - label: workflow?.commandLabel ?? "Build Repository Fact Graph", - command: workflow?.command ?? `project-brain code-graph . ${output}`, - rationale: workflow?.rationale ?? "A factual graph gives later runs a compact structural index before spending tokens on broad model analysis.", - priority: hasArtifact(result.artifacts, "Codebase Map") ? "high" : "medium" - }); - } - - if (hasArtifact(result.artifacts, "Repository Fact Graph") && !hasArtifact(result.artifacts, "Fact Query")) { - const workflow = workflowById.get("fact-query"); - actions.push({ - label: workflow?.commandLabel ?? "Query Factual Memory", - command: workflow?.command ?? `project-brain fact-query "memory optimization" . ${output}`, - rationale: workflow?.rationale ?? "A deterministic query gives agents a compact starting context before broad model analysis.", - priority: workflow?.priority ?? "medium" - }); - } - - if (!hasArtifact(result.artifacts, "Runbook")) { - const workflow = workflowById.get("runbook"); - actions.push({ - label: workflow?.commandLabel ?? "Create Token-Aware Runbook", - command: workflow?.command ?? `project-brain runbook "optimize analysis and cost" . ${output}`, - rationale: workflow?.rationale ?? "A runbook orders deterministic memory, graph, query, governance, and swarm steps before expensive analysis.", - priority: workflow?.priority ?? "medium" - }); - } - - if (!hasArtifact(result.artifacts, "Harness Audit")) { - const workflow = workflowById.get("harness-audit"); - actions.push({ - label: workflow?.commandLabel ?? "Audit Harness Readiness", - command: workflow?.command ?? `project-brain harness-audit . ${output}`, - rationale: workflow?.rationale ?? "A harness audit checks progressive memory, cost gates, and continuity before model-heavy work.", - priority: workflow?.priority ?? "medium" - }); - } - - if (!hasArtifact(result.artifacts, "Swarm")) { - const workflow = workflowById.get("swarm"); - actions.push({ - label: workflow?.commandLabel ?? "Run Self Improve", - command: workflow?.command ?? `project-brain self-improve . ${output}`, - rationale: workflow?.rationale ?? "There is no swarm/self-improvement run in this output path yet.", - priority: workflow?.priority ?? "high" - }); - } - - if (!hasArtifact(result.artifacts, "Improvement Plan") && hasArtifact(result.artifacts, "Swarm")) { - const workflow = workflowById.get("plan-improvements"); - actions.push({ - label: workflow?.commandLabel ?? "Build Improvement Plan", - command: workflow?.command ?? `project-brain plan-improvements . ${output}`, - rationale: workflow?.rationale ?? "You already have analysis artifacts, so the next useful step is a persistent roadmap.", - priority: workflow?.priority ?? "medium" - }); - } - - if (!hasArtifact(result.artifacts, "Firewall")) { - const workflow = workflowById.get("firewall"); - actions.push({ - label: workflow?.commandLabel ?? "Inspect Firewall", - command: workflow?.command ?? `project-brain firewall . --trigger repository-change ${output}`, - rationale: workflow?.rationale ?? "There is no current agent policy snapshot in this output path.", - priority: workflow?.priority ?? "medium" - }); - } - - if (!hasArtifact(result.artifacts, "Impact Radius")) { - const workflow = workflowById.get("review-delta"); - actions.push({ - label: workflow?.commandLabel ?? "Review Recent Changes", - command: workflow?.command ?? `project-brain review-delta . ${output}`, - rationale: workflow?.rationale ?? "There is no bounded review surface for recent git changes.", - priority: workflow?.priority ?? "low" - }); - } - - return uniqueSuggestions(actions); -} - -export function deriveResumeSuggestions( - result: Pick, - statusSuggestions: SuggestedAction[] -): SuggestedAction[] { - const actions: SuggestedAction[] = []; - const output = outputFlag(result.context); - - if (result.summary.stage === "bootstrap") { - actions.push({ - label: "Run Doctor", - command: `project-brain doctor . ${output}`, - rationale: "There is no resumable output state yet, so doctor is the safest bootstrap step.", - priority: "high" - }); - } - - if (result.summary.stage === "start" && !hasArtifact(result.artifacts, "Swarm")) { - actions.push({ - label: "Continue With Cheap Swarm", - command: `project-brain swarm "optimize analysis and cost" . ${output} --preset cheap`, - rationale: "The guided start path prepared cheap context; the next optional step is bounded delegated analysis.", - priority: "high" - }); - } - - if (result.summary.stage === "doctor" && !hasArtifact(result.artifacts, "Codebase Map")) { - actions.push({ - label: "Continue With Discovery", - command: `project-brain map-codebase . ${output}`, - rationale: "Doctor is complete, but structural discovery has not been generated yet.", - priority: "high" - }); - } - - if ((result.summary.stage === "map-codebase" || result.summary.stage === "ask") && !hasArtifact(result.artifacts, "Swarm")) { - if (!hasArtifact(result.artifacts, "Repository Fact Graph")) { - actions.push({ - label: "Continue With Fact Graph", - command: `project-brain code-graph . ${output}`, - rationale: "Structural facts are available from deterministic analysis and should be captured before a bounded swarm run.", - priority: "high" - }); - } - - actions.push({ - label: "Continue With Swarm", - command: `project-brain self-improve . ${output}`, - rationale: "The repo already has discovery context, so the next useful step is a bounded delegated analysis.", - priority: "high" - }); - } - - if (result.summary.stage === "fact-query" && !hasArtifact(result.artifacts, "Runbook")) { - actions.push({ - label: "Continue With Runbook", - command: `project-brain runbook "optimize analysis and cost" . ${output}`, - rationale: "Filtered memory exists; the next useful step is an ordered low-cost execution plan.", - priority: "high" - }); - } - - if (result.summary.stage === "runbook" && !hasArtifact(result.artifacts, "Harness Audit")) { - actions.push({ - label: "Continue With Harness Audit", - command: `project-brain harness-audit . ${output}`, - rationale: "A runbook exists; audit memory and cost gates before model-heavy work.", - priority: "high" - }); - } - - if (result.summary.stage === "harness-audit" && !hasArtifact(result.artifacts, "Firewall")) { - actions.push({ - label: "Continue With Firewall", - command: `project-brain firewall . --trigger repository-change ${output}`, - rationale: "Harness readiness exists; inspect governance boundaries before delegated analysis.", - priority: "high" - }); - } - - if (result.summary.stage === "swarm" && !hasArtifact(result.artifacts, "Improvement Plan")) { - actions.push({ - label: "Continue With Improvement Plan", - command: `project-brain plan-improvements . ${output}`, - rationale: "A swarm run already exists, so the next step is to convert findings into a persistent roadmap.", - priority: "high" - }); - } - - if (result.summary.stage === "plan-improvements" && !hasArtifact(result.artifacts, "Impact Radius")) { - actions.push({ - label: "Review Latest Changes", - command: `project-brain review-delta . ${output}`, - rationale: "A plan exists already; the next useful checkpoint is a bounded review of recent changes.", - priority: "medium" - }); - } - - return uniqueSuggestions([...actions, ...statusSuggestions]); -} diff --git a/core/resume/index.ts b/core/resume/index.ts deleted file mode 100644 index 91446cd..0000000 --- a/core/resume/index.ts +++ /dev/null @@ -1,346 +0,0 @@ -import path from "node:path"; - -import { buildStatus } from "../status"; -import { deriveResumeSuggestions } from "../reaction_engine"; -import { workflowForArtifactLabel } from "../workflow_registry"; -import { writeExecutiveSummaryArtifacts } from "../../memory/executive_summary"; -import { readJsonSafe, readTextSafe, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ProjectContext, ResumeResult, ResumeStage, StatusArtifactSummary, StatusResult } from "../../shared/types"; - -interface ResumeDeps { - buildStatus?: (context: ProjectContext) => Promise; -} - -function artifactPriority(label: string): number { - return workflowForArtifactLabel(label)?.resumePriority ?? 0; -} - -function latestArtifact(artifacts: StatusArtifactSummary[]): StatusArtifactSummary | undefined { - return artifacts - .filter((artifact) => artifact.exists && artifact.updatedAt) - .sort((left, right) => { - const priorityDelta = artifactPriority(right.label) - artifactPriority(left.label); - if (priorityDelta !== 0) { - return priorityDelta; - } - - return Date.parse(right.updatedAt ?? "") - Date.parse(left.updatedAt ?? ""); - })[0]; -} - -function stageFromArtifactLabel(label: string | undefined): ResumeStage { - const stage = label ? workflowForArtifactLabel(label)?.resumeStage : undefined; - return stage ?? "bootstrap"; -} - -function firstUsefulLines(input: string, limit = 2): string[] { - return input - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0 && !/^#/.test(line)) - .slice(0, limit); -} - -async function buildStageNotes( - context: ProjectContext, - stage: ResumeStage, - latest: StatusArtifactSummary | undefined, - status: StatusResult -): Promise { - const notes: string[] = []; - - if (!latest) { - notes.push("No resumable project-brain artifacts were found in this output path yet."); - if (status.git.isGitRepo) { - notes.push("The target is a git repository, so doctor or map-codebase are the best bootstrap steps."); - } - return notes; - } - - notes.push(`Latest artifact: ${latest.label}${latest.updatedAt ? ` at ${latest.updatedAt}` : ""}.`); - - if (status.summary.doctorStatus !== "unknown") { - notes.push(`Doctor status: ${status.summary.doctorStatus}.`); - } - - if (stage === "doctor") { - const doctor = await readJsonSafe<{ summary?: { headline?: string } }>(path.join(context.memoryDir, "doctor", "doctor.json")); - if (doctor?.summary?.headline) { - notes.push(doctor.summary.headline); - } - } - - if (stage === "start") { - const start = await readJsonSafe<{ headline?: string; nextCommand?: string }>(path.join(context.memoryDir, "start", "start.json")); - if (start?.headline) { - notes.push(start.headline); - } - if (start?.nextCommand) { - notes.push(`Start next command: ${start.nextCommand}`); - } - } - - if (stage === "swarm") { - const swarm = await readJsonSafe<{ - synthesis?: { headline?: string; summary?: string }; - resilience?: { runTimedOut?: boolean; timedOutTasks?: number }; - }>(path.join(context.memoryDir, "swarm", "swarm_run.json")); - if (swarm?.synthesis?.headline) { - notes.push(swarm.synthesis.headline); - } - if (swarm?.resilience?.runTimedOut) { - notes.push(`The last swarm run exhausted its global time budget${swarm.resilience.timedOutTasks ? ` with ${swarm.resilience.timedOutTasks} timed-out tasks` : ""}.`); - } else if (swarm?.synthesis?.summary) { - notes.push(swarm.synthesis.summary); - } - } - - if (stage === "plan-improvements") { - const lines = firstUsefulLines(await readTextSafe(path.join(context.docsDir, "improvement_plan", "SUMMARY.md"))); - if (lines.length > 0) { - notes.push(...lines); - } else { - notes.push("An improvement plan is already present for this output path."); - } - } - - if (stage === "map-codebase") { - const codebaseMap = status.artifacts.find((artifact) => artifact.label === "Codebase Map" && artifact.exists); - const lines = codebaseMap ? firstUsefulLines(await readTextSafe(path.join(context.docsDir, "codebase_map", "SUMMARY.md"))) : []; - if (lines.length > 0) { - notes.push(...lines); - } else if (codebaseMap) { - notes.push("A codebase map is already present for this output path."); - } else { - notes.push("A codebase map is still missing for this output path."); - } - - const factGraph = status.artifacts.find((artifact) => artifact.label === "Repository Fact Graph" && artifact.exists); - if (factGraph) { - notes.push("A repository fact graph is available and should be reused before running broad model analysis."); - } - - const memoryBrief = status.artifacts.find((artifact) => artifact.label === "Memory Brief" && artifact.exists); - if (memoryBrief) { - notes.push("A compact memory brief is available for agents and future model handoffs."); - } - } - - if (stage === "fact-query") { - const lines = firstUsefulLines(await readTextSafe(path.join(context.reportsDir, "fact_query.md")), 3); - if (lines.length > 0) { - notes.push(...lines); - } - notes.push("A factual memory query is available; use it before broad swarm analysis."); - } - - if (stage === "runbook") { - const lines = firstUsefulLines(await readTextSafe(path.join(context.reportsDir, "runbook.md")), 3); - if (lines.length > 0) { - notes.push(...lines); - } - notes.push("A token-aware runbook exists; continue with its first non-done step."); - } - - if (stage === "harness-audit") { - const audit = await readJsonSafe<{ score?: number; tokenRisk?: string; suggestedCommands?: string[] }>( - path.join(context.memoryDir, "harness_audit", "harness_audit.json") - ); - if (audit) { - notes.push(`Harness audit: score=${audit.score ?? "unknown"}, tokenRisk=${audit.tokenRisk ?? "unknown"}.`); - if (audit.suggestedCommands && audit.suggestedCommands.length > 0) { - notes.push(`Harness next command: ${audit.suggestedCommands[0]}`); - } - } else { - notes.push("A harness audit exists; use it to confirm memory, cost gates, and continuity before model-heavy work."); - } - } - - if (stage === "ask") { - const lines = firstUsefulLines(await readTextSafe(path.join(context.reportsDir, "ask_brief.md"))); - if (lines.length > 0) { - notes.push(...lines); - } else { - notes.push("An ask brief already exists for this output path."); - } - } - - if (stage === "firewall") { - notes.push("An agent firewall snapshot exists and can be reused as the current safety baseline."); - } - - if (stage === "review-delta") { - notes.push("A recent impact review already exists for this output path."); - } - - const missingArtifacts = status.artifacts.filter((artifact) => !artifact.exists).map((artifact) => artifact.label); - if (missingArtifacts.length > 0) { - const preview = missingArtifacts.slice(0, 4); - notes.push( - missingArtifacts.length > preview.length - ? `Missing artifacts: ${preview.join(", ")}, plus more.` - : `Missing artifacts: ${preview.join(", ")}.` - ); - } - - return notes; -} - -function buildResumeHeadline(stage: ResumeStage, latest: StatusArtifactSummary | undefined, notes: string[]): string { - if (!latest) { - return "No resumable project-brain artifacts were found in this output path."; - } - - if (stage === "swarm" && notes.length > 1) { - return `Resume from Swarm: ${notes[1]}`; - } - - if (stage === "start") { - return "Resume from Start: the guided path already prepared the project context."; - } - - if (stage === "plan-improvements") { - return "Resume from Improvement Plan: a persistent roadmap already exists for this output path."; - } - - if (stage === "map-codebase") { - return "Resume from Codebase Map: structural discovery is already in place."; - } - - if (stage === "fact-query") { - return "Resume from Fact Query: filtered factual memory is ready for the next analysis step."; - } - - if (stage === "runbook") { - return "Resume from Runbook: a token-aware execution path is ready."; - } - - if (stage === "harness-audit") { - return "Resume from Harness Audit: memory and cost readiness have been checked."; - } - - if (stage === "doctor") { - return "Resume from Doctor: environment checks are complete and the repo is ready for deeper analysis."; - } - - return `Resume from ${latest.label}: project-brain found a persisted checkpoint to continue from.`; -} - -function renderArtifacts(artifacts: StatusArtifactSummary[]): string { - return artifacts - .map((artifact) => { - const updated = artifact.updatedAt ? ` | updated=${artifact.updatedAt}` : ""; - return `- ${artifact.label}: ${artifact.exists ? "present" : "missing"} | ${artifact.path}${updated}`; - }) - .join("\n"); -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderSuggestions(result: ResumeResult): string { - return result.suggestions.length > 0 - ? result.suggestions - .map( - (suggestion) => `### ${suggestion.label} - -- Priority: ${suggestion.priority.toUpperCase()} -- Command: \`${suggestion.command}\` -- Rationale: ${suggestion.rationale}` - ) - .join("\n\n") - : "No immediate follow-up actions suggested."; -} - -function renderResumeReport(result: ResumeResult): string { - return `# Resume - -## Summary - -- Repository: ${result.context.repoName} -- Target: ${result.context.targetPath} -- Output: ${result.context.outputPath} -- Git repo: ${result.git.isGitRepo ? "yes" : "no"} -- Branch: ${result.git.branch ?? "unknown"} -- Stage: ${result.summary.stage} -- Memory readiness: ${result.memoryReadiness.status} (${result.memoryReadiness.reason}) -- Executive summary: ${result.executiveSummary.reportPath} -- Artifact count: ${result.summary.artifactCount} -- Latest artifact: ${result.summary.latestArtifactLabel ?? "none"}${result.summary.latestArtifactUpdatedAt ? ` (${result.summary.latestArtifactUpdatedAt})` : ""} -- Headline: ${result.summary.headline} - -## Notes - -${renderList(result.notes)} - -## Artifacts - -${renderArtifacts(result.artifacts)} - -## Suggested Actions - -${renderSuggestions(result)} -`; -} - -export async function buildResume(context: ProjectContext, deps: ResumeDeps = {}): Promise { - const status = deps.buildStatus ? await deps.buildStatus(context) : await buildStatus(context); - const executiveSummary = status.executiveSummary ?? await writeExecutiveSummaryArtifacts(context); - const latest = latestArtifact(status.artifacts); - const stage = stageFromArtifactLabel(latest?.label); - const notes = await buildStageNotes(context, stage, latest, status); - const summary: ResumeResult["summary"] = { - headline: buildResumeHeadline(stage, latest, notes), - stage, - artifactCount: status.summary.artifactCount, - latestArtifactLabel: latest?.label, - latestArtifactUpdatedAt: latest?.updatedAt - }; - const suggestions = deriveResumeSuggestions( - { - context, - summary, - artifacts: status.artifacts - }, - status.suggestions - ); - - const reportPath = path.join(context.reportsDir, "resume.md"); - const memoryPath = path.join(context.memoryDir, "resume", "resume.json"); - - const result: ResumeResult = { - context, - reportPath, - memoryPath, - git: status.git, - summary, - latestArtifact: latest, - memoryReadiness: status.memoryReadiness, - executiveSummary, - artifacts: status.artifacts, - notes, - suggestions - }; - - await writeFileEnsured(reportPath, renderResumeReport(result)); - await writeJsonEnsured(memoryPath, { - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - git: result.git, - summary, - latestArtifact: latest, - memoryReadiness: status.memoryReadiness, - executiveSummary: { - reportPath: executiveSummary.reportPath, - memoryPath: executiveSummary.memoryPath, - status: executiveSummary.status - }, - artifacts: result.artifacts, - notes, - suggestions - }); - - return result; -} diff --git a/core/scheduler/index.ts b/core/scheduler/index.ts deleted file mode 100644 index b12256d..0000000 --- a/core/scheduler/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class WeeklyScheduler { - describeWindow(referenceDate = new Date()): { label: string; nextRun: string } { - const end = new Date(referenceDate); - const start = new Date(referenceDate); - start.setDate(start.getDate() - 7); - - const label = `${start.toISOString().slice(0, 10)} -> ${end.toISOString().slice(0, 10)}`; - const nextRun = new Date(end.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(); - - return { label, nextRun }; - } -} diff --git a/core/security_audit/index.ts b/core/security_audit/index.ts deleted file mode 100644 index 968047c..0000000 --- a/core/security_audit/index.ts +++ /dev/null @@ -1,473 +0,0 @@ -import path from "node:path"; - -import { uniqueSorted, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; - -import type { - AgentReport, - ContextLiteResult, - GovernanceSummary, - ProjectContext, - SecurityAuditArea, - SecurityAuditResult, - SecurityCoverageStatus, - SecurityFinding, - SecurityFindingSeverity, - VerifiedAppContext -} from "../../shared/types"; - -const REQUIRED_AREAS: SecurityAuditArea[] = [ - "auth_sessions", - "authorization", - "input_validation", - "web_attacks", - "http_headers", - "infra_config", - "abuse_protection", - "sensitive_data", - "observability" -]; - -const AREA_LABELS: Record = { - auth_sessions: "Autenticación y sesiones", - authorization: "Autorización", - input_validation: "Validación de inputs", - web_attacks: "Protección contra ataques web", - http_headers: "Headers HTTP", - infra_config: "Configuración e infraestructura", - abuse_protection: "Protección contra abuso", - sensitive_data: "Datos sensibles", - observability: "Observabilidad y trazabilidad" -}; - -const SEVERITY_ORDER: Record = { - critical: 5, - high: 4, - medium: 3, - low: 2, - info: 1 -}; - -const SEVERITY_LABELS: Record = { - critical: "🔴 CRITICAL", - high: "🟠 HIGH", - medium: "🟡 MEDIUM", - low: "🟢 LOW", - info: "🔵 INFO" -}; - -function flattenDependencies(context: ProjectContext): string[] { - return uniqueSorted( - context.discovery.dependencies.flatMap((manifest) => manifest.dependencies.map((dependency) => dependency.toLowerCase())) - ); -} - -function pickMany(files: string[], pattern: RegExp, limit: number): string[] { - return files.filter((filePath) => pattern.test(filePath)).slice(0, limit); -} - -function detectDataSignals(context: ProjectContext, flatDependencies: string[]): string[] { - const signals: string[] = []; - if (flatDependencies.some((dependency) => dependency.includes("prisma"))) { - signals.push("Prisma"); - } - if (flatDependencies.some((dependency) => dependency.includes("postgres"))) { - signals.push("PostgreSQL"); - } - if (flatDependencies.some((dependency) => dependency.includes("mysql"))) { - signals.push("MySQL"); - } - if (flatDependencies.some((dependency) => dependency.includes("mongo"))) { - signals.push("MongoDB"); - } - if (context.discovery.files.some((filePath) => /schema\.prisma$/i.test(filePath))) { - signals.push("Prisma schema"); - } - if (context.discovery.files.some((filePath) => /migration/i.test(filePath))) { - signals.push("Migraciones versionadas"); - } - return uniqueSorted(signals); -} - -function detectStorageSignals(context: ProjectContext, flatDependencies: string[]): string[] { - const signals: string[] = []; - if (flatDependencies.some((dependency) => /minio|s3|@aws-sdk|storage/i.test(dependency))) { - signals.push("Object storage SDK"); - } - const fileMatches = pickMany(context.discovery.files, /(storage|bucket|uploads?|media|assets|blob)/i, 8); - if (fileMatches.length > 0) { - signals.push(...fileMatches); - } - return uniqueSorted(signals); -} - -function detectAuthSignals(context: ProjectContext, flatDependencies: string[]): string[] { - const signals: string[] = []; - if (flatDependencies.some((dependency) => /(next-auth|auth0|clerk|lucia|jsonwebtoken|passport|express-session)/i.test(dependency))) { - signals.push("Dependencias de auth/sesión"); - } - signals.push(...pickMany(context.discovery.files, /(^|\/)(auth|session|permissions?|roles?|access|acl|rbac)/i, 8)); - return uniqueSorted(signals); -} - -function detectEndpointSurfaces(context: ProjectContext): string[] { - return uniqueSorted( - [ - ...pickMany(context.discovery.files, /(^|\/)(src\/)?app\/api\/.+\/route\.(ts|tsx|js|jsx)$/i, 12), - ...pickMany(context.discovery.files, /(^|\/)(src\/)?pages\/api\/.+\.(ts|tsx|js|jsx)$/i, 8), - ...pickMany(context.discovery.files, /(^|\/)(routes|controllers|api)\//i, 8) - ].slice(0, 16) - ); -} - -function detectAttackSurface(context: ProjectContext): { - publicEndpoints: string[]; - forms: string[]; - uploads: string[]; - privateDashboards: string[]; - adminPanels: string[]; - webhooks: string[]; - integrations: string[]; - storage: string[]; -} { - return { - publicEndpoints: detectEndpointSurfaces(context), - forms: pickMany(context.discovery.files, /(login|signup|register|form|checkout|profile)/i, 8), - uploads: pickMany(context.discovery.files, /(upload|avatar|media|file|blob)/i, 8), - privateDashboards: pickMany(context.discovery.files, /(dashboard|vendor|backoffice|private)/i, 8), - adminPanels: pickMany(context.discovery.files, /(^|\/)(admin|administrator|moderation)/i, 8), - webhooks: pickMany(context.discovery.files, /(webhook|hooks)/i, 8), - integrations: pickMany(context.discovery.files, /(integrations?|clients?|adapters?|stripe|slack|twilio|sendgrid|s3|aws)/i, 8), - storage: pickMany(context.discovery.files, /(storage|bucket|uploads?|media|assets|blob)/i, 8) - }; -} - -function buildVerifiedContext( - context: ProjectContext, - contextLite: ContextLiteResult, - scopeNote?: string -): VerifiedAppContext { - const flatDependencies = flattenDependencies(context); - const dataSignals = detectDataSignals(context, flatDependencies); - const authSignals = detectAuthSignals(context, flatDependencies); - const storageSignals = detectStorageSignals(context, flatDependencies); - const attackSurface = detectAttackSurface(context); - - const architectureSummary = [ - `Framework real en runtime: ${context.discovery.frameworks.join(", ") || "Pendiente de confirmar"}.`, - `Frontend/backend servidos por: ${context.discovery.apis.join(", ") || "sin API confirmada"}; infraestructura detectada=${context.discovery.infrastructure.join(", ") || "sin infraestructura explícita"}.`, - `Base de datos o capa de datos: ${dataSignals.join(", ") || "No confirmada"}.`, - `Storage: ${storageSignals.join(", ") || "No confirmado"}.`, - `Manejo de sesiones o auth: ${authSignals.join(", ") || "No confirmado"}.`, - `Exposición operativa observada: CI=${context.discovery.ci.providers.join(", ") || "sin CI confirmada"}, logging=${context.discovery.logging.frameworks.join(", ") || "sin logging dedicado"}, métricas=${context.discovery.metrics.tools.join(", ") || "sin métricas confirmadas"}.`, - ...(scopeNote ? [scopeNote] : []) - ]; - - const criticalAssets = uniqueSorted([ - authSignals.length > 0 ? "Sesiones, identidad autenticada y decisiones de autorización" : "", - dataSignals.length > 0 ? `Datos persistidos y modelos de negocio (${dataSignals.join(", ")})` : "", - storageSignals.length > 0 ? `Archivos y blobs potencialmente sensibles (${storageSignals.join(", ")})` : "", - context.discovery.files.some((filePath) => /(^|\/)\.env($|[^/])|\.pem$|\.key$/i.test(filePath)) ? "Secretos, llaves o configuración sensible en archivos del repo" : "", - attackSurface.privateDashboards.length > 0 ? "Dashboards privados y privilegios de operador/admin" : "", - context.discovery.logging.frameworks.length > 0 || context.discovery.metrics.tools.length > 0 ? "Logs, trazas y telemetría operacional" : "" - ].filter(Boolean)); - - const trustBoundaries = uniqueSorted([ - attackSurface.publicEndpoints.length > 0 ? `Código servidor expuesto en rutas/endpoints: ${attackSurface.publicEndpoints.slice(0, 6).join(", ")}` : "", - attackSurface.privateDashboards.length > 0 ? `Código cliente en dashboards privados: ${attackSurface.privateDashboards.slice(0, 6).join(", ")}` : "", - authSignals.length > 0 ? `Decisiones de auth/authz aparentes en: ${authSignals.slice(0, 6).join(", ")}` : "", - attackSurface.forms.length > 0 ? `Entradas no confiables visibles en formularios o páginas: ${attackSurface.forms.slice(0, 6).join(", ")}` : "", - attackSurface.integrations.length > 0 ? `Integraciones y conectores externos: ${attackSurface.integrations.slice(0, 6).join(", ")}` : "" - ].filter(Boolean)); - - return { - architectureSummary, - attackSurface: uniqueSorted([ - attackSurface.publicEndpoints.length > 0 ? `Endpoints públicos o handlers: ${attackSurface.publicEndpoints.join(", ")}` : "", - attackSurface.forms.length > 0 ? `Formularios o superficies de entrada: ${attackSurface.forms.join(", ")}` : "", - attackSurface.uploads.length > 0 ? `Uploads o manejo de archivos: ${attackSurface.uploads.join(", ")}` : "", - attackSurface.privateDashboards.length > 0 ? `Dashboards privados: ${attackSurface.privateDashboards.join(", ")}` : "", - attackSurface.adminPanels.length > 0 ? `Admin panels: ${attackSurface.adminPanels.join(", ")}` : "", - attackSurface.webhooks.length > 0 ? `Webhooks: ${attackSurface.webhooks.join(", ")}` : "", - attackSurface.integrations.length > 0 ? `Integraciones externas: ${attackSurface.integrations.join(", ")}` : "", - attackSurface.storage.length > 0 ? `Buckets/storage accesible o referido: ${attackSurface.storage.join(", ")}` : "" - ].filter(Boolean)), - criticalAssets, - trustBoundaries, - contextGaps: uniqueSorted(contextLite.openQuestions).slice(0, 12) - }; -} - -function mergeCoverage(agentReports: AgentReport[]): SecurityCoverageStatus[] { - const byArea = new Map(); - for (const report of agentReports) { - for (const entry of report.coverage ?? []) { - byArea.set(entry.area, [...(byArea.get(entry.area) ?? []), entry]); - } - } - - return REQUIRED_AREAS.map((area) => { - const entries = byArea.get(area) ?? []; - if (entries.some((entry) => entry.status === "finding")) { - return { - area, - status: "finding" as const, - note: uniqueSorted(entries.map((entry) => entry.note)).join(" | "), - agentId: entries.find((entry) => entry.status === "finding")?.agentId - }; - } - if (entries.some((entry) => entry.status === "ok")) { - return { - area, - status: "ok" as const, - note: uniqueSorted(entries.map((entry) => entry.note)).join(" | "), - agentId: entries.find((entry) => entry.status === "ok")?.agentId - }; - } - return { - area, - status: "not-reviewed" as const, - note: `No revisado — se requiere acceso adicional o detectores específicos para ${AREA_LABELS[area].toLowerCase()}.` - }; - }); -} - -function sortFindings(findings: SecurityFinding[]): SecurityFinding[] { - return [...findings].sort((left, right) => { - const severityDiff = SEVERITY_ORDER[right.severity] - SEVERITY_ORDER[left.severity]; - if (severityDiff !== 0) { - return severityDiff; - } - return left.title.localeCompare(right.title); - }); -} - -function deriveVerdict(findings: SecurityFinding[], coverage: SecurityCoverageStatus[]): SecurityAuditResult["verdict"] { - const criticalCount = findings.filter((finding) => finding.severity === "critical").length; - const highCount = findings.filter((finding) => finding.severity === "high").length; - const notReviewedCount = coverage.filter((entry) => entry.status === "not-reviewed").length; - - if (criticalCount > 0 || highCount >= 2) { - return "No apta para producción"; - } - if (highCount > 0 || findings.some((finding) => finding.severity === "medium") || notReviewedCount >= 3) { - return "Apta con remediaciones obligatorias"; - } - return "Apta con hardening recomendado"; -} - -function summarizeExecutive( - verifiedContext: VerifiedAppContext, - findings: SecurityFinding[], - verdict: SecurityAuditResult["verdict"] -): string[] { - const primarySurface = verifiedContext.attackSurface[0] ?? "Superficie principal no confirmada"; - const highestFinding = findings[0]; - const state = - findings.length === 0 - ? "No se confirmaron hallazgos explotables de alta severidad con la evidencia disponible." - : `Hallazgo más crítico: ${SEVERITY_LABELS[highestFinding.severity]} ${highestFinding.title}.`; - - return [ - `Superficie de ataque principal: ${primarySurface}`, - state, - `Estado general: ${findings.length} hallazgos estructurados y ${verifiedContext.contextGaps.length} huecos de contexto abiertos.`, - `Producción: ${verdict}` - ]; -} - -function renderCoverage(coverage: SecurityCoverageStatus[]): string { - return coverage - .map((entry) => { - if (entry.status === "ok") { - return `- ✅ ${AREA_LABELS[entry.area]}: implementación correcta — ${entry.note}`; - } - if (entry.status === "finding") { - return `- ⚠️ ${AREA_LABELS[entry.area]}: requiere remediación — ${entry.note}`; - } - return `- No revisado — se requiere acceso a ${AREA_LABELS[entry.area].toLowerCase()} o detectores más específicos. ${entry.note}`; - }) - .join("\n"); -} - -function renderFinding(finding: SecurityFinding): string { - return `--- -**${SEVERITY_LABELS[finding.severity]} — ${finding.title}** -- **Ubicación**: ${finding.location} -- **Evidencia**: ${finding.evidence} -- **Vector de ataque**: ${finding.attackVector.join(" -> ")} -- **Impacto real**: ${finding.impact} -- **Fix**: ${finding.fix} -- **Referencias**: ${finding.references.join(" / ")} ----`; -} - -function renderPriorityTable(findings: SecurityFinding[]): string { - if (findings.length === 0) { - return "| # | Severidad | Vulnerabilidad | Ubicación | Esfuerzo de fix | Impacto |\n| --- | --- | --- | --- | --- | --- |\n| 1 | INFO | Sin hallazgos estructurados confirmados | N/A | low | Sin impacto explotable confirmado con la evidencia disponible |"; - } - - return [ - "| # | Severidad | Vulnerabilidad | Ubicación | Esfuerzo de fix | Impacto |", - "| --- | --- | --- | --- | --- | --- |", - ...findings.map( - (finding, index) => - `| ${index + 1} | ${SEVERITY_LABELS[finding.severity]} | ${finding.title} | ${finding.location.replace(/\|/g, "\\|")} | ${finding.effort} | ${finding.impact.replace(/\|/g, "\\|")} |` - ) - ].join("\n"); -} - -function deriveChecklist(findings: SecurityFinding[], coverage: SecurityCoverageStatus[]): string[] { - const lowEffort = findings.filter((finding) => finding.effort === "low"); - const mediumEffort = findings.filter((finding) => finding.effort === "medium"); - const highImpact = findings.filter((finding) => finding.severity === "critical" || finding.severity === "high"); - const unresolvedCoverage = coverage.filter((entry) => entry.status === "not-reviewed"); - - return uniqueSorted([ - ...lowEffort.map((finding) => `Fix rápido: ${finding.title} en ${finding.location}.`), - ...highImpact.map((finding) => `Impacto alto: remediar ${finding.title} (${finding.location}) antes de exponer la app a tráfico real.`), - ...mediumEffort.map((finding) => `Deuda técnica inmediata: aplicar el fix propuesto para ${finding.title}.`), - ...unresolvedCoverage.map((entry) => `Cerrar hueco de revisión en ${AREA_LABELS[entry.area].toLowerCase()}.`) - ]).slice(0, 14); -} - -function deriveSecurityDebt( - coverage: SecurityCoverageStatus[], - verifiedContext: VerifiedAppContext -): string[] { - return uniqueSorted([ - ...coverage - .filter((entry) => entry.status === "not-reviewed") - .map((entry) => `Completar revisiones estructuradas y detectores dedicados para ${AREA_LABELS[entry.area].toLowerCase()}.`), - verifiedContext.trustBoundaries.length === 0 - ? "Separar explícitamente decisiones de seguridad de frontend y backend para reducir controles implícitos." - : "", - verifiedContext.contextGaps.length > 0 - ? "Resolver huecos de contexto canónico antes de declarar la superficie como lista para producción." - : "", - "Formalizar una política de hardening, logging de seguridad y ownership por capa (auth, infra, storage, observability)." - ].filter(Boolean)).slice(0, 12); -} - -function countBySeverity(findings: SecurityFinding[]): Record { - return findings.reduce>( - (accumulator, finding) => { - accumulator[finding.severity] += 1; - return accumulator; - }, - { critical: 0, high: 0, medium: 0, low: 0, info: 0 } - ); -} - -function buildHeadline(findings: SecurityFinding[], verdict: SecurityAuditResult["verdict"]): string { - const counts = countBySeverity(findings); - return `Security audit completo: critical=${counts.critical}, high=${counts.high}, medium=${counts.medium}, low=${counts.low}, info=${counts.info}; verdict=${verdict}.`; -} - -function renderReport( - result: SecurityAuditResult, - executiveSummary: string[] -): string { - return `# Security Audit - -### 1. Resumen ejecutivo -${executiveSummary.map((line) => `- ${line}`).join("\n")} - -### 2. Contexto verificado de la app -- arquitectura real: ${result.verifiedContext.architectureSummary.join(" | ")} -- auth real: ${result.verifiedContext.architectureSummary.find((line) => line.startsWith("Manejo de sesiones")) ?? "No confirmado"} -- storage real: ${result.verifiedContext.architectureSummary.find((line) => line.startsWith("Storage")) ?? "No confirmado"} -- exposición real: ${result.verifiedContext.attackSurface.join(" | ") || "No confirmada"} -- módulos no revisados: ${result.verifiedContext.contextGaps.join(" | ") || "Ninguno"} - -**Resumen de arquitectura verificada** -${result.verifiedContext.architectureSummary.map((line) => `- ${line}`).join("\n")} - -**Superficie de ataque identificada** -${result.verifiedContext.attackSurface.length > 0 ? result.verifiedContext.attackSurface.map((line) => `- ${line}`).join("\n") : "- No se confirmó una superficie expuesta suficiente con la evidencia disponible."} - -**Activos críticos** -${result.verifiedContext.criticalAssets.length > 0 ? result.verifiedContext.criticalAssets.map((line) => `- ${line}`).join("\n") : "- No se confirmaron activos críticos adicionales fuera de los patrones básicos del repositorio."} - -**Módulos no revisados o con contexto insuficiente** -${result.verifiedContext.contextGaps.length > 0 ? result.verifiedContext.contextGaps.map((line) => `- ${line}`).join("\n") : "- Sin módulos marcados como no revisados en esta corrida."} - -### 3. Hallazgos -${result.findings.length > 0 ? result.findings.map(renderFinding).join("\n\n") : "✅ No se confirmaron hallazgos explotables con la evidencia disponible en esta corrida."} - -**Cobertura obligatoria** -${renderCoverage(result.coverage)} - -### 4. Tabla de prioridades -${renderPriorityTable(result.findings)} - -### 5. Checklist de producción -${result.checklist.length > 0 ? result.checklist.map((line) => `- ${line}`).join("\n") : "- No quedan fixes rápidos confirmados; aplicar hardening recomendado y cerrar huecos de contexto."} - -### 6. Deuda de seguridad -${result.securityDebt.length > 0 ? result.securityDebt.map((line) => `- ${line}`).join("\n") : "- Sin deuda estructural adicional confirmada más allá del hardening continuo."} - -### 7. Veredicto final -${result.verdict} -`; -} - -export async function runSecurityAudit( - context: ProjectContext, - governanceRun: { - agentReports: AgentReport[]; - summary: GovernanceSummary; - }, - contextLite: ContextLiteResult, - options: { - trigger: SecurityAuditResult["trigger"]; - scopeNote?: string; - } -): Promise { - const verifiedContext = buildVerifiedContext(context, contextLite, options.scopeNote); - const findings = sortFindings( - governanceRun.agentReports.flatMap((report) => report.securityFindings ?? []) - ); - const coverage = mergeCoverage(governanceRun.agentReports); - const verdict = deriveVerdict(findings, coverage); - const checklist = deriveChecklist(findings, coverage); - const securityDebt = deriveSecurityDebt(coverage, verifiedContext); - const reportPath = path.join(context.reportsDir, "security_audit.md"); - const memoryPath = path.join(context.runtimeMemoryDir, "security", "security_audit.json"); - const sourceReports = governanceRun.agentReports.map((report) => report.outputPath); - const headline = buildHeadline(findings, verdict); - - const result: SecurityAuditResult = { - context, - trigger: options.trigger, - reportPath, - memoryPath, - contextLiteReportPath: contextLite.reportPath, - verifiedContext, - findings, - coverage, - checklist, - securityDebt, - sourceReports, - verdict, - headline - }; - - const executiveSummary = summarizeExecutive(verifiedContext, findings, verdict); - - await writeJsonEnsured(memoryPath, { - trigger: result.trigger, - headline: result.headline, - verdict: result.verdict, - verifiedContext: result.verifiedContext, - findings: result.findings, - coverage: result.coverage, - checklist: result.checklist, - securityDebt: result.securityDebt, - sourceReports: result.sourceReports, - contextLiteReportPath: result.contextLiteReportPath, - firewallReportPath: governanceRun.summary.firewall?.reportPath - }); - await writeFileEnsured(reportPath, renderReport(result, executiveSummary)); - - return result; -} diff --git a/core/status/index.ts b/core/status/index.ts deleted file mode 100644 index af86cb3..0000000 --- a/core/status/index.ts +++ /dev/null @@ -1,284 +0,0 @@ -import path from "node:path"; -import { promises as fs } from "node:fs"; - -import { deriveStatusSuggestions } from "../reaction_engine"; -import { buildWorkflowRuntimeDefinitions } from "../workflow_registry"; -import { writeExecutiveSummaryArtifacts } from "../../memory/executive_summary"; -import { assessMemoryReadiness } from "../../memory/readiness"; -import { fileExists, readJsonSafe, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { DoctorCheckStatus, ProjectContext, StatusArtifactSummary, StatusResult, SuggestedAction } from "../../shared/types"; - -interface StatusDeps { - runCommand?: (command: string, args: string[], options?: { cwd?: string; timeoutMs?: number }) => Promise<{ - ok: boolean; - stdout: string; - stderr: string; - exitCode: number | null; - }>; -} - -async function defaultRunCommand( - command: string, - args: string[], - options: { cwd?: string; timeoutMs?: number } = {} -): Promise<{ ok: boolean; stdout: string; stderr: string; exitCode: number | null }> { - const { spawn } = await import("node:child_process"); - - return new Promise((resolve) => { - const child = spawn(command, args, { - cwd: options.cwd, - stdio: ["ignore", "pipe", "pipe"] - }); - - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - let timedOut = false; - - child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk))); - child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk))); - - const timeout = options.timeoutMs - ? setTimeout(() => { - timedOut = true; - child.kill("SIGTERM"); - }, options.timeoutMs) - : undefined; - - child.on("error", (error) => { - if (timeout) { - clearTimeout(timeout); - } - resolve({ - ok: false, - stdout: "", - stderr: error.message, - exitCode: null - }); - }); - - child.on("close", (exitCode) => { - if (timeout) { - clearTimeout(timeout); - } - resolve({ - ok: !timedOut && exitCode === 0, - stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(), - stderr: timedOut ? "Command timed out." : Buffer.concat(stderrChunks).toString("utf8").trim(), - exitCode - }); - }); - }); -} - -async function artifactSummary(label: string, filePath: string): Promise { - const exists = await fileExists(filePath); - if (!exists) { - return { - label, - path: filePath, - exists - }; - } - - const stat = await fs.stat(filePath); - return { - label, - path: filePath, - exists, - updatedAt: stat.mtime.toISOString() - }; -} - -function renderArtifacts(artifacts: StatusArtifactSummary[]): string { - return artifacts - .map((artifact) => { - const time = artifact.updatedAt ? ` | updated=${artifact.updatedAt}` : ""; - return `- ${artifact.label}: ${artifact.exists ? "present" : "missing"} | ${artifact.path}${time}`; - }) - .join("\n"); -} - -function renderSuggestions(suggestions: SuggestedAction[]): string { - return suggestions.length > 0 - ? suggestions - .map( - (suggestion) => `### ${suggestion.label} - -- Priority: ${suggestion.priority.toUpperCase()} -- Command: \`${suggestion.command}\` -- Rationale: ${suggestion.rationale}` - ) - .join("\n\n") - : "No immediate follow-up actions suggested."; -} - -function doctorStatusFromSummary(doctorSummary: { failed?: number; warnings?: number } | undefined): DoctorCheckStatus | "unknown" { - if (!doctorSummary) { - return "unknown"; - } - - if ((doctorSummary.failed ?? 0) > 0) { - return "fail"; - } - - if ((doctorSummary.warnings ?? 0) > 0) { - return "warn"; - } - - return "pass"; -} - -function buildHeadline(summary: StatusResult["summary"]): string { - const parts = [ - `doctor=${summary.doctorStatus}`, - `swarm=${summary.swarmStatus}`, - `plan=${summary.planStatus}`, - `artifacts=${summary.artifactCount}` - ]; - - return `Status snapshot: ${parts.join(", ")}`; -} - -function renderMemoryReadiness(result: StatusResult["memoryReadiness"]): string { - return `- Status: ${result.status} -- Reason: ${result.reason} -- Facts: ${result.factsCount} -- Evidence refs: ${result.evidenceCount} -- Token guidance items: ${result.tokenGuidanceCount} -- Generated: ${result.generatedAt ?? "unknown"} -- Age hours: ${result.ageHours ?? "unknown"} -- Max age hours: ${result.maxAgeHours} -- Markdown: ${result.memoryBriefPath} -- JSON: ${result.memoryBriefJsonPath}`; -} - -function renderExecutiveSummaryStatus(result: StatusResult["executiveSummary"]): string { - return `- Markdown: ${result.reportPath} -- JSON: ${result.memoryPath} -- Scopes: ${result.status.scopeCount} -- Fresh complete scopes: ${result.status.completeFreshScopes} -- Stale scopes: ${result.status.staleScopes} -- Partial scopes: ${result.status.partialScopes} -- Latest swarm intent: ${result.status.latestSwarmIntent ?? "None"}`; -} - -function renderStatusReport( - context: ProjectContext, - result: StatusResult -): string { - return `# Status - -## Summary - -- Repository: ${context.repoName} -- Target: ${context.targetPath} -- Output: ${context.outputPath} -- Git repo: ${result.git.isGitRepo ? "yes" : "no"} -- Branch: ${result.git.branch ?? "unknown"} -- Headline: ${result.summary.headline} - -## Artifacts - -${renderArtifacts(result.artifacts)} - -## Memory Readiness - -${renderMemoryReadiness(result.memoryReadiness)} - -## Executive Summary - -${renderExecutiveSummaryStatus(result.executiveSummary)} - -## Suggested Actions - -${renderSuggestions(result.suggestions)} -`; -} - -export async function buildStatus( - context: ProjectContext, - deps: StatusDeps = {} -): Promise { - const runCommand = deps.runCommand ?? defaultRunCommand; - const doctorMemoryPath = path.join(context.memoryDir, "doctor", "doctor.json"); - const workflows = buildWorkflowRuntimeDefinitions(context); - const artifacts = ( - await Promise.all( - workflows.flatMap((workflow) => - workflow.artifactPaths.map((artifactPath, index) => - artifactSummary(workflow.artifactLabels[index] ?? workflow.artifactLabels[0] ?? workflow.humanLabel, artifactPath) - ) - ) - ) - ).filter((artifact, index, all) => all.findIndex((candidate) => candidate.label === artifact.label && candidate.path === artifact.path) === index); - - const doctorMemory = await readJsonSafe<{ summary?: { failed?: number; warnings?: number } }>(doctorMemoryPath); - const doctorStatus = doctorStatusFromSummary(doctorMemory?.summary); - const executiveSummary = await writeExecutiveSummaryArtifacts(context); - const memoryReadiness = await assessMemoryReadiness(context); - - const gitRepo = await runCommand("git", ["-C", context.targetPath, "rev-parse", "--is-inside-work-tree"], { timeoutMs: 5_000 }); - const branch = gitRepo.ok - ? await runCommand("git", ["-C", context.targetPath, "branch", "--show-current"], { timeoutMs: 5_000 }) - : undefined; - - const summary: StatusResult["summary"] = { - headline: "", - artifactCount: artifacts.filter((artifact) => artifact.exists).length, - doctorStatus, - swarmStatus: artifacts.find((artifact) => artifact.label === "Swarm")?.exists ? "available" : "missing", - planStatus: artifacts.find((artifact) => artifact.label === "Improvement Plan")?.exists ? "available" : "missing" - }; - summary.headline = buildHeadline(summary); - const suggestions = deriveStatusSuggestions({ - context, - summary, - artifacts - }); - - const reportPath = path.join(context.reportsDir, "status.md"); - const memoryPath = path.join(context.memoryDir, "status", "status.json"); - await writeFileEnsured(reportPath, renderStatusReport(context, { - context, - reportPath, - memoryPath, - git: { - isGitRepo: gitRepo.ok, - branch: branch?.stdout || undefined - }, - summary, - memoryReadiness, - executiveSummary, - artifacts, - suggestions - })); - await writeJsonEnsured(memoryPath, { - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - git: { - isGitRepo: gitRepo.ok, - branch: branch?.stdout || undefined - }, - summary, - memoryReadiness, - executiveSummary, - artifacts, - suggestions - }); - - return { - context, - reportPath, - memoryPath, - git: { - isGitRepo: gitRepo.ok, - branch: branch?.stdout || undefined - }, - summary, - memoryReadiness, - executiveSummary, - artifacts, - suggestions - }; -} diff --git a/core/swarm_runtime/index.ts b/core/swarm_runtime/index.ts deleted file mode 100644 index 267c646..0000000 --- a/core/swarm_runtime/index.ts +++ /dev/null @@ -1,2232 +0,0 @@ -import { createHash } from "node:crypto"; -import os from "node:os"; -import path from "node:path"; - -import { buildMemoryBriefSummary, buildRepoSummary } from "../../agents/ai-support"; -import { loadScopeMemoryRecords, renderScopeMemoryForPrompt, writeScopeMemoryFromSwarmResult } from "../../memory/scope_store"; -import { appendError, appendLearning } from "../../memory/session_log"; -import { readJsonSafe, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ProjectContext, ScopeMemoryRecord, SwarmPlanTask, SwarmRunResult, SwarmWorkerResult } from "../../shared/types"; -import type { AIRouterRequest, AIRouterTask, ModelProfile, ModelSelection } from "../ai_router/router"; -import { applyPresetPolicy, applyTokenPolicy, type TokenPreset } from "../token_policy"; - -interface SwarmAssistant { - ask(input: AIRouterRequest): Promise; - selectModel(input: AIRouterRequest): Promise; -} - -interface SwarmRuntimeOptions { - parallelism?: number; - chunkSize?: number; - preset?: TokenPreset; - taskTimeoutMs?: number; - maxRetries?: number; - plannerTimeoutMs?: number; - synthesisTimeoutMs?: number; - runTimeoutMs?: number; - maxQueuedTasks?: number; - scopeBias?: SwarmRunResult["chunking"]["scopeBias"]; -} - -interface PlannerPayload { - overview: string; - tasks: SwarmPlanTask[]; -} - -interface WorkerPayload { - summary: string; - findings: string[]; - recommendations: string[]; - verifiedFacts: string[]; - unknowns: string[]; - evidenceRefs: string[]; -} - -interface SynthesisPayload { - headline: string; - summary: string; - priorities: string[]; - next_steps: string[]; - verified_facts: string[]; - unknowns: string[]; - evidence_refs: string[]; -} - -interface ScopeChunk { - chunkId: string; - label: string; - scopePaths: string[]; -} - -interface QueuedSwarmTask { - taskId: string; - parentTaskId: string; - title: string; - goal: string; - profile: SwarmPlanTask["profile"]; - deliverable: string; - chunk: ScopeChunk; - attempt: number; -} - -interface SwarmTaskOutcome { - result?: SwarmWorkerResult; - requeue?: QueuedSwarmTask[]; -} - -interface SwarmDeadline { - startedAtMs: number; - deadlineMs: number; -} - -interface ScopeUnitStat { - entry: string; - directory: boolean; - hidden: boolean; - manifest: boolean; - sourceLike: boolean; - testLike: boolean; - fileCount: number; - sourceFileCount: number; -} - -interface SwarmResponseCacheEntry { - key: string; - request: { - task?: AIRouterTask; - profile?: ModelProfile; - prompt: string; - context?: string; - allowRemote?: boolean; - }; - selection: Pick; - response: string; - createdAt: string; - lastUsedAt: string; - hits: number; -} - -interface SwarmResponseCacheDocument { - version: 1; - updatedAt: string; - entries: Record; -} - -interface SwarmLearningScopeRecord { - signalScore: number; - completedRuns: number; - failureCount: number; - timeoutCount: number; - lastSeenAt: string; -} - -interface SwarmLearningDocument { - version: 1; - updatedAt: string; - scopes: Record; -} - -interface SwarmOptimizationStats { - cacheHits: number; - cacheMisses: number; - cacheWrites: number; - scopeMemoryHits: number; - scopeMemoryMisses: number; - scopeMemoryStale: number; - scopeMemoryWrites: number; - scopeMemoryReuseCandidates: number; - scopeMemoryReductionHints: string[]; - derivedTasksQueued: number; - derivedTasksSkipped: number; - learnedScopeBoosts: string[]; -} - -const SOURCE_FILE_PATTERN = /\.(ts|tsx|js|jsx|py|go|java|rs|cs|rb|php)$/i; -const ROOT_MANIFEST_FILES = new Set([ - "package.json", - "requirements.txt", - "go.mod", - "pom.xml", - "Cargo.toml", - "Gemfile", - "composer.json" -]); -const SOURCE_LIKE_SCOPE_PATTERN = - /^(src|app|apps|server|api|core|lib|packages|services|service|modules|module|features|feature|analysis|agents|cli|governance|planning|memory|shared|tools)$/i; -const SWARM_RESPONSE_CACHE_MAX_ENTRIES = 160; - -type ResourcePressure = SwarmRunResult["parallelism"]["pressure"]; -type ScopeBias = SwarmRunResult["chunking"]["scopeBias"]; - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function clamp(value: number, min: number, max: number): number { - return Math.min(max, Math.max(min, value)); -} - -function extractJsonObject(input: string): Record | undefined { - const trimmed = input.trim(); - const candidate = trimmed.startsWith("```") - ? trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "") - : trimmed; - - try { - const parsed = JSON.parse(candidate) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : undefined; - } catch { - const start = candidate.indexOf("{"); - const end = candidate.lastIndexOf("}"); - if (start < 0 || end <= start) { - return undefined; - } - - try { - const parsed = JSON.parse(candidate.slice(start, end + 1)) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : undefined; - } catch { - return undefined; - } - } -} - -type StructuredSectionKey = - | "body" - | "headline" - | "summary" - | "findings" - | "recommendations" - | "priorities" - | "next_steps" - | "verified_facts" - | "unknowns" - | "evidence_refs"; - -function stripCodeFences(input: string): string { - return input - .trim() - .replace(/^```(?:json|markdown|md|text)?\s*/i, "") - .replace(/\s*```$/, "") - .trim(); -} - -function normalizeSectionKey(rawKey: string): StructuredSectionKey | undefined { - const normalized = rawKey.trim().toLowerCase().replace(/\s+/g, " "); - - if (normalized === "headline") { - return "headline"; - } - if (normalized === "summary" || normalized === "overview") { - return "summary"; - } - if (normalized === "findings" || normalized === "issues" || normalized === "risks" || normalized === "observations") { - return "findings"; - } - if (normalized === "recommendations" || normalized === "actions" || normalized === "action items") { - return "recommendations"; - } - if (normalized === "priorities") { - return "priorities"; - } - if (normalized === "next steps" || normalized === "next_steps" || normalized === "next-step" || normalized === "next step") { - return "next_steps"; - } - if (normalized === "verified facts" || normalized === "verified_facts" || normalized === "facts") { - return "verified_facts"; - } - if (normalized === "unknowns" || normalized === "unknown" || normalized === "not verified" || normalized === "not_verified") { - return "unknowns"; - } - if (normalized === "evidence refs" || normalized === "evidence_refs" || normalized === "evidence" || normalized === "sources") { - return "evidence_refs"; - } - - return undefined; -} - -function parseStructuredSections(input: string): Partial> { - const cleaned = stripCodeFences(input); - const sections: Partial> = { - body: [] - }; - let currentSection: StructuredSectionKey = "body"; - - for (const rawLine of cleaned.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line) { - continue; - } - - const headingMatch = line.match(/^(?:#{1,6}\s*)?([A-Za-z][A-Za-z _-]+?)(?::\s*(.*))?$/); - const sectionKey = headingMatch ? normalizeSectionKey(headingMatch[1] ?? "") : undefined; - if (sectionKey) { - currentSection = sectionKey; - sections[currentSection] ??= []; - const inlineValue = headingMatch?.[2]?.trim(); - if (inlineValue) { - sections[currentSection]!.push(inlineValue); - } - continue; - } - - sections[currentSection] ??= []; - sections[currentSection]!.push(line); - } - - return sections; -} - -function stripListPrefix(value: string): string { - return value.replace(/^[-*+]\s+/, "").replace(/^\d+\.\s+/, "").trim(); -} - -function sectionToList(lines: string[] | undefined): string[] { - if (!lines || lines.length === 0) { - return []; - } - - return lines - .map((line) => stripListPrefix(line)) - .filter((line) => line.length > 0); -} - -function sectionToText(lines: string[] | undefined): string { - if (!lines || lines.length === 0) { - return ""; - } - - return lines - .map((line) => stripListPrefix(line)) - .filter((line) => line.length > 0) - .join(" ") - .trim(); -} - -function looksLikeCodeOnlyResponse(input: string): boolean { - const cleaned = stripCodeFences(input); - const lines = cleaned - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean); - if (lines.length < 3) { - return false; - } - - const hasStructuredSections = lines.some((line) => { - const headingMatch = line.match(/^(?:#{1,6}\s*)?([A-Za-z][A-Za-z _-]+?)(?::\s*(.*))?$/); - return Boolean(headingMatch && normalizeSectionKey(headingMatch[1] ?? "")); - }); - if (hasStructuredSections) { - return false; - } - - const codeSignalCount = lines.filter((line) => - /^(?:#!|import\s+|from\s+\S+\s+import\s+|def\s+|class\s+|for\s+|while\s+|if\s+__name__|print\(|const\s+|let\s+|var\s+|function\s+|export\s+|package\s+main|use\s+|fn\s+)/.test(line) || - /[{};]$/.test(line) - ).length; - - return codeSignalCount >= Math.max(3, Math.ceil(lines.length * 0.4)); -} - -function normalizeStringList(value: unknown): string[] { - return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) : []; -} - -function normalizeProfile(value: unknown): SwarmPlanTask["profile"] | undefined { - if (typeof value !== "string") { - return undefined; - } - - const normalized = value.trim().toLowerCase(); - if (normalized === "worker" || normalized === "reviewer" || normalized === "reasoning" || normalized === "planner" || normalized === "synthesizer") { - return normalized; - } - - return undefined; -} - -function normalizeTaskDependencies(tasks: SwarmPlanTask[]): SwarmPlanTask[] { - const validTaskIds = new Set(tasks.map((task) => task.taskId)); - - return tasks.map((task) => { - const dependsOn = uniqueStrings((task.dependsOn ?? []).filter((dependencyId) => dependencyId !== task.taskId && validTaskIds.has(dependencyId))); - return dependsOn.length > 0 - ? { - ...task, - dependsOn - } - : { - ...task, - dependsOn: undefined - }; - }); -} - -function buildFallbackPlan(intent: string): PlannerPayload { - return { - overview: `This swarm run breaks the request into bounded repository scanning, risk review, and implementation reasoning for: ${intent}`, - tasks: normalizeTaskDependencies([ - { - taskId: "scan-scope", - title: "Scan project scope", - goal: "Identify the main stack, repo shape, and obvious hotspots tied to the request.", - profile: "worker", - deliverable: "Short scan of relevant modules and project characteristics." - }, - { - taskId: "review-risks", - title: "Review critical risks", - goal: "Surface concrete technical, security, or process risks related to the request.", - profile: "reviewer", - deliverable: "Findings and improvement recommendations.", - dependsOn: ["scan-scope"] - }, - { - taskId: "reason-next-steps", - title: "Reason about next steps", - goal: "Turn the scan and risk review into practical next steps and tradeoffs.", - profile: "reasoning", - deliverable: "Decision-oriented next-step guidance.", - dependsOn: ["scan-scope", "review-risks"] - } - ]) - }; -} - -function normalizePlannerPayload(raw: string, intent: string): PlannerPayload { - const parsed = extractJsonObject(raw); - if (!parsed) { - return buildFallbackPlan(intent); - } - - const tasks = Array.isArray(parsed.tasks) - ? parsed.tasks - .map((task, index): SwarmPlanTask | undefined => { - if (!task || typeof task !== "object" || Array.isArray(task)) { - return undefined; - } - - const record = task as Record; - const title = typeof record.title === "string" ? record.title.trim() : ""; - const goal = typeof record.goal === "string" ? record.goal.trim() : ""; - const deliverable = typeof record.deliverable === "string" ? record.deliverable.trim() : ""; - const profile = normalizeProfile(record.profile) ?? (index === 0 ? "worker" : index === 1 ? "reviewer" : "reasoning"); - const dependsOn = normalizeStringList(record.dependsOn ?? record.depends_on); - - if (!title || !goal || !deliverable) { - return undefined; - } - - return { - taskId: typeof record.taskId === "string" && record.taskId.trim().length > 0 ? record.taskId.trim() : `task-${index + 1}`, - title, - goal, - profile, - deliverable, - dependsOn - }; - }) - .filter((task): task is SwarmPlanTask => Boolean(task)) - .slice(0, 4) - : []; - - if (tasks.length === 0) { - return buildFallbackPlan(intent); - } - - return { - overview: - typeof parsed.overview === "string" && parsed.overview.trim().length > 0 - ? parsed.overview.trim() - : buildFallbackPlan(intent).overview, - tasks: normalizeTaskDependencies(tasks) - }; -} - -function normalizeWorkerPayload(raw: string, task: SwarmPlanTask): WorkerPayload { - const parsed = extractJsonObject(raw); - if (parsed) { - return { - summary: - typeof parsed.summary === "string" && parsed.summary.trim().length > 0 - ? parsed.summary.trim() - : `The ${task.title} worker finished without a summary.`, - findings: normalizeStringList(parsed.findings), - recommendations: normalizeStringList(parsed.recommendations), - verifiedFacts: normalizeStringList(parsed.verified_facts ?? parsed.verifiedFacts), - unknowns: normalizeStringList(parsed.unknowns), - evidenceRefs: normalizeStringList(parsed.evidence_refs ?? parsed.evidenceRefs) - }; - } - - const sections = parseStructuredSections(raw); - const findings = sectionToList(sections.findings); - const recommendations = sectionToList((sections.recommendations?.length ?? 0) > 0 ? sections.recommendations : sections.next_steps); - const summary = sectionToText(sections.summary) || sectionToText(sections.body); - - if ( - summary && - findings.length === 0 && - recommendations.length === 0 && - sectionToList(sections.verified_facts).length === 0 && - sectionToList(sections.evidence_refs).length === 0 && - looksLikeCodeOnlyResponse(raw) - ) { - return { - summary: `The ${task.title} worker returned code instead of structured analysis.`, - findings: [], - recommendations: ["Rerun this worker with a narrower analysis-only prompt or a stronger structured-output model."], - verifiedFacts: [], - unknowns: ["Worker response looked like generated code/script instead of evidence-backed analysis."], - evidenceRefs: [] - }; - } - - if (summary || findings.length > 0 || recommendations.length > 0) { - return { - summary: summary || `The ${task.title} worker returned partial structured text.`, - findings, - recommendations, - verifiedFacts: sectionToList(sections.verified_facts), - unknowns: sectionToList(sections.unknowns), - evidenceRefs: sectionToList(sections.evidence_refs) - }; - } - - return { - summary: `The ${task.title} worker could not return structured JSON.`, - findings: [], - recommendations: [], - verifiedFacts: [], - unknowns: [], - evidenceRefs: [] - }; -} - -function normalizeSynthesisPayload(raw: string, intent: string): SynthesisPayload { - const parsed = extractJsonObject(raw); - if (parsed) { - return { - headline: - typeof parsed.headline === "string" && parsed.headline.trim().length > 0 - ? parsed.headline.trim() - : `Completed a bounded swarm review for: ${intent}`, - summary: - typeof parsed.summary === "string" && parsed.summary.trim().length > 0 - ? parsed.summary.trim() - : "The swarm synthesized the delegated outputs.", - priorities: normalizeStringList(parsed.priorities), - next_steps: normalizeStringList(parsed.next_steps), - verified_facts: normalizeStringList(parsed.verified_facts ?? parsed.verifiedFacts), - unknowns: normalizeStringList(parsed.unknowns), - evidence_refs: normalizeStringList(parsed.evidence_refs ?? parsed.evidenceRefs) - }; - } - - const sections = parseStructuredSections(raw); - const headline = sectionToText(sections.headline); - const summary = sectionToText(sections.summary) || sectionToText(sections.body); - const priorities = sectionToList(sections.priorities); - const nextSteps = sectionToList((sections.next_steps?.length ?? 0) > 0 ? sections.next_steps : sections.recommendations); - - if (headline || summary || priorities.length > 0 || nextSteps.length > 0) { - return { - headline: headline || `Completed a bounded swarm review for: ${intent}`, - summary: summary || "The swarm synthesized the delegated outputs.", - priorities, - next_steps: nextSteps, - verified_facts: sectionToList(sections.verified_facts), - unknowns: sectionToList(sections.unknowns), - evidence_refs: sectionToList(sections.evidence_refs) - }; - } - - return { - headline: `Completed a bounded swarm review for: ${intent}`, - summary: "The swarm finished, but synthesis did not return structured JSON.", - priorities: [], - next_steps: [], - verified_facts: [], - unknowns: [], - evidence_refs: [] - }; -} - -function taskTypeForProfile(profile: ModelProfile): AIRouterTask { - if (profile === "reviewer") { - return "code-smell-detection"; - } - if (profile === "planner") { - return "architecture-review"; - } - if (profile === "synthesizer") { - return "report-synthesis"; - } - return "generic-analysis"; -} - -function recommendedChunkSize( - context: ProjectContext, - requested?: number, - scopeBias: ScopeBias = "balanced" -): SwarmRunResult["chunking"] { - const sourceFileCount = context.discovery.structure.sourceFileCount; - const scopeUnits = Math.max(context.discovery.structure.topLevelDirectories.length, 1); - const adaptiveChunkSize = - sourceFileCount >= 800 ? 1 - : sourceFileCount >= 250 ? 2 - : sourceFileCount >= 120 ? 3 - : 4; - const selectedChunkSize = requested ? clamp(Math.trunc(requested), 1, 6) : adaptiveChunkSize; - - return { - selectedChunkSize, - requestedChunkSize: requested, - scopeUnits, - scopeChunks: 0, - queuedTasks: 0, - queueStrategy: "round-robin", - scopeBias, - scopeHints: [] - }; -} - -function recommendedParallelism(requested?: number): SwarmRunResult["parallelism"] { - const cpuCount = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length; - const loadAverage1m = Number(os.loadavg()[0]?.toFixed(2) ?? 0); - const totalMemoryMb = Math.round(os.totalmem() / 1024 / 1024); - const freeMemoryMb = Math.round(os.freemem() / 1024 / 1024); - const baseParallelism = clamp(Math.floor(cpuCount / 2), 2, 4); - const highLoad = loadAverage1m >= cpuCount * 0.75; - const lowMemory = freeMemoryMb < 2048; - const adaptiveParallelism = highLoad || lowMemory ? Math.max(1, baseParallelism - 1) : baseParallelism; - const selected = requested ? clamp(Math.trunc(requested), 1, 8) : adaptiveParallelism; - const pressure = deriveResourcePressure({ - cpuCount, - loadAverage1m, - freeMemoryMb - }); - - return { - selected, - requested, - cpuCount, - loadAverage1m, - freeMemoryMb, - totalMemoryMb, - pressure - }; -} - -export function recommendedResilience(requestedTimeoutMs?: number, requestedRetries?: number): SwarmRunResult["resilience"] { - return { - runTimeoutMs: 90_000, - plannerTimeoutMs: 18_000, - synthesisTimeoutMs: 15_000, - taskTimeoutMs: requestedTimeoutMs ? clamp(Math.trunc(requestedTimeoutMs), 5_000, 120_000) : 20_000, - requestedTaskTimeoutMs: requestedTimeoutMs, - queueBudget: 0, - maxRetries: requestedRetries === undefined ? 1 : clamp(Math.trunc(requestedRetries), 0, 4), - plannerTimedOut: false, - synthesisTimedOut: false, - runTimedOut: false, - timedOutTasks: 0, - retriedTasks: 0, - splitTasks: 0, - failedTasks: 0, - droppedTasks: 0, - localBudgetMode: false, - adaptiveQueueBudget: false - }; -} - -export function deriveResourcePressure(parallelism: Pick): ResourcePressure { - const loadRatio = parallelism.cpuCount > 0 ? parallelism.loadAverage1m / parallelism.cpuCount : 0; - - if (loadRatio >= 0.75 || parallelism.freeMemoryMb < 1024) { - return "high"; - } - - if (loadRatio >= 0.5 || parallelism.freeMemoryMb < 2048) { - return "medium"; - } - - return "low"; -} - -export function deriveAdaptiveQueueBudget( - parallelism: Pick -): number { - const pressure = deriveResourcePressure(parallelism); - const balancedBudget = Math.max(parallelism.selected * 4, 12); - - if (pressure === "high") { - return Math.max(parallelism.selected * 2, 6); - } - - if (pressure === "medium") { - return Math.max(parallelism.selected * 3, 8); - } - - return balancedBudget; -} - -export function deriveSplitGroupSize(pressure: ResourcePressure, localBudgetMode: boolean): number { - if (localBudgetMode && pressure === "high") { - return 1; - } - - if (localBudgetMode && pressure === "medium") { - return 2; - } - - if (localBudgetMode) { - return 3; - } - - if (pressure === "high") { - return 2; - } - - if (pressure === "medium") { - return 3; - } - - return 4; -} - -function applyResilienceOverrides( - resilience: SwarmRunResult["resilience"], - options: SwarmRuntimeOptions, - parallelism: SwarmRunResult["parallelism"] -): void { - resilience.runTimeoutMs = options.runTimeoutMs ? clamp(Math.trunc(options.runTimeoutMs), 10_000, 600_000) : 90_000; - resilience.requestedRunTimeoutMs = options.runTimeoutMs; - resilience.plannerTimeoutMs = options.plannerTimeoutMs - ? clamp(Math.trunc(options.plannerTimeoutMs), 3_000, resilience.runTimeoutMs) - : Math.min(18_000, resilience.runTimeoutMs); - resilience.requestedPlannerTimeoutMs = options.plannerTimeoutMs; - resilience.synthesisTimeoutMs = options.synthesisTimeoutMs - ? clamp(Math.trunc(options.synthesisTimeoutMs), 3_000, resilience.runTimeoutMs) - : Math.min(15_000, resilience.runTimeoutMs); - resilience.requestedSynthesisTimeoutMs = options.synthesisTimeoutMs; - resilience.adaptiveQueueBudget = !options.maxQueuedTasks; - resilience.queueBudget = options.maxQueuedTasks - ? clamp(Math.trunc(options.maxQueuedTasks), parallelism.selected, 64) - : deriveAdaptiveQueueBudget(parallelism); - resilience.requestedQueueBudget = options.maxQueuedTasks; -} - -function shouldUseLocalBudgetMode(resilience: SwarmRunResult["resilience"]): boolean { - return ( - resilience.runTimeoutMs <= 45_000 || - resilience.plannerTimeoutMs <= 8_000 || - resilience.synthesisTimeoutMs <= 8_000 - ); -} - -async function mapWithConcurrency( - items: TInput[], - concurrency: number, - worker: (item: TInput, index: number) => Promise -): Promise { - const results = new Array(items.length); - let nextIndex = 0; - - async function runWorker(): Promise { - while (true) { - const currentIndex = nextIndex; - nextIndex += 1; - - if (currentIndex >= items.length) { - return; - } - - results[currentIndex] = await worker(items[currentIndex]!, currentIndex); - } - } - - await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => runWorker())); - return results; -} - -async function drainQueueWithConcurrency( - queue: QueuedSwarmTask[], - concurrency: number, - worker: (task: QueuedSwarmTask) => Promise -): Promise { - const results: SwarmWorkerResult[] = []; - - async function runWorker(): Promise { - while (true) { - const task = queue.shift(); - if (!task) { - return; - } - - const outcome = await worker(task); - if (outcome.result) { - results.push(outcome.result); - } - if (outcome.requeue && outcome.requeue.length > 0) { - queue.push(...outcome.requeue); - } - } - } - - await Promise.all(Array.from({ length: Math.min(concurrency, Math.max(queue.length, 1)) }, () => runWorker())); - return results; -} - -async function drainTaskLevelsWithConcurrency( - levels: QueuedSwarmTask[][], - concurrency: number, - worker: (task: QueuedSwarmTask) => Promise -): Promise { - const results: SwarmWorkerResult[] = []; - - for (const level of levels) { - if (level.length === 0) { - continue; - } - - const levelResults = await drainQueueWithConcurrency([...level], concurrency, worker); - results.push(...levelResults); - } - - return results; -} - -function uniqueStrings(items: string[]): string[] { - return [...new Set(items.filter((item) => item.trim().length > 0))]; -} - -function stableSerialize(value: unknown): string { - if (value === null || value === undefined) { - return "null"; - } - - if (Array.isArray(value)) { - return `[${value.map((entry) => stableSerialize(entry)).join(",")}]`; - } - - if (typeof value === "object") { - const record = value as Record; - return `{${Object.keys(record) - .sort((left, right) => left.localeCompare(right)) - .map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`) - .join(",")}}`; - } - - return JSON.stringify(value); -} - -function canonicalizePromptText(text: string | undefined): string | undefined { - if (!text) { - return undefined; - } - - return text - .replace(/\r\n/g, "\n") - .replace(/^Attempt:\s+\d+\s*$/gim, "Attempt: ") - .split("\n") - .map((line) => line.trimEnd()) - .join("\n") - .trim(); -} - -function buildSwarmCacheKey( - context: ProjectContext, - request: AIRouterRequest, - selection: Pick -): string { - return createHash("sha256") - .update( - stableSerialize({ - repoName: context.repoName, - targetPath: context.targetPath, - gitCommit: context.discovery.git.latestCommit ?? "", - request: { - task: request.task, - profile: request.profile, - allowRemote: request.allowRemote, - prompt: canonicalizePromptText(request.prompt), - context: canonicalizePromptText(request.context) - }, - selection - }) - ) - .digest("hex"); -} - -function createEmptySwarmResponseCache(): SwarmResponseCacheDocument { - return { - version: 1, - updatedAt: new Date(0).toISOString(), - entries: {} - }; -} - -function normalizeSwarmResponseCache(document: SwarmResponseCacheDocument | undefined): SwarmResponseCacheDocument { - if (!document || document.version !== 1 || !document.entries || typeof document.entries !== "object") { - return createEmptySwarmResponseCache(); - } - - return { - version: 1, - updatedAt: typeof document.updatedAt === "string" ? document.updatedAt : new Date(0).toISOString(), - entries: document.entries - }; -} - -function pruneSwarmResponseCache(cache: SwarmResponseCacheDocument): void { - const entries = Object.entries(cache.entries); - if (entries.length <= SWARM_RESPONSE_CACHE_MAX_ENTRIES) { - return; - } - - entries - .sort((left, right) => { - const lastUsedDelta = Date.parse(right[1].lastUsedAt) - Date.parse(left[1].lastUsedAt); - if (lastUsedDelta !== 0) { - return lastUsedDelta; - } - return right[1].hits - left[1].hits; - }) - .slice(SWARM_RESPONSE_CACHE_MAX_ENTRIES) - .forEach(([key]) => { - delete cache.entries[key]; - }); -} - -function createEmptySwarmLearning(): SwarmLearningDocument { - return { - version: 1, - updatedAt: new Date(0).toISOString(), - scopes: {} - }; -} - -function normalizeSwarmLearning(document: SwarmLearningDocument | undefined): SwarmLearningDocument { - if (!document || document.version !== 1 || !document.scopes || typeof document.scopes !== "object") { - return createEmptySwarmLearning(); - } - - return { - version: 1, - updatedAt: typeof document.updatedAt === "string" ? document.updatedAt : new Date(0).toISOString(), - scopes: document.scopes - }; -} - -function normalizeLearningScopeKeys(scopePath: string): string[] { - const normalized = normalizeHintPath(scopePath) || "."; - if (normalized === ".") { - return ["."]; - } - - const topLevel = normalized.split("/")[0] ?? normalized; - return uniqueStrings([normalized, topLevel]); -} - -function learningSignalForScope(scopePath: string, learning: SwarmLearningDocument): number { - const normalizedScope = normalizeHintPath(scopePath) || "."; - let signal = 0; - - for (const [candidatePath, record] of Object.entries(learning.scopes)) { - const normalizedCandidate = normalizeHintPath(candidatePath) || "."; - - if (normalizedCandidate === normalizedScope) { - signal += record.signalScore * 3; - continue; - } - - if (normalizedCandidate.startsWith(`${normalizedScope}/`)) { - signal += Math.max(1, Math.floor(record.signalScore * 1.5)); - continue; - } - - if (normalizedScope.startsWith(`${normalizedCandidate}/`)) { - signal += Math.max(1, Math.floor(record.signalScore / 2)); - } - } - - return signal; -} - -function summarizeLearnedScopeBoosts(scopePaths: string[], learning: SwarmLearningDocument): string[] { - return scopePaths - .map((scopePath) => ({ - scopePath, - signal: learningSignalForScope(scopePath, learning) - })) - .filter((entry) => entry.signal > 0) - .sort((left, right) => right.signal - left.signal || left.scopePath.localeCompare(right.scopePath)) - .slice(0, 4) - .map((entry) => entry.scopePath); -} - -function signalScoreForResult(result: SwarmWorkerResult): number { - if (result.status !== "completed") { - return 0; - } - - return result.findings.length * 2 + result.recommendations.length; -} - -function updateSwarmLearning(learning: SwarmLearningDocument, workerResults: SwarmWorkerResult[]): void { - const now = new Date().toISOString(); - - for (const result of workerResults) { - for (const scopeKey of result.scopePaths.flatMap((scopePath) => normalizeLearningScopeKeys(scopePath))) { - const existing = learning.scopes[scopeKey] ?? { - signalScore: 0, - completedRuns: 0, - failureCount: 0, - timeoutCount: 0, - lastSeenAt: now - }; - - if (result.status === "completed") { - existing.completedRuns += 1; - existing.signalScore = Math.min(existing.signalScore + signalScoreForResult(result), 60); - } else if (result.status === "timed_out") { - existing.timeoutCount += 1; - existing.signalScore = Math.max(0, existing.signalScore - 1); - } else { - existing.failureCount += 1; - existing.signalScore = Math.max(0, existing.signalScore - 2); - } - - existing.lastSeenAt = now; - learning.scopes[scopeKey] = existing; - } - } - - learning.updatedAt = now; -} - -function createOptimizationStats(): SwarmOptimizationStats { - return { - cacheHits: 0, - cacheMisses: 0, - cacheWrites: 0, - scopeMemoryHits: 0, - scopeMemoryMisses: 0, - scopeMemoryStale: 0, - scopeMemoryWrites: 0, - scopeMemoryReuseCandidates: 0, - scopeMemoryReductionHints: [], - derivedTasksQueued: 0, - derivedTasksSkipped: 0, - learnedScopeBoosts: [] - }; -} - -function reusableScopeMemory(records: ScopeMemoryRecord[]): ScopeMemoryRecord[] { - return records.filter((record) => record.freshness.status === "fresh" && record.coverage?.status === "complete"); -} - -function scopeMemoryReductionHints(records: ScopeMemoryRecord[]): string[] { - return reusableScopeMemory(records).map( - (record) => `${record.scope}: fresh complete memory available; prefer delta analysis and verify only changed evidence.` - ); -} - -function isReducibleScopeMemory(record: ScopeMemoryRecord): boolean { - return record.freshness.status === "fresh" && record.coverage?.status === "complete" && !record.files?.hashTruncated; -} - -function reducibleChunkIds(scopeChunks: ScopeChunk[], scopeMemoryByChunk: Map): Set { - return new Set( - scopeChunks - .filter((chunk) => { - const records = scopeMemoryByChunk.get(chunk.chunkId) ?? []; - const recordScopes = new Set(records.filter(isReducibleScopeMemory).map((record) => record.scope)); - return chunk.scopePaths.length > 0 && chunk.scopePaths.every((scopePath) => recordScopes.has(scopePath)); - }) - .map((chunk) => chunk.chunkId) - ); -} - -function splitPlannerTasks(planner: PlannerPayload): { - initialTasks: SwarmPlanTask[]; - deferredReasoningTasks: SwarmPlanTask[]; -} { - const deferredReasoningTasks = planner.tasks.filter((task) => task.profile === "reasoning"); - const initialTasks = planner.tasks.filter((task) => task.profile !== "reasoning"); - - if (deferredReasoningTasks.length === 0 || initialTasks.length === 0) { - return { - initialTasks: planner.tasks, - deferredReasoningTasks: [] - }; - } - - return { - initialTasks, - deferredReasoningTasks - }; -} - -function createTaskLevels(tasks: SwarmPlanTask[]): SwarmPlanTask[][] { - const orderedTasks = normalizeTaskDependencies(tasks); - const completed = new Set(); - const levels: SwarmPlanTask[][] = []; - - while (completed.size < orderedTasks.length) { - const level = orderedTasks.filter((task) => !completed.has(task.taskId) && (task.dependsOn ?? []).every((dependencyId) => completed.has(dependencyId))); - - if (level.length === 0) { - levels.push(orderedTasks.filter((task) => !completed.has(task.taskId))); - break; - } - - levels.push(level); - for (const task of level) { - completed.add(task.taskId); - } - } - - return levels; -} - -function reserveDerivedReasoningBudget( - deferredReasoningTasks: SwarmPlanTask[], - scopeChunks: ScopeChunk[], - queueBudget: number, - parallelism: number, - localBudgetMode: boolean -): number { - if (deferredReasoningTasks.length === 0 || scopeChunks.length === 0) { - return 0; - } - - const potentialTasks = deferredReasoningTasks.length * scopeChunks.length; - const baselineReserve = localBudgetMode ? 1 : Math.min(Math.max(parallelism, 1), 2); - const budgetCap = Math.max(1, Math.floor(queueBudget / 3)); - - return Math.min(potentialTasks, baselineReserve, budgetCap); -} - -function deriveReasoningTasks( - deferredReasoningTasks: SwarmPlanTask[], - scopeChunks: ScopeChunk[], - workerResults: SwarmWorkerResult[], - reservedBudget: number, - scopeHints: string[] -): { - tasks: QueuedSwarmTask[]; - skipped: number; -} { - if (deferredReasoningTasks.length === 0 || reservedBudget <= 0) { - return { - tasks: [], - skipped: deferredReasoningTasks.length * scopeChunks.length - }; - } - - const chunkScores = scopeChunks - .map((chunk, index) => ({ - chunk, - index, - hintMatched: chunk.scopePaths.some((scopePath) => matchesScopeHint(scopePath, scopeHints)), - evidenceScore: workerResults - .filter((result) => result.chunkId === chunk.chunkId) - .reduce((total, result) => total + signalScoreForResult(result), 0) - })) - .filter((entry) => entry.hintMatched || entry.evidenceScore > 0) - .sort((left, right) => { - if (left.hintMatched !== right.hintMatched) { - return left.hintMatched ? -1 : 1; - } - if (right.evidenceScore !== left.evidenceScore) { - return right.evidenceScore - left.evidenceScore; - } - return left.index - right.index; - }); - - const candidates = chunkScores.flatMap((entry) => - deferredReasoningTasks.map((task) => ({ - taskId: `${task.taskId}__${entry.chunk.chunkId}`, - parentTaskId: task.taskId, - title: `${task.title} [${entry.chunk.label}]`, - goal: task.goal, - profile: task.profile, - deliverable: task.deliverable, - chunk: entry.chunk, - attempt: 1 - })) - ); - - const selectedTasks = candidates.slice(0, reservedBudget); - return { - tasks: selectedTasks, - skipped: Math.max(0, deferredReasoningTasks.length * scopeChunks.length - selectedTasks.length) - }; -} - -function buildScopeUnitStat(context: ProjectContext, scopePath: string): ScopeUnitStat { - const normalized = scopePath.trim().replace(/^\.\/+/, "") || "."; - const baseName = normalized === "." ? "." : path.posix.basename(normalized); - const fileCount = context.discovery.files.filter((file) => file === normalized || file.startsWith(`${normalized}/`)).length; - const sourceFileCount = context.discovery.files.filter( - (file) => (file === normalized || file.startsWith(`${normalized}/`)) && SOURCE_FILE_PATTERN.test(file) - ).length; - - return { - entry: normalized, - directory: context.discovery.files.some((file) => file.startsWith(`${normalized}/`)), - hidden: baseName.startsWith("."), - manifest: ROOT_MANIFEST_FILES.has(baseName), - sourceLike: SOURCE_LIKE_SCOPE_PATTERN.test(baseName), - testLike: /(^|\/)(__tests__|tests?|spec)$/i.test(baseName), - fileCount, - sourceFileCount - }; -} - -function scoreScopeUnit(stat: ScopeUnitStat, scopeBias: ScopeBias, learning: SwarmLearningDocument): number { - const sourceLikeBoost = scopeBias === "source-first" ? 90 : 35; - const testPenalty = scopeBias === "source-first" ? -140 : -25; - const manifestBonus = scopeBias === "source-first" ? 15 : 30; - const learningBoost = Math.min(120, learningSignalForScope(stat.entry, learning) * 6); - - return ( - (stat.directory ? 40 : 0) + - (stat.hidden ? -20 : 20) + - (stat.sourceLike ? sourceLikeBoost : 0) + - (stat.testLike ? testPenalty : 0) + - (stat.sourceFileCount > 0 ? 100 + stat.sourceFileCount * 5 : stat.manifest ? manifestBonus : Math.min(stat.fileCount, 10)) + - learningBoost + - (!stat.directory && !stat.manifest && stat.sourceFileCount === 0 ? -30 : 0) - ); -} - -function normalizeHintPath(value: string): string { - return value.trim().replace(/^\.\/+/, "").replace(/\/+$/, ""); -} - -function matchesScopeHint(entry: string, scopeHints: string[]): boolean { - const normalizedEntry = normalizeHintPath(entry); - - return scopeHints.some((hint) => { - const normalizedHint = normalizeHintPath(hint); - return ( - normalizedHint === normalizedEntry || - normalizedHint.startsWith(`${normalizedEntry}/`) || - normalizedEntry.startsWith(`${normalizedHint}/`) - ); - }); -} - -function escapeRegex(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function extractIntentScopeHints(context: ProjectContext, intent: string): string[] { - const hints = new Set(); - const normalizedIntent = intent.trim(); - - for (const match of normalizedIntent.matchAll(/\b([A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+)\b/g)) { - const candidate = normalizeHintPath(match[1] ?? ""); - if (!candidate) { - continue; - } - - if (context.discovery.files.some((file) => file === candidate || file.startsWith(`${candidate}/`))) { - hints.add(candidate); - continue; - } - - const topLevelCandidate = candidate.split("/")[0]; - if (topLevelCandidate && context.discovery.structure.topLevelDirectories.includes(topLevelCandidate)) { - hints.add(candidate); - } - } - - for (const topLevelDirectory of context.discovery.structure.topLevelDirectories) { - const pattern = new RegExp(`(^|[^A-Za-z0-9_])${escapeRegex(topLevelDirectory)}($|[^A-Za-z0-9_])`, "i"); - if (pattern.test(normalizedIntent)) { - hints.add(topLevelDirectory); - } - } - - return [...hints]; -} - -function prioritizeScopePaths( - context: ProjectContext, - scopePaths: string[], - scopeBias: ScopeBias, - scopeHints: string[] = [], - learning: SwarmLearningDocument = createEmptySwarmLearning() -): string[] { - const stats: ScopeUnitStat[] = uniqueStrings(scopePaths).map((scopePath) => buildScopeUnitStat(context, scopePath)); - - return stats - .sort((left, right) => { - const leftHint = matchesScopeHint(left.entry, scopeHints); - const rightHint = matchesScopeHint(right.entry, scopeHints); - if (leftHint !== rightHint) { - return rightHint ? 1 : -1; - } - - const leftScore = scoreScopeUnit(left, scopeBias, learning); - const rightScore = scoreScopeUnit(right, scopeBias, learning); - - if (rightScore !== leftScore) { - return rightScore - leftScore; - } - if (right.sourceFileCount !== left.sourceFileCount) { - return right.sourceFileCount - left.sourceFileCount; - } - if (right.fileCount !== left.fileCount) { - return right.fileCount - left.fileCount; - } - return left.entry.localeCompare(right.entry); - }) - .map((entry) => entry.entry); -} - -function prioritizeScopeUnits( - context: ProjectContext, - scopeBias: ScopeBias, - scopeHints: string[] = [], - learning: SwarmLearningDocument = createEmptySwarmLearning() -): string[] { - return prioritizeScopePaths(context, context.discovery.structure.topLevelDirectories, scopeBias, scopeHints, learning); -} - -function createScopeChunks( - context: ProjectContext, - chunkSize: number, - scopeBias: ScopeBias, - scopeHints: string[] = [], - learning: SwarmLearningDocument = createEmptySwarmLearning() -): ScopeChunk[] { - const directories = prioritizeScopeUnits(context, scopeBias, scopeHints, learning); - const scopeUnits = directories.length > 0 ? directories : ["."]; - const chunks: ScopeChunk[] = []; - - for (let index = 0; index < scopeUnits.length; index += chunkSize) { - const group = scopeUnits.slice(index, index + chunkSize); - chunks.push({ - chunkId: `scope-${chunks.length + 1}`, - label: group.join(", "), - scopePaths: group - }); - } - - return chunks.slice(0, 6); -} - -function createQueuedTasks( - planner: PlannerPayload, - scopeChunks: ScopeChunk[], - parallelism: number, - queueBudget: number, - reducedChunkIds: Set = new Set() -): QueuedSwarmTask[][] { - const maxQueuedTasks = Math.max(Math.min(queueBudget, 64), planner.tasks.length, parallelism); - const levels = createTaskLevels(planner.tasks); - const queuedLevels: QueuedSwarmTask[][] = []; - let queuedCount = 0; - - for (const level of levels) { - const queuedLevel: QueuedSwarmTask[] = []; - - for (let chunkIndex = 0; chunkIndex < scopeChunks.length; chunkIndex += 1) { - const chunk = scopeChunks[chunkIndex]!; - const tasksForChunk = reducedChunkIds.has(chunk.chunkId) ? level.slice(0, 1) : level; - for (const task of tasksForChunk) { - queuedLevel.push({ - taskId: `${task.taskId}__${chunk.chunkId}`, - parentTaskId: task.taskId, - title: `${task.title} [${chunk.label}]`, - goal: task.goal, - profile: task.profile, - deliverable: task.deliverable, - chunk, - attempt: 1 - }); - queuedCount += 1; - - if (queuedCount >= maxQueuedTasks) { - return queuedLevel.length > 0 ? [...queuedLevels, queuedLevel] : queuedLevels; - } - } - } - - if (queuedLevel.length > 0) { - queuedLevels.push(queuedLevel); - } - } - - return queuedLevels; -} - -function totalPotentialQueuedTasks(planner: PlannerPayload, scopeChunks: ScopeChunk[]): number { - return planner.tasks.length * scopeChunks.length; -} - -function createDeadline(runTimeoutMs: number): SwarmDeadline { - const startedAtMs = Date.now(); - return { - startedAtMs, - deadlineMs: startedAtMs + runTimeoutMs - }; -} - -function remainingBudgetMs(deadline: SwarmDeadline): number { - return Math.max(0, deadline.deadlineMs - Date.now()); -} - -function listImmediateChildScopePaths( - context: ProjectContext, - scopePath: string, - scopeBias: ScopeBias, - scopeHints: string[] = [], - learning: SwarmLearningDocument = createEmptySwarmLearning() -): string[] { - const normalizedScope = scopePath.trim().replace(/^\.\/+/, "") || "."; - const prefix = normalizedScope === "." ? "" : `${normalizedScope}/`; - const children = new Set(); - - for (const file of context.discovery.files) { - if (normalizedScope !== "." && !(file === normalizedScope || file.startsWith(prefix))) { - continue; - } - - const relative = normalizedScope === "." ? file : file.slice(prefix.length); - if (!relative || relative === file && file === normalizedScope) { - continue; - } - - const [head] = relative.split("/"); - if (!head) { - continue; - } - - children.add(normalizedScope === "." ? head : `${normalizedScope}/${head}`); - } - - return prioritizeScopePaths(context, [...children], scopeBias, scopeHints, learning); -} - -function groupScopePaths(scopePaths: string[], maxGroupSize: number): string[][] { - const groups: string[][] = []; - const safeGroupSize = Math.max(1, maxGroupSize); - - for (let index = 0; index < scopePaths.length; index += safeGroupSize) { - groups.push(scopePaths.slice(index, index + safeGroupSize)); - } - - return groups; -} - -function splitScopeChunk( - context: ProjectContext, - chunk: ScopeChunk, - scopeBias: ScopeBias, - scopeHints: string[], - learning: SwarmLearningDocument, - pressure: ResourcePressure, - localBudgetMode: boolean -): ScopeChunk[] { - const splitGroupSize = deriveSplitGroupSize(pressure, localBudgetMode); - - if (chunk.scopePaths.length === 1) { - const childScopePaths = listImmediateChildScopePaths(context, chunk.scopePaths[0]!, scopeBias, scopeHints, learning); - if (childScopePaths.length <= 1) { - return []; - } - - const childGroups = groupScopePaths(childScopePaths, splitGroupSize); - return childGroups.map((scopePaths, index) => ({ - chunkId: `${chunk.chunkId}.${index + 1}`, - label: scopePaths.join(", "), - scopePaths - })); - } - - const effectiveGroupSize = Math.max(1, Math.min(splitGroupSize, chunk.scopePaths.length - 1)); - const parts = groupScopePaths(chunk.scopePaths, effectiveGroupSize).filter((group) => group.length > 0); - - return parts.map((scopePaths, index) => ({ - chunkId: `${chunk.chunkId}.${index + 1}`, - label: scopePaths.join(", "), - scopePaths - })); -} - -function buildChunkContext(context: ProjectContext, scopePaths: string[]): string { - const relevantFiles = context.discovery.files.filter((file) => - scopePaths.some((scopePath) => scopePath === "." || file === scopePath || file.startsWith(`${scopePath}/`)) - ); - const sampleFiles = relevantFiles.slice(0, 10); - const sourceFiles = relevantFiles.filter((file) => !/(^|\/)(tests?|spec)\//i.test(file)).length; - const testFiles = relevantFiles.length - sourceFiles; - - return [ - `Repository: ${context.repoName}`, - `Focus scope: ${scopePaths.join(", ")}`, - `Scoped file count: ${relevantFiles.length}`, - `Scoped source files: ${sourceFiles}`, - `Scoped test files: ${testFiles}`, - `Sample files: ${sampleFiles.join(", ") || "None"}`, - `Languages: ${context.discovery.languages.join(", ") || "Unknown"}`, - `Frameworks: ${context.discovery.frameworks.join(", ") || "Unknown"}`, - `Testing: ${context.discovery.testing.join(", ") || "Not detected"}`, - buildMemoryBriefSummary(context, 16) - ].join("\n"); -} - -function buildPlannerPrompt(context: ProjectContext, intent: string): AIRouterRequest { - return { - task: "intent-routing", - profile: "planner", - context: buildRepoSummary(context), - prompt: [ - "You are planning a bounded model swarm for project-brain.", - "MEMORY_BRIEF is the priority context. Use it before repository summary details and previous reports.", - "Do not invent repository facts.", - "Split the user request into at most 4 small analysis tasks.", - "Each task must fit one profile: worker, reviewer, or reasoning.", - "When a task logically needs prior evidence, add dependsOn with the upstream task IDs.", - "Return JSON only in this shape:", - '{ "overview": string, "tasks": [{ "taskId": string, "title": string, "goal": string, "profile": "worker|reviewer|reasoning", "deliverable": string, "dependsOn"?: string[] }] }', - `User intent: ${intent}` - ].join("\n") - }; -} - -function buildWorkerPrompt( - context: ProjectContext, - intent: string, - overview: string, - task: QueuedSwarmTask, - scopeMemory: ScopeMemoryRecord[] -): AIRouterRequest { - return { - task: taskTypeForProfile(task.profile as ModelProfile), - profile: task.profile as ModelProfile, - allowRemote: task.profile === "planner" || task.profile === "synthesizer", - context: [buildChunkContext(context, task.chunk.scopePaths), renderScopeMemoryForPrompt(scopeMemory)].join("\n\n"), - prompt: [ - "You are a bounded worker inside a project-brain swarm.", - "MEMORY_BRIEF is the priority context. Use it first, then scoped files and generated artifacts.", - "If scope memory is available and fresh, reuse it and only add new evidence or changed facts.", - "If scope memory coverage is complete, do not restate old facts unless they are needed to explain a delta.", - "If scope memory is stale, call out changed or missing evidence instead of repeating the old analysis blindly.", - "Use only the scoped repository context provided.", - "Do not assume facts that are not in the repository summary.", - "Every factual claim must be backed by a file path, route, config, manifest, or generated artifact reference.", - "Use unknowns for missing or unverified relationships.", - "Return JSON only in this shape:", - '{ "summary": string, "findings": string[], "recommendations": string[], "verified_facts": string[], "unknowns": string[], "evidence_refs": string[] }', - `User intent: ${intent}`, - `Swarm overview: ${overview}`, - `Task title: ${task.title}`, - `Attempt: ${task.attempt}`, - `Task goal: ${task.goal}`, - `Scope chunk: ${task.chunk.label}`, - `Scope paths: ${task.chunk.scopePaths.join(", ")}`, - `Expected deliverable: ${task.deliverable}` - ].join("\n") - }; -} - -function buildSynthesisPrompt( - context: ProjectContext, - intent: string, - overview: string, - workerResults: SwarmWorkerResult[] -): AIRouterRequest { - return { - task: "report-synthesis", - profile: "synthesizer", - context: buildMemoryBriefSummary(context, 24), - prompt: [ - "You are the synthesizer for a project-brain swarm run.", - "MEMORY_BRIEF is the priority context. Use it to preserve decisions, corrections, unknowns, and token guidance.", - "Merge the worker outputs into a concise, decision-oriented result.", - "Keep facts separate from unknowns. Do not promote worker recommendations into facts unless evidence_refs support them.", - "Return JSON only in this shape:", - '{ "headline": string, "summary": string, "verified_facts": string[], "unknowns": string[], "evidence_refs": string[], "priorities": string[], "next_steps": string[] }', - `User intent: ${intent}`, - `Swarm overview: ${overview}`, - "Worker outputs:", - JSON.stringify(workerResults, null, 2) - ].join("\n") - }; -} - -function selectionIdentity(selection: Pick): Pick< - ModelSelection, - "provider" | "model" | "residency" | "profile" -> { - return { - provider: selection.provider, - model: selection.model, - residency: selection.residency, - profile: selection.profile - }; -} - -function recordSwarmCacheEntry( - cache: SwarmResponseCacheDocument, - key: string, - context: ProjectContext, - request: AIRouterRequest, - selection: Pick, - response: string -): void { - const now = new Date().toISOString(); - cache.entries[key] = { - key, - request: { - task: request.task, - profile: request.profile, - prompt: canonicalizePromptText(request.prompt) ?? "", - context: canonicalizePromptText(request.context), - allowRemote: request.allowRemote - }, - selection: selectionIdentity(selection), - response, - createdAt: now, - lastUsedAt: now, - hits: 0 - }; - cache.updatedAt = now; - pruneSwarmResponseCache(cache); -} - -async function askWithSwarmCache( - context: ProjectContext, - assistant: SwarmAssistant, - request: AIRouterRequest, - selection: ModelSelection, - cache: SwarmResponseCacheDocument, - optimization: SwarmOptimizationStats -): Promise { - const policyRequest = applyTokenPolicy(request); - const key = buildSwarmCacheKey(context, policyRequest, selectionIdentity(selection)); - const cachedEntry = cache.entries[key]; - - if (cachedEntry) { - cachedEntry.hits += 1; - cachedEntry.lastUsedAt = new Date().toISOString(); - cache.updatedAt = cachedEntry.lastUsedAt; - optimization.cacheHits += 1; - return cachedEntry.response; - } - - optimization.cacheMisses += 1; - const response = await assistant.ask(policyRequest); - recordSwarmCacheEntry(cache, key, context, policyRequest, selection, response); - optimization.cacheWrites += 1; - return response; -} - -function applySwarmRequestPolicy(request: AIRouterRequest, preset: TokenPreset | undefined): AIRouterRequest { - return applyPresetPolicy(request, preset ?? "balanced"); -} - -function renderSwarmReport( - context: ProjectContext, - intent: string, - resilience: SwarmRunResult["resilience"], - chunking: SwarmRunResult["chunking"], - parallelism: SwarmRunResult["parallelism"], - plannerSelection: ModelSelection, - planner: PlannerPayload, - optimization: SwarmOptimizationStats, - workerResults: SwarmWorkerResult[], - synthesisSelection: ModelSelection, - synthesis: SynthesisPayload -): string { - return `# Swarm Run - -## Intent - -- Repository: ${context.repoName} -- Intent: ${intent} - -## Planner - -- Run timeout: ${resilience.runTimeoutMs} ms -- Planner timeout: ${resilience.plannerTimeoutMs} ms -- Synthesis timeout: ${resilience.synthesisTimeoutMs} ms -- Worker timeout: ${resilience.taskTimeoutMs} ms -- Local budget mode: ${resilience.localBudgetMode ? "yes" : "no"} -- Adaptive queue budget: ${resilience.adaptiveQueueBudget ? "yes" : "no"} -- Queue budget: ${resilience.queueBudget} -- Max retries: ${resilience.maxRetries} -- Planner timed out: ${resilience.plannerTimedOut ? "yes" : "no"} -- Synthesis timed out: ${resilience.synthesisTimedOut ? "yes" : "no"} -- Run timed out: ${resilience.runTimedOut ? "yes" : "no"} -- Timed out tasks: ${resilience.timedOutTasks} -- Retried tasks: ${resilience.retriedTasks} -- Split tasks: ${resilience.splitTasks} -- Failed tasks: ${resilience.failedTasks} -- Dropped tasks: ${resilience.droppedTasks} -- Chunk size: ${chunking.selectedChunkSize}${chunking.requestedChunkSize ? ` (requested=${chunking.requestedChunkSize})` : ""} -- Queue strategy: ${chunking.queueStrategy} -- Scope bias: ${chunking.scopeBias} -- Scope hints: ${chunking.scopeHints.join(", ") || "None"} -- Scope units: ${chunking.scopeUnits} -- Scope chunks: ${chunking.scopeChunks} -- Queued worker tasks: ${chunking.queuedTasks} -- Parallel workers: ${parallelism.selected}${parallelism.requested ? ` (requested=${parallelism.requested})` : ""} -- CPU cores seen: ${parallelism.cpuCount} -- Load average (1m): ${parallelism.loadAverage1m} -- Free memory: ${parallelism.freeMemoryMb} MB -- Resource pressure: ${parallelism.pressure} -- Model: ${plannerSelection.model} -- Provider: ${plannerSelection.provider} -- Profile: ${plannerSelection.profile} -- Residency: ${plannerSelection.residency} -- Overview: ${planner.overview} -- Cache hits: ${optimization.cacheHits} -- Cache misses: ${optimization.cacheMisses} -- Cache writes: ${optimization.cacheWrites} -- Scope memory hits: ${optimization.scopeMemoryHits} -- Scope memory misses: ${optimization.scopeMemoryMisses} -- Scope memory stale: ${optimization.scopeMemoryStale} -- Scope memory writes: ${optimization.scopeMemoryWrites} -- Scope memory reuse candidates: ${optimization.scopeMemoryReuseCandidates} -- Scope memory reduction hints: ${optimization.scopeMemoryReductionHints.join(", ") || "None"} -- Derived reasoning tasks queued: ${optimization.derivedTasksQueued} -- Derived reasoning tasks skipped: ${optimization.derivedTasksSkipped} -- Learned scope boosts: ${optimization.learnedScopeBoosts.join(", ") || "None"} - -## Delegated tasks - -${planner.tasks - .map( - (task) => `### ${task.title} - -- Task ID: ${task.taskId} -- Profile: ${task.profile} -- Depends on: ${task.dependsOn?.join(", ") || "None"} -- Goal: ${task.goal} -- Deliverable: ${task.deliverable}` - ) - .join("\n\n")} - -## Worker outputs - -${workerResults - .map( - (result) => `### ${result.title} - -- Parent task: ${result.parentTaskId} -- Chunk: ${result.chunkId} -- Attempt: ${result.attempt} -- Status: ${result.status} -- Scope: ${result.scopePaths.join(", ")} -- Model: ${result.model} -- Provider: ${result.provider} -- Profile: ${result.profile} -- Residency: ${result.residency} -- Summary: ${result.summary} - -${result.error ? `- Error: ${result.error}\n` : ""} - -Findings: -${renderList(result.findings)} - -Verified facts: -${renderList(result.verifiedFacts ?? [])} - -Unknowns: -${renderList(result.unknowns ?? [])} - -Evidence refs: -${renderList(result.evidenceRefs ?? [])} - -Recommendations: -${renderList(result.recommendations)}` - ) - .join("\n\n")} - -## Synthesis - -- Model: ${synthesisSelection.model} -- Provider: ${synthesisSelection.provider} -- Profile: ${synthesisSelection.profile} -- Residency: ${synthesisSelection.residency} -- Headline: ${synthesis.headline} - -${synthesis.summary} - -### Verified facts - -${renderList(synthesis.verified_facts)} - -### Unknowns - -${renderList(synthesis.unknowns)} - -### Evidence refs - -${renderList(synthesis.evidence_refs)} - -### Priorities - -${renderList(synthesis.priorities)} - -### Next steps - -${renderList(synthesis.next_steps)} -`; -} - -export async function runSwarm( - context: ProjectContext, - intent: string, - assistant: SwarmAssistant, - options: SwarmRuntimeOptions = {} -): Promise { - const parallelism = recommendedParallelism(options.parallelism); - const chunking = recommendedChunkSize(context, options.chunkSize, options.scopeBias ?? "balanced"); - chunking.scopeHints = extractIntentScopeHints(context, intent); - const optimization = createOptimizationStats(); - const resilience = recommendedResilience(options.taskTimeoutMs, options.maxRetries); - applyResilienceOverrides(resilience, options, parallelism); - resilience.localBudgetMode = shouldUseLocalBudgetMode(resilience); - if (resilience.localBudgetMode && !parallelism.requested) { - parallelism.selected = Math.min(parallelism.selected, 2); - } - if (resilience.localBudgetMode && resilience.adaptiveQueueBudget) { - resilience.queueBudget = Math.min(resilience.queueBudget, Math.max(parallelism.selected * 2 + 2, 6)); - } - - const swarmResponseCachePath = path.join(context.memoryDir, "swarm", "request_cache.json"); - const swarmLearningPath = path.join(context.memoryDir, "swarm", "learning.json"); - const responseCache = normalizeSwarmResponseCache(await readJsonSafe(swarmResponseCachePath)); - const swarmLearning = normalizeSwarmLearning(await readJsonSafe(swarmLearningPath)); - optimization.learnedScopeBoosts = summarizeLearnedScopeBoosts(context.discovery.structure.topLevelDirectories, swarmLearning); - - const deadline = createDeadline(resilience.runTimeoutMs); - const plannerRequest: AIRouterRequest = { - ...applySwarmRequestPolicy(buildPlannerPrompt(context, intent), options.preset), - allowRemote: !resilience.localBudgetMode, - timeoutMs: Math.min(resilience.plannerTimeoutMs, remainingBudgetMs(deadline)) - }; - const plannerSelection = await assistant.selectModel(plannerRequest); - let planner: PlannerPayload; - - try { - const plannerResponse = await askWithSwarmCache(context, assistant, plannerRequest, plannerSelection, responseCache, optimization); - planner = normalizePlannerPayload(plannerResponse, intent); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (/abort|timeout/i.test(message)) { - resilience.plannerTimedOut = true; - planner = buildFallbackPlan(intent); - } else { - throw error; - } - } - - const scopeChunks = createScopeChunks(context, chunking.selectedChunkSize, chunking.scopeBias, chunking.scopeHints, swarmLearning); - const scopeMemoryByChunk = new Map(); - for (const chunk of scopeChunks) { - const lookup = await loadScopeMemoryRecords(context, chunk.scopePaths); - scopeMemoryByChunk.set(chunk.chunkId, lookup.records); - optimization.scopeMemoryHits += lookup.hits; - optimization.scopeMemoryMisses += lookup.misses; - optimization.scopeMemoryStale += lookup.stale; - optimization.scopeMemoryReuseCandidates += reusableScopeMemory(lookup.records).length; - optimization.scopeMemoryReductionHints = [ - ...optimization.scopeMemoryReductionHints, - ...scopeMemoryReductionHints(lookup.records) - ].slice(0, 12); - } - const reducedChunkIds = reducibleChunkIds(scopeChunks, scopeMemoryByChunk); - if (reducedChunkIds.size > 0) { - optimization.scopeMemoryReductionHints = [ - ...optimization.scopeMemoryReductionHints, - `Reduced queued work for ${reducedChunkIds.size} fresh complete scope chunk(s).` - ].slice(0, 12); - } - const { initialTasks, deferredReasoningTasks } = splitPlannerTasks(planner); - const reservedReasoningBudget = - deferredReasoningTasks.length > 0 && resilience.queueBudget > initialTasks.length - ? reserveDerivedReasoningBudget( - deferredReasoningTasks, - scopeChunks, - resilience.queueBudget, - parallelism.selected, - resilience.localBudgetMode - ) - : 0; - const initialPlanner: PlannerPayload = { - overview: planner.overview, - tasks: initialTasks.length > 0 ? initialTasks : planner.tasks - }; - const initialQueueBudget = reservedReasoningBudget > 0 ? resilience.queueBudget - reservedReasoningBudget : resilience.queueBudget; - const queuedTaskLevels = createQueuedTasks(initialPlanner, scopeChunks, parallelism.selected, initialQueueBudget, reducedChunkIds); - chunking.scopeChunks = scopeChunks.length; - chunking.queuedTasks = queuedTaskLevels.reduce((total, level) => total + level.length, 0); - - const executeQueuedTask = async (task: QueuedSwarmTask): Promise => { - const remainingMs = remainingBudgetMs(deadline); - if (remainingMs <= 0) { - resilience.runTimedOut = true; - resilience.droppedTasks += 1; - return { - result: { - taskId: task.taskId, - parentTaskId: task.parentTaskId, - chunkId: task.chunk.chunkId, - attempt: task.attempt, - status: "timed_out", - title: task.title, - profile: task.profile, - scopePaths: task.chunk.scopePaths, - provider: "ollama", - model: "budget-exhausted", - residency: "local", - summary: "The global swarm time budget was exhausted before this task could run.", - findings: [], - recommendations: ["Increase the run timeout or reduce the queue budget/chunk size."], - verifiedFacts: [], - unknowns: ["The task did not run because the global time budget was exhausted."], - evidenceRefs: [], - error: "Run timeout exceeded before task execution." - } - }; - } - - const request = buildWorkerPrompt( - context, - intent, - planner.overview, - { - ...task - }, - scopeMemoryByChunk.get(task.chunk.chunkId) ?? [] - ); - const timedRequest: AIRouterRequest = { - ...applySwarmRequestPolicy(request, options.preset), - timeoutMs: Math.min(resilience.taskTimeoutMs, remainingMs) - }; - let selection: ModelSelection | undefined; - - try { - selection = await assistant.selectModel(timedRequest); - const response = await askWithSwarmCache(context, assistant, timedRequest, selection, responseCache, optimization); - const payload = normalizeWorkerPayload(response, { - taskId: task.taskId, - title: task.title, - goal: task.goal, - profile: task.profile, - deliverable: task.deliverable - }); - - return { - result: { - taskId: task.taskId, - parentTaskId: task.parentTaskId, - chunkId: task.chunk.chunkId, - attempt: task.attempt, - status: "completed", - title: task.title, - profile: task.profile, - scopePaths: task.chunk.scopePaths, - provider: selection.provider, - model: selection.model, - residency: selection.residency, - summary: payload.summary, - findings: payload.findings, - recommendations: payload.recommendations, - verifiedFacts: payload.verifiedFacts, - unknowns: payload.unknowns, - evidenceRefs: payload.evidenceRefs - } - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - const timedOut = /abort|timeout/i.test(message); - if (timedOut) { - resilience.timedOutTasks += 1; - } - - if (timedOut) { - const splitChunks = splitScopeChunk( - context, - task.chunk, - chunking.scopeBias, - chunking.scopeHints, - swarmLearning, - parallelism.pressure, - resilience.localBudgetMode - ); - if (splitChunks.length > 0) { - resilience.splitTasks += splitChunks.length; - return { - requeue: splitChunks.map((chunk) => ({ - taskId: `${task.parentTaskId}__${chunk.chunkId}`, - parentTaskId: task.parentTaskId, - title: `${task.title.split(" [")[0]} [${chunk.label}]`, - goal: task.goal, - profile: task.profile, - deliverable: task.deliverable, - chunk, - attempt: task.attempt + 1 - })) - }; - } - } - - if (task.attempt <= resilience.maxRetries) { - resilience.retriedTasks += 1; - return { - requeue: [ - { - ...task, - attempt: task.attempt + 1 - } - ] - }; - } - - resilience.failedTasks += 1; - return { - result: { - taskId: task.taskId, - parentTaskId: task.parentTaskId, - chunkId: task.chunk.chunkId, - attempt: task.attempt, - status: timedOut ? "timed_out" : "failed", - title: task.title, - profile: task.profile, - scopePaths: task.chunk.scopePaths, - provider: selection?.provider ?? "ollama", - model: selection?.model ?? "selection-failed", - residency: selection?.residency ?? "local", - summary: timedOut - ? "The worker exceeded the time budget for this scope chunk." - : "The worker failed before producing structured output.", - findings: [], - recommendations: timedOut - ? ["Reduce chunk size or increase the worker timeout for this task."] - : ["Retry the task or inspect the affected scope manually."], - verifiedFacts: [], - unknowns: [timedOut ? "The worker exceeded its time budget." : "The worker failed before producing structured output."], - evidenceRefs: [], - error: message - } - }; - } - }; - - let workerResults = await drainTaskLevelsWithConcurrency( - queuedTaskLevels, - parallelism.selected, - executeQueuedTask - ); - - if (deferredReasoningTasks.length > 0) { - const derivedReasoningScopeChunks = scopeChunks.filter((chunk) => !reducedChunkIds.has(chunk.chunkId)); - const derivedReasoning = deriveReasoningTasks( - deferredReasoningTasks, - derivedReasoningScopeChunks, - workerResults, - reservedReasoningBudget, - chunking.scopeHints - ); - optimization.derivedTasksQueued = derivedReasoning.tasks.length; - optimization.derivedTasksSkipped = derivedReasoning.skipped; - chunking.queuedTasks += derivedReasoning.tasks.length; - - if (derivedReasoning.tasks.length > 0 && remainingBudgetMs(deadline) > 0) { - const reasoningResults = await drainQueueWithConcurrency( - [...derivedReasoning.tasks], - Math.min(parallelism.selected, derivedReasoning.tasks.length), - executeQueuedTask - ); - workerResults = [...workerResults, ...reasoningResults]; - } - } - - resilience.droppedTasks = Math.max(0, totalPotentialQueuedTasks(planner, scopeChunks) - chunking.queuedTasks); - - let synthesis: SynthesisPayload; - const synthesisRequest: AIRouterRequest = { - ...applySwarmRequestPolicy(buildSynthesisPrompt(context, intent, planner.overview, workerResults), options.preset), - allowRemote: !resilience.localBudgetMode, - timeoutMs: Math.min(resilience.synthesisTimeoutMs, Math.max(remainingBudgetMs(deadline), 1_000)) - }; - const synthesisSelection = await assistant.selectModel(synthesisRequest); - - if (remainingBudgetMs(deadline) <= 0) { - resilience.runTimedOut = true; - resilience.synthesisTimedOut = true; - synthesis = { - headline: `The swarm hit its global time budget for: ${intent}`, - summary: "The global run deadline was exhausted before synthesis could complete, so project-brain returned a partial merge from finished worker results.", - priorities: workerResults.flatMap((result) => result.recommendations).slice(0, 5), - next_steps: [ - "Increase the run timeout for broader swarm runs.", - "Reduce queue budget or chunk size to finish within the current budget." - ], - verified_facts: workerResults.flatMap((result) => result.verifiedFacts ?? []).slice(0, 12), - unknowns: ["Synthesis did not run because the global time budget was exhausted."], - evidence_refs: workerResults.flatMap((result) => result.evidenceRefs ?? []).slice(0, 12) - }; - } else { - try { - const synthesisResponse = await askWithSwarmCache( - context, - assistant, - synthesisRequest, - synthesisSelection, - responseCache, - optimization - ); - synthesis = normalizeSynthesisPayload(synthesisResponse, intent); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (/abort|timeout/i.test(message)) { - resilience.synthesisTimedOut = true; - synthesis = { - headline: `The swarm finished with a partial synthesis for: ${intent}`, - summary: "The synthesis step exceeded its time budget, so project-brain returned a partial result from the completed worker outputs.", - priorities: workerResults.flatMap((result) => result.recommendations).slice(0, 5), - next_steps: [ - "Increase synthesis timeout for broader merges.", - "Reduce queue budget or chunk size if the run must finish faster." - ], - verified_facts: workerResults.flatMap((result) => result.verifiedFacts ?? []).slice(0, 12), - unknowns: ["Synthesis timed out before producing a full structured merge."], - evidence_refs: workerResults.flatMap((result) => result.evidenceRefs ?? []).slice(0, 12) - }; - } else { - throw error; - } - } - } - - updateSwarmLearning(swarmLearning, workerResults); - await writeJsonEnsured(swarmResponseCachePath, responseCache); - await writeJsonEnsured(swarmLearningPath, swarmLearning); - await appendLearning(context, synthesis.headline); - for (const nextStep of synthesis.next_steps) { - await appendLearning(context, nextStep); - } - const unknownLoggedAt = new Date().toISOString(); - for (const result of workerResults) { - for (const unknown of result.unknowns ?? []) { - await appendError(context, `${unknownLoggedAt} [${result.scopePaths.join(", ") || "."}] ${unknown}`); - } - } - for (const unknown of synthesis.unknowns) { - await appendError(context, `${unknownLoggedAt} [synthesis] ${unknown}`); - } - - const reportPath = path.join(context.reportsDir, "swarm_run.md"); - const memoryPath = path.join(context.memoryDir, "swarm", "swarm_run.json"); - optimization.scopeMemoryWrites = await writeScopeMemoryFromSwarmResult(context, { - engine: "bounded", - context, - intent, - reportPath, - memoryPath, - resilience, - chunking, - parallelism, - planner: { - provider: plannerSelection.provider, - model: plannerSelection.model, - residency: plannerSelection.residency, - overview: planner.overview - }, - optimization, - tasks: planner.tasks, - workerResults, - synthesis: { - provider: synthesisSelection.provider, - model: synthesisSelection.model, - residency: synthesisSelection.residency, - headline: synthesis.headline, - summary: synthesis.summary, - priorities: synthesis.priorities, - nextSteps: synthesis.next_steps, - verifiedFacts: synthesis.verified_facts, - unknowns: synthesis.unknowns, - evidenceRefs: synthesis.evidence_refs - } - }); - - await writeFileEnsured( - reportPath, - renderSwarmReport( - context, - intent, - resilience, - chunking, - parallelism, - plannerSelection, - planner, - optimization, - workerResults, - synthesisSelection, - synthesis - ) - ); - await writeJsonEnsured(memoryPath, { - repoName: context.repoName, - intent, - resilience, - chunking, - parallelism, - planner: { - selection: plannerSelection, - overview: planner.overview, - tasks: planner.tasks - }, - optimization, - workers: workerResults, - synthesis: { - selection: synthesisSelection, - ...synthesis - } - }); - - return { - engine: "bounded", - context, - intent, - reportPath, - memoryPath, - resilience, - chunking, - parallelism, - planner: { - provider: plannerSelection.provider, - model: plannerSelection.model, - residency: plannerSelection.residency, - overview: planner.overview - }, - optimization, - tasks: planner.tasks, - workerResults, - synthesis: { - provider: synthesisSelection.provider, - model: synthesisSelection.model, - residency: synthesisSelection.residency, - headline: synthesis.headline, - summary: synthesis.summary, - priorities: synthesis.priorities, - nextSteps: synthesis.next_steps, - verifiedFacts: synthesis.verified_facts, - unknowns: synthesis.unknowns, - evidenceRefs: synthesis.evidence_refs - } - }; -} diff --git a/core/token_policy/index.ts b/core/token_policy/index.ts deleted file mode 100644 index a8273f0..0000000 --- a/core/token_policy/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { AIRouterRequest, ModelProfile } from "../ai_router/router"; - -const TOKEN_POLICY_MARKER = "[project-brain-token-policy:v1]"; -const PRESET_POLICY_MARKER = "[project-brain-preset-policy:v1]"; - -export type TokenPreset = "cheap" | "balanced" | "thorough"; - -function policyForProfile(profile: ModelProfile | undefined): string[] { - const common = [ - TOKEN_POLICY_MARKER, - "Be concise and structured.", - "Do not repeat the prompt or restate obvious context.", - "Do not invent paths, APIs, versions, entities, or relationships.", - "Use UNKNOWN when evidence is missing.", - "Prefer JSON or compact bullets when the prompt asks for structured output." - ]; - - if (profile === "worker" || profile === "reviewer") { - return [ - ...common, - "Return only findings that are supported by provided repository context.", - "Limit findings, recommendations, evidence_refs, and unknowns to the highest-signal items." - ]; - } - - if (profile === "synthesizer") { - return [ - ...common, - "Deduplicate worker outputs aggressively.", - "Keep verified facts separate from priorities and next steps." - ]; - } - - if (profile === "planner") { - return [ - ...common, - "Plan the smallest useful task set.", - "Avoid broad scans when existing artifacts or narrow scopes can answer the request." - ]; - } - - return common; -} - -export function applyTokenPolicy(request: AIRouterRequest): AIRouterRequest { - if (request.prompt.includes(TOKEN_POLICY_MARKER)) { - return request; - } - - const policy = policyForProfile(request.profile).join("\n"); - return { - ...request, - prompt: `${policy}\n\n${request.prompt.trim()}` - }; -} - -export function applyPresetPolicy(request: AIRouterRequest, preset: TokenPreset = "balanced"): AIRouterRequest { - if (preset === "balanced" || request.prompt.includes(PRESET_POLICY_MARKER)) { - return request; - } - - const presetInstruction = - preset === "cheap" - ? "Be maximally concise. Return only the top 3 findings. Omit evidence_refs longer than one path." - : "Be exhaustive. Include all evidence_refs. Do not truncate findings or unknowns."; - - return { - ...request, - prompt: `${PRESET_POLICY_MARKER}\n${presetInstruction}\n\n${request.prompt.trim()}` - }; -} - -export function tokenPolicyMarker(): string { - return TOKEN_POLICY_MARKER; -} diff --git a/core/workflow_registry/index.ts b/core/workflow_registry/index.ts deleted file mode 100644 index c84b72c..0000000 --- a/core/workflow_registry/index.ts +++ /dev/null @@ -1,343 +0,0 @@ -import path from "node:path"; - -import type { ProjectContext, ResumeStage } from "../../shared/types"; - -export type WorkflowId = - | "memory-brief" - | "start" - | "doctor" - | "map-codebase" - | "code-graph" - | "fact-query" - | "runbook" - | "harness-audit" - | "firewall" - | "swarm" - | "plan-improvements" - | "resume" - | "ask" - | "review-delta"; - -export interface WorkflowDefinition { - workflowId: WorkflowId; - resumeStage: ResumeStage; - resumePriority: number; - humanLabel: string; - commandLabel: string; - artifactLabels: string[]; - dependencies: WorkflowId[]; - cheap: boolean; - usesModel: boolean; - updatesMemory: boolean; - rationale: string; - priority: "high" | "medium" | "low"; -} - -export interface WorkflowRuntimeDefinition extends WorkflowDefinition { - artifactPaths: string[]; - command: string; -} - -function quoteArg(value: string): string { - return JSON.stringify(value); -} - -function outputFlag(context: ProjectContext): string { - return `--output ${quoteArg(context.outputPath)}`; -} - -function targetArg(context: ProjectContext): string { - return quoteArg(context.targetPath); -} - -export const WORKFLOW_DEFINITIONS: WorkflowDefinition[] = [ - { - workflowId: "memory-brief", - resumeStage: "map-codebase", - resumePriority: 5, - humanLabel: "Refresh memory brief", - commandLabel: "Refresh Memory Brief", - artifactLabels: ["Memory Brief", "Memory Brief JSON"], - dependencies: [], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Maintain the compact memory handoff before deeper analysis.", - priority: "high" - }, - { - workflowId: "start", - resumeStage: "start", - resumePriority: 11, - humanLabel: "Guided start", - commandLabel: "Run Guided Start", - artifactLabels: ["Start"], - dependencies: ["memory-brief"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Prepare memory, facts, runbook, harness audit, and firewall before model-heavy work.", - priority: "high" - }, - { - workflowId: "doctor", - resumeStage: "doctor", - resumePriority: 1, - humanLabel: "Check local readiness", - commandLabel: "Run Doctor", - artifactLabels: ["Doctor"], - dependencies: [], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Confirm local tooling and models before spending analysis time.", - priority: "high" - }, - { - workflowId: "map-codebase", - resumeStage: "map-codebase", - resumePriority: 2, - humanLabel: "Refresh codebase map", - commandLabel: "Generate Codebase Map", - artifactLabels: ["Codebase Map"], - dependencies: ["doctor"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Give humans and agents a stable structural overview.", - priority: "high" - }, - { - workflowId: "code-graph", - resumeStage: "map-codebase", - resumePriority: 5, - humanLabel: "Build factual graph", - commandLabel: "Build Repository Fact Graph", - artifactLabels: ["Repository Fact Graph", "Repository Fact Graph Report"], - dependencies: ["map-codebase"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Extract structural evidence before running model workers.", - priority: "high" - }, - { - workflowId: "fact-query", - resumeStage: "fact-query", - resumePriority: 6, - humanLabel: "Query factual memory", - commandLabel: "Query Factual Memory", - artifactLabels: ["Fact Query"], - dependencies: ["code-graph"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Select relevant facts and decisions for the requested work.", - priority: "medium" - }, - { - workflowId: "runbook", - resumeStage: "runbook", - resumePriority: 7, - humanLabel: "Create token-aware runbook", - commandLabel: "Create Token-Aware Runbook", - artifactLabels: ["Runbook"], - dependencies: ["fact-query"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Order deterministic memory, graph, query, governance, and swarm steps before expensive analysis.", - priority: "medium" - }, - { - workflowId: "harness-audit", - resumeStage: "harness-audit", - resumePriority: 8, - humanLabel: "Audit harness readiness", - commandLabel: "Audit Harness Readiness", - artifactLabels: ["Harness Audit"], - dependencies: ["runbook"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Check progressive memory, cost gates, and continuity before model-heavy analysis.", - priority: "medium" - }, - { - workflowId: "firewall", - resumeStage: "firewall", - resumePriority: 3, - humanLabel: "Inspect governance firewall", - commandLabel: "Inspect Firewall", - artifactLabels: ["Firewall"], - dependencies: ["harness-audit"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Confirm agent actions stay review-only and policy-safe.", - priority: "medium" - }, - { - workflowId: "swarm", - resumeStage: "swarm", - resumePriority: 9, - humanLabel: "Run bounded swarm", - commandLabel: "Run Cheap Swarm", - artifactLabels: ["Swarm"], - dependencies: ["firewall", "fact-query"], - cheap: false, - usesModel: true, - updatesMemory: true, - rationale: "Use model workers only after deterministic memory, graph, and governance context exist.", - priority: "high" - }, - { - workflowId: "plan-improvements", - resumeStage: "plan-improvements", - resumePriority: 10, - humanLabel: "Persist improvement plan", - commandLabel: "Build Improvement Plan", - artifactLabels: ["Improvement Plan"], - dependencies: ["swarm"], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Convert findings into durable direction instead of repeating analysis.", - priority: "medium" - }, - { - workflowId: "resume", - resumeStage: "bootstrap", - resumePriority: 0, - humanLabel: "Resume from current state", - commandLabel: "Resume Project", - artifactLabels: ["Resume"], - dependencies: [], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Show the next useful checkpoint from current artifacts.", - priority: "low" - }, - { - workflowId: "ask", - resumeStage: "ask", - resumePriority: 1, - humanLabel: "Ask brief", - commandLabel: "Resume Ask", - artifactLabels: ["Ask Brief"], - dependencies: [], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Continue from the latest ask brief.", - priority: "low" - }, - { - workflowId: "review-delta", - resumeStage: "review-delta", - resumePriority: 4, - humanLabel: "Review recent changes", - commandLabel: "Review Recent Changes", - artifactLabels: ["Impact Radius"], - dependencies: [], - cheap: true, - usesModel: false, - updatesMemory: true, - rationale: "Build a bounded review surface for recent git changes.", - priority: "low" - } -]; - -export function workflowCommand(context: ProjectContext, workflowId: WorkflowId, intent = "optimize analysis and cost"): string { - const output = outputFlag(context); - switch (workflowId) { - case "memory-brief": - return `project-brain status ${targetArg(context)} ${output}`; - case "start": - return `project-brain start ${quoteArg(intent)} ${targetArg(context)} ${output}`; - case "doctor": - return `project-brain doctor ${targetArg(context)} ${output}`; - case "map-codebase": - return `project-brain map-codebase ${targetArg(context)} ${output}`; - case "code-graph": - return `project-brain code-graph ${targetArg(context)} ${output}`; - case "fact-query": - return `project-brain fact-query ${quoteArg(`${context.repoName} ${intent}`)} ${targetArg(context)} ${output}`; - case "runbook": - return `project-brain runbook ${quoteArg(intent)} ${targetArg(context)} ${output}`; - case "harness-audit": - return `project-brain harness-audit ${targetArg(context)} ${output}`; - case "firewall": - return `project-brain firewall ${targetArg(context)} --trigger repository-change ${output}`; - case "swarm": - return `project-brain swarm ${quoteArg(intent)} ${targetArg(context)} ${output} --preset cheap`; - case "plan-improvements": - return `project-brain plan-improvements ${targetArg(context)} ${output}`; - case "resume": - return `project-brain resume ${targetArg(context)} ${output}`; - case "ask": - return `project-brain ask ${quoteArg(intent)} ${targetArg(context)} ${output}`; - case "review-delta": - return `project-brain review-delta ${targetArg(context)} ${output}`; - } -} - -export function workflowArtifactPaths(context: ProjectContext, workflowId: WorkflowId): string[] { - switch (workflowId) { - case "memory-brief": - return [ - path.join(context.memoryDir, "MEMORY_BRIEF.md"), - path.join(context.runtimeMemoryDir, "memory_brief", "memory_brief.json") - ]; - case "start": - return [path.join(context.reportsDir, "start.md")]; - case "doctor": - return [path.join(context.memoryDir, "doctor", "doctor.json")]; - case "map-codebase": - return [path.join(context.docsDir, "codebase_map", "SUMMARY.md")]; - case "code-graph": - return [ - path.join(context.runtimeMemoryDir, "knowledge_graph", "repository_fact_graph.json"), - path.join(context.reportsDir, "repository_fact_graph.md") - ]; - case "fact-query": - return [path.join(context.reportsDir, "fact_query.md")]; - case "runbook": - return [path.join(context.reportsDir, "runbook.md")]; - case "harness-audit": - return [path.join(context.reportsDir, "harness_audit.md")]; - case "firewall": - return [path.join(context.reportsDir, "agent_firewall.md")]; - case "swarm": - return [path.join(context.memoryDir, "swarm", "swarm_run.json")]; - case "plan-improvements": - return [path.join(context.docsDir, "improvement_plan", "SUMMARY.md")]; - case "resume": - return [path.join(context.reportsDir, "resume.md")]; - case "ask": - return [path.join(context.reportsDir, "ask_brief.md")]; - case "review-delta": - return [path.join(context.reportsDir, "impact_radius.md")]; - } -} - -export function buildWorkflowRuntimeDefinitions(context: ProjectContext, intent?: string): WorkflowRuntimeDefinition[] { - return WORKFLOW_DEFINITIONS.map((definition) => ({ - ...definition, - artifactPaths: workflowArtifactPaths(context, definition.workflowId), - command: workflowCommand(context, definition.workflowId, intent) - })); -} - -export function getWorkflowDefinition(workflowId: WorkflowId): WorkflowDefinition { - const definition = WORKFLOW_DEFINITIONS.find((item) => item.workflowId === workflowId); - if (!definition) { - throw new Error(`Unknown workflow: ${workflowId}`); - } - return definition; -} - -export function workflowForArtifactLabel(label: string): WorkflowDefinition | undefined { - return WORKFLOW_DEFINITIONS.find((definition) => definition.artifactLabels.includes(label)); -} diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 7788c82..0000000 --- a/docs/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Documentation Index - -This directory holds the long-form product, architecture, operational, and assessment material for `project-brain`. - -## Core docs - -- [AI Review Start Here](../AI_REVIEW_START_HERE.md) -- [Architecture](architecture.md) -- [Agents](agents.md) -- [Usage](usage.md) -- [External Repository Integration](external-repository-integration.md) -- [Agent Self-Governance](agent-self-governance.md) -- [Production Architecture Spec](production-architecture-spec.md) -- [Self-Improvement Framework](self-improvement-framework.md) -- [Product Blueprint](product-blueprint.md) -- [GitHub Hardening](github-hardening.md) -- [Reference Repo Analysis](reference-repo-analysis.md) - -## Assessments - -- [System Architecture Audit](assessments/system-architecture-audit.md) -- [Agent Model Analysis](assessments/agent-model-analysis.md) -- [Weak Points](assessments/weak-points.md) -- [Final Score](assessments/final-score.md) - -## Reference repo analysis - -- [Reference Repo Analysis](reference-repo-analysis.md) -- [Claude Mem Comparison](reference-repo-analysis/claude-mem-comparison.md) - -## Roadmap - -- [Evolution Plan](roadmap/evolution-plan.md) -- [Evolution Architecture V2](roadmap/evolution-architecture-v2.md) -- [Fact-Based Context Roadmap](roadmap/fact-based-context-roadmap.md) -- [Token-Aware Orchestration](roadmap/token-aware-orchestration.md) - -## Root-level project files - -These intentionally stay at the repository root because they are standard project entrypoints or community/legal files: - -- `README.md` -- `LICENSE` -- `CONTRIBUTING.md` -- `CODE_OF_CONDUCT.md` -- `SECURITY.md` -- `SUPPORT.md` -- `CITATION.cff` -- `ACKNOWLEDGEMENTS.md` diff --git a/docs/agent-self-governance.md b/docs/agent-self-governance.md deleted file mode 100644 index 405db58..0000000 --- a/docs/agent-self-governance.md +++ /dev/null @@ -1,258 +0,0 @@ -# Agent Self-Governance System - -## Scope - -This document describes the practical self-governance layer implemented for `project-brain`. - -It is built around these concrete modules: - -- `governance/self-governance-system.ts` -- `governance/agent-registry.ts` -- `governance/agent-council.ts` -- `governance/agent-supervisor.ts` -- `governance/agent-evaluator.ts` -- `governance/task-board.ts` -- `governance/message-center.ts` -- `memory/learnings/index.ts` - -The system never modifies production code automatically. Agents only analyze, propose, and report. - -## 1. Architecture Diagram - -```mermaid -flowchart TD - REPO["Repository Snapshot"] --> DISC["Discovery Engine"] - DISC --> CTX["Context Builder"] - CTX --> GOV["Agent Self-Governance System"] - - GOV --> REG["AgentRegistry"] - GOV --> COUNCIL["AgentCouncil"] - GOV --> SUP["AgentSupervisor"] - GOV --> EVAL["AgentEvaluator"] - GOV --> BOARD["AgentTaskBoard"] - GOV --> MSG["AgentMessageCenter"] - GOV --> LEARN["AgentLearningStore"] - GOV --> SCHED["AutonomousScheduler"] - - REG --> AGENTS["Governed Agents"] - AGENTS --> REPORTS["Reports"] - AGENTS --> PROPOSALS["proposal/"] - AGENTS --> TASKS["tasks/"] - AGENTS --> MEMORY["memory/learnings/"] - - SUP --> HUMAN["Human Approval"] - EVAL --> LEARN - LEARN --> COUNCIL -``` - -## 2. Directory Structure - -### Source code - -```text -project-brain/ - governance/ - agent-council.ts - agent-evaluator.ts - agent-registry.ts - agent-supervisor.ts - autonomous-scheduler.ts - message-center.ts - self-governance-system.ts - task-board.ts - memory/ - learnings/ - index.ts - agents/ - architecture_agent/ - dependency_agent/ - product_owner_agent/ - catalog.ts -``` - -### Runtime artifacts generated per analyzed repository - -```text -/ - AI_CONTEXT/ - reports/ - agent_activity_report.md - improvement_report.md - risk_report.md - tasks/ - backlog.json - active.json - completed.json - messages.json - memory/ - learnings/ - index.json - .json - proposal/ - improved_security_rules.md - improved_architecture_analysis.md - improved_.md -``` - -## 3. Runtime Flow - -1. Discovery scans the repository and builds normalized context. -2. `AgentSelfGovernanceSystem` loads previous learnings from `memory/learnings/`. -3. `AutonomousScheduler` selects agents based on the trigger. -4. `AgentCouncil` creates prioritized tasks. -5. `AgentTaskBoard` persists `NEW` tasks to `tasks/backlog.json`. -6. `AgentMessageCenter` sends `QUESTION` messages from `AgentCouncil` to each agent. -7. `AgentSupervisor` validates safety rules and monitors execution. -8. Each agent runs and emits a report. -9. `AgentEvaluator` scores the output quality and ranks proposals. -10. `AgentMessageCenter` records `ANALYSIS_RESULT`, `PROPOSAL`, `FEEDBACK`, or `ESCALATION` messages. -11. `AgentSelfGovernanceSystem` derives learning records and writes evolution proposals. -12. Reports are generated in `reports/` and the task board is updated. -13. Human feedback can later move tasks to `APPROVED`, `REJECTED`, or `ARCHIVED` using the CLI `feedback` command. - -## 4. Governance Rules - -The active guardrails are enforced in `governance/agent-supervisor.ts`: - -- agents cannot execute destructive operations -- agents cannot commit code -- agents cannot merge pull requests -- agents cannot deploy infrastructure -- agents can only analyze, propose, and report -- human approval is required for structural changes, architectural decisions, and security-sensitive proposals - -## 5. Task Lifecycle - -The shared task economy uses these states: - -- `NEW` -- `ANALYZING` -- `PROPOSED` -- `APPROVED` -- `REJECTED` -- `ARCHIVED` - -State model: - -```text -NEW -> ANALYZING -> PROPOSED -> APPROVED -NEW -> ANALYZING -> PROPOSED -> REJECTED -NEW -> ANALYZING -> PROPOSED -> ARCHIVED -ANALYZING -> REJECTED -``` - -Persistence rules: - -- `backlog.json` stores `NEW` -- `active.json` stores `ANALYZING` and `PROPOSED` -- `completed.json` stores `APPROVED`, `REJECTED`, and `ARCHIVED` - -## 6. Example Agent Interactions - -The message protocol is implemented through the `AGENT_MESSAGE` envelope in `shared/types.ts`. - -Example interaction: - -1. `AgentCouncil -> SecurityAgent` - `QUESTION`: run security review for `weekly-review` -2. `SecurityAgent -> AgentCouncil` - `ANALYSIS_RESULT`: high-risk secrets or lockfile findings -3. `AgentCouncil -> HumanApproval` - `ESCALATION`: security-sensitive proposal requires approval -4. `QAAgent -> ProductOwnerAgent` - `PROPOSAL`: test risk should influence backlog priority -5. `ArchitectureAgent -> DocumentationAgent` - `FEEDBACK`: architecture findings should update docs - -The live message log for a run is written to `tasks/messages.json`. - -## 7. Example Learning Record - -Example stored record shape: - -```json -{ - "lessonId": "lesson_qa-agent_1773125259140_7vit5r", - "agentId": "qa-agent", - "taskId": "task_qa-agent_1773125247883_2", - "context": "Weekly review validation", - "detectedProblem": "No automated tests detected", - "actionTaken": "Escalated smoke-test baseline proposal", - "outcome": "SUCCESSFUL_PROPOSAL", - "confidenceScore": 0.92, - "createdAt": "2026-03-10T06:47:39.140Z" -} -``` - -Stored in: - -- `memory/learnings/index.json` -- `memory/learnings/.json` - -## 8. Example Proposal - -Example evolution proposal generated by the self-governance runtime: - -```md -# Improved Security Rules - -## Source agent - -- Agent: security-agent -- Task score: 0.78 - -## Proposed refinement - -- Refine prompts and heuristics using the latest findings. -- Strengthen rules around secret leakage and lockfile coverage. -- Preserve safety constraints: analyze, propose, and report only. - -## Human approval required - -Yes. This proposal must be reviewed before activation. -``` - -Stored in: - -- `proposal/improved_security_rules.md` -- `proposal/improved_architecture_analysis.md` -- `proposal/improved_.md` - -## 9. Scheduling Model - -The implemented trigger model supports: - -- `manual` -- `repository-change` -- `weekly-review` -- `incident-detection` -- `dependency-update` -- `security-advisory` - -Default cycles in `governance/autonomous-scheduler.ts`: - -- daily: `security-agent`, `dependency-agent` -- weekly: `architecture-agent`, `optimization-agent`, `documentation-agent` - -## 10. CLI Integration - -Relevant commands: - -```bash -project-brain analyze /path/to/repo --trigger weekly-review -project-brain agents /path/to/repo --trigger security-advisory -project-brain feedback /path/to/repo --agent qa-agent --task --context "..." --problem "..." --action "..." --outcome SUCCESSFUL_PROPOSAL -``` - -## 11. Practical Outcome - -This layer turns `project-brain` from a simple multi-agent analyzer into a governed agent ecosystem that can: - -- register and supervise agents dynamically -- plan and prioritize work instead of running blindly -- communicate through structured messages -- persist learnings across runs -- generate self-improvement proposals for prompts, heuristics, and rules -- require human approval before any behavioral activation - -It remains non-destructive by design. diff --git a/docs/agents.md b/docs/agents.md deleted file mode 100644 index 6cb819b..0000000 --- a/docs/agents.md +++ /dev/null @@ -1,42 +0,0 @@ -# agents - -`project-brain` uses specialist agents with constrained responsibilities. - -## Core analysis agents - -- `QAAgent`: test gaps, bug risk, release confidence -- `UXAgent`: operational usability analysis -- `UXImprovementAgent`: implementation-task generation for UX changes -- `ArchitectureAgent`: boundaries, coupling, and structural risk -- `OptimizationAgent`: performance and efficiency review -- `DocumentationAgent`: documentation gaps and operational clarity -- `DevAgent`: review-only engineering task and patch proposal generation - -## Supporting agents - -- `ProductOwnerAgent` -- `SecurityAgent` -- `DependencyAgent` -- `ObservabilityAgent` -- `LegalAgent` - -## Agent safety contract - -All agents are constrained to: - -- analyze -- propose -- report - -They must not: - -- modify target repositories automatically -- deploy code -- push changes -- bypass human review - -## Prompt handling - -Runtime system prompts remain under `agents/prompts/` for compatibility with the current implementation. - -Reusable exported prompts for other repositories live under `prompts/context_templates/`. diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index e1f579b..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,45 +0,0 @@ -# project-brain architecture - -## Purpose - -`project-brain` is a repository intelligence engine. Its job is to understand software systems, persist analysis context, run specialist agents, and produce safe recommendations for humans or downstream coding agents. - -## Runtime flow - -```text -CLI -> Orchestrator -> Discovery -> Context Builder -> Agents -> Reports -> Patch Proposals -``` - -## Main modules - -- `cli/`: command entrypoints -- `core/`: orchestration, routing, discovery coordination, and runtime services -- `agents/`: specialist agents such as QA, UX, architecture, optimization, documentation, and development review -- `analysis/`: deterministic scanners and report builders -- `memory/`: AI context, learnings, and persisted cycle artifacts -- `tools/`: helper modules for patch proposal generation and repo-level operations -- `prompts/context_templates/`: reusable context prompts for external project work - -## Source layout decision - -The repository currently uses top-level runtime directories such as `agents/`, `core/`, `cli/`, and `memory/`. - -A physical move into `src/` was intentionally not performed in this baseline because it would require a functional refactor across imports, build configuration, and execution paths. The current layout remains compatible with the existing CLI and build pipeline. - -## Safety model - -`project-brain` is non-destructive by design. - -- target repositories are analyzed, not modified -- generated diffs are review-only -- human approval is required before implementation -- unsafe surfaces must stay blocked from automated proposals - -## External repository workflow - -For external repositories and similar systems, the normal flow is: - -1. run repository analysis -2. generate `AI_CONTEXT`, reports, and implementation tasks -3. export prompt-ready context for a downstream coding agent -4. review proposed diffs before any manual application diff --git a/docs/assessments/agent-model-analysis.md b/docs/assessments/agent-model-analysis.md deleted file mode 100644 index b2bdd97..0000000 --- a/docs/assessments/agent-model-analysis.md +++ /dev/null @@ -1,241 +0,0 @@ -# Agent Model Analysis - -## Executive judgment - -The agent model is modular at the class level, but only partially modular at the system level. - -A new agent can be added without rewriting the orchestrator itself. -A new agent cannot be added cleanly as a first-class citizen without touching multiple governance heuristics. - -That means the framework is extensible, but not cleanly pluggable. - -## How agents are defined - -Every agent extends `BaseAgent` and implements one method: - -- `evaluate(context: ProjectContext): Promise` - -The base class handles: - -- logging -- report rendering -- report file writing -- conversion from `AgentEvaluation` to `AgentReport` - -This is a good narrow contract. - -The problem is that the contract is also very weak: - -- agents receive only `ProjectContext` -- they do not receive prior learnings -- they do not receive other agent messages -- they do not receive task metadata beyond what can be inferred from the output files -- they do not expose structured actions, tools, or plans - -So the framework supports interchangeable report producers, not autonomous specialists with memory and behavior policies. - -## Agent catalog and registration - -Registration is static. - -`buildAgentCatalog()` constructs the catalog in code and `AgentSelfGovernanceSystem` registers all entries in its constructor. - -This provides: - -- deterministic startup -- explicit metadata per agent -- trigger routing based on descriptors - -But it also means: - -- no dynamic agent loading -- no manifest-based plugin discovery -- no runtime capability negotiation -- no hot-swapping of agent versions -- no per-repository agent configuration - -The registry itself is simple and clean. The surrounding system is not dynamic. - -## Actual agent behavior - -### Most agents - -Most agents are thin heuristics over `context.discovery`. - -Examples: - -- `QAAgent` checks testing counts and ratios -- `SecurityAgent` checks risky filenames, lockfiles, and `.dockerignore` -- `ObservabilityAgent` checks whether logging, metrics, and alerts were detected -- `LegalAgent` checks for license and notice files -- `ArchitectureAgent` looks at broad structural signals - -These agents are lightweight and easy to understand, but they are not deeply analytical. They do not inspect code semantics, runtime traces, issue trackers, or change history beyond the static discovery snapshot. - -### DevAgent - -`DevAgent` is materially different. It uses local tooling and file snapshots to build module metrics, coupling analysis, duplication signals, unused exports, missing logging, missing error handling, and architecture risk proposals. - -This is the only agent that feels structurally closer to an engineering assistant instead of a checklist rule set. - -### DocumentationAgent - -`DocumentationAgent` is also special because it writes generated docs to `docs/architecture.md`, `docs/api.md`, and `docs/runbook.md` in addition to its report. - -## Are agents independent? - -Not really. - -They are independent in the narrow sense that each can run by itself against the same `ProjectContext`. - -They are not independent in the stronger architectural sense because: - -- they all depend on the same discovery snapshot -- they do not own isolated memory -- they do not manage their own tools or plans -- they do not participate in a real event bus -- they do not consume other agents' outputs before producing their own results -- their follow-up routing is hardcoded centrally, not emergent - -This is closer to a fan-out report stage than to autonomous cooperating agents. - -## Do agents communicate? - -Only cosmetically. - -`AgentMessageCenter` creates structured messages and persists them to `tasks/messages.json`, but no agent ever consumes that message stream inside the same cycle. - -What happens in practice: - -- `AgentCouncil` seeds assignment messages -- each agent run generates an `ANALYSIS_RESULT` -- a few hardcoded follow-up messages are emitted for specific agent/risk combinations -- messages are persisted for audit purposes - -What does not happen: - -- no agent reads inbound messages before acting -- no agent changes its behavior based on those messages -- no negotiation, delegation, or retry occurs -- no downstream agent is re-run after receiving a follow-up - -So communication exists as logging, not coordination. - -## Can new agents be added without rewriting the orchestrator? - -### Narrow answer - -Yes. - -If you create a new `BaseAgent` subclass and add it to `buildAgentCatalog()`, the orchestrator will run it through the registry-driven governance path without changes to `ProjectBrainOrchestrator`. - -### Real answer - -Only partially. - -To integrate a new agent cleanly, you will usually also need to touch: - -- `governance/agent-council.ts` for priority mapping -- `governance/autonomous-scheduler.ts` for documented cycles -- `governance/message-center.ts` for follow-up logic -- `governance/self-governance-system.ts` helper functions such as `defaultAffectedFiles()` and `expectedBenefitFor()` -- maybe proposal safety heuristics if the new agent works in a sensitive area - -That is hidden coupling. - -So the orchestrator is not the problem. The governance layer is where extensibility becomes brittle. - -## Tight-coupling signals - -The codebase contains several architecture drift signals around agents: - -- `ChiefAgent` still exists as a separate orchestration abstraction, but the runtime path does not use it. -- `ProductAgent` exists as a full class, but the actual catalog registers `ProductOwnerAgent` instead. -- `ProductAgent` and `ProductOwnerAgent` currently duplicate the same logic almost verbatim. -- agent behavior assumptions are embedded in hardcoded agent IDs across governance helpers. - -Those are signs that the abstraction boundary has already drifted from the implementation. - -## Can agents evolve? - -Not in the implemented system. - -The framework does not provide: - -- prompt versioning -- tool policy versioning -- agent configuration storage -- agent experiment runs -- outcome-linked parameter tuning -- automatic replacement of heuristics -- model selection or model routing - -The so-called self-governance layer produces repository improvement proposals, not agent-improvement proposals. - -That distinction matters. - -The system can say: - -- add tests -- improve logging -- write docs -- refactor a hotspot - -It cannot say and then apply: - -- QAAgent should change its detection heuristic because last 30 approvals showed false positives -- SecurityAgent version 1.1 outperformed 1.0 on validated secrets findings, promote it -- DevAgent should use a different prompt, metric threshold, or toolchain for Java repositories - -Without that loop, agents do not evolve. - -## Scalability of the agent model - -### What scales reasonably well - -- adding more simple report-style agents -- running a fixed set of agents against small to medium repositories -- generating more artifact types from the same discovery snapshot - -### What does not scale well - -- large agent catalogs with differentiated policies -- agents with different memory needs -- agents that require real coordination -- iterative plan/execute/validate loops -- cross-agent negotiation -- heterogeneous execution backends -- cross-repository adaptive specialization - -The current design will become governance-heavy and repetitive before it becomes agent-rich. - -## Autonomy potential of the current agent model - -Current autonomy level: - -- autonomous detection: partial -- autonomous prioritization: weak -- autonomous proposal generation: partial -- autonomous execution: none -- autonomous validation: none -- autonomous learning: weak -- autonomous self-modification: none - -The model is useful as an analysis swarm. It is not yet a self-improving engineering workforce. - -## Bottom line - -The agent layer is good enough for a v1 analysis framework. - -It is not yet architected for: - -- independent agent cognition -- robust inter-agent collaboration -- durable adaptive behavior -- scalable plugin growth -- agent self-evolution - -The design choice that limits it most is this: agents are treated as functions from `ProjectContext` to markdown reports, while all meaningful orchestration intelligence remains centralized and hardcoded. - -That keeps v1 simple. -It also caps v1 far below a true autonomous improvement engine. diff --git a/docs/assessments/final-score.md b/docs/assessments/final-score.md deleted file mode 100644 index f1c0f4c..0000000 --- a/docs/assessments/final-score.md +++ /dev/null @@ -1,73 +0,0 @@ -# Final Score - -## Scoring scale - -- `1` = fundamentally broken for the category -- `5` = workable but clearly limited -- `10` = production-grade and strategically strong - -## Scores - -### Architecture: 5/10 - -Why: - -The core pipeline is coherent and the codebase has a real structure. But the implementation has already drifted from its own abstractions, the main governance runtime is too overloaded, and there is a verified state-isolation bug in cycle execution records. - -### Modularity: 4/10 - -Why: - -The `BaseAgent` contract and static catalog are decent. But real extensibility is weakened by hardcoded agent identities throughout governance, duplicated agent logic, and vestigial orchestration classes. - -### Autonomy Potential: 3/10 - -Why: - -The system can detect issues and generate proposals. It cannot autonomously apply fixes, validate them, learn from validated outcomes, or evolve its own agents. That is far below a true autonomous improvement engine. - -### Safety: 7/10 - -Why: - -The current runtime is non-destructive by design and routes risky proposals toward human review. That said, safety is achieved mostly by not executing changes at all. Governance is metadata-based, not enforced by a real execution sandbox or policy engine. - -### Scalability: 3/10 - -Why: - -The design does not scale well to large repositories, monorepos, large agent catalogs, or continuous autonomous execution. Discovery is capped, workspace handling is shallow, telemetry isolation is weak, and the execution model remains single-pass and centrally hardcoded. - -## Brutally honest summary - -As implemented today, `project-brain` is a promising repository analysis framework with governance-themed packaging. - -It is not yet a real autonomous engineering system. - -Best description: - -- good v1 analyzer -- weak v1 agent platform -- not yet a self-improving engine - -## Overall verdict - -If the question is "Is this already a true autonomous improvement engine?" - -Answer: no. - -If the question is "Is there enough structure here to evolve into one with serious architectural work?" - -Answer: yes, but only if v2 replaces the current report-centric control model with: - -- structured memory -- real eventing -- sandboxed execution -- validation loops -- outcome-based learning -- policy-enforced autonomy -- agent version evolution - -## Final one-line assessment - -Current state: `project-brain` is much closer to a governed static-analysis and reporting pipeline than to an autonomous software improvement platform. diff --git a/docs/assessments/system-architecture-audit.md b/docs/assessments/system-architecture-audit.md deleted file mode 100644 index ea86a46..0000000 --- a/docs/assessments/system-architecture-audit.md +++ /dev/null @@ -1,362 +0,0 @@ -# System Architecture Audit - -## Executive verdict - -The implemented system is not a true autonomous agent platform. It is a non-destructive repository analysis pipeline with a governed agent catalog layered on top. - -The real runtime shape is: - -CLI -> `ProjectBrainOrchestrator` -> `DiscoveryEngine` -> `ContextBuilder` -> `AgentSelfGovernanceSystem` -> reports, proposals, task files, learnings, telemetry. - -That is a batch pipeline, not an adaptive autonomous improvement engine. - -## What the system actually is - -The implementation follows a staged pipeline pattern with artifact persistence: - -1. The CLI creates a single `ProjectBrainOrchestrator` instance and routes `init`, `analyze`, `agents`, `weekly`, `report`, and `feedback` into it. -2. The orchestrator runs repository discovery and builds a `ProjectContext` rooted in generated filesystem output. -3. The governance runtime selects agents from a static catalog, creates one task per selected agent, runs them, scores their outputs, generates proposal markdown, persists task/message/learning files, and writes summary reports. -4. Telemetry and runtime observability are written as JSON and markdown artifacts. - -This is materially different from the documented story that a `ChiefAgent` coordinates the system. The code path used by the CLI goes directly into `AgentSelfGovernanceSystem`; the `ChiefAgent` class exists but is not part of the actual runtime path. - -## Real design pattern - -The implemented design is best described as: - -- batch analysis pipeline -- static plugin catalog -- heuristic scanner suite -- governance wrapper around report generation -- filesystem-backed memory and telemetry - -It is not: - -- a blackboard multi-agent system -- an event-driven autonomous controller -- a continuously scheduled improvement daemon -- a self-modifying agent runtime - -## Runtime flow - -### 1. CLI entrypoints - -`cli/project-brain.ts` is the only real entry surface. The CLI resolves the target path, output path, and trigger, then forwards to the orchestrator. - -Important observations: - -- The CLI is thin and mostly correct. -- The `feedback` command is the only human-in-the-loop mutation path. -- Trigger handling is not fully faithful: `security-advisory` is aliased to `security-audit`, so the dedicated advisory trigger cannot actually be invoked from the CLI. - -### 2. Central orchestrator - -`core/orchestrator/main.ts` is the real control plane. - -Responsibilities: - -- create cycle IDs and log context -- run discovery -- build persistent project context -- invoke self-governance -- append memory artifacts -- append learning artifacts -- write weekly and risk reports -- emit telemetry and runtime observability -- aggregate workspace results into ecosystem artifacts - -There is exactly one central orchestrator. All execution funnels through it. - -### 3. Discovery and context build - -`DiscoveryEngine` composes several scanners: - -- repository structure scanner -- dependency scanner -- API scanner -- infrastructure scanner -- CI detector -- git detector -- logging detector -- metrics detector - -`ContextBuilder` then turns discovery output into a `ProjectContext` plus generated directories: - -- `AI_CONTEXT/` -- `reports/` -- `docs/` -- `memory/learnings/` -- `tasks/` -- `docs/proposals/` - -The context object passed to agents is mostly a repository snapshot plus output directories. It does not contain prior learnings, scored proposals, or any live inter-agent state. - -### 4. Governance runtime - -`AgentSelfGovernanceSystem` is the actual execution engine. - -Its cycle is: - -1. register all agents from a static catalog -2. load prior learning records from `memory/learnings/index.json` -3. select agents for the trigger -4. plan one task per agent -5. persist backlog files and seed synthetic messages -6. run each agent sequentially -7. score outputs -8. classify proposals -9. derive synthetic learning records -10. persist task/message/learning/report artifacts - -This is a governance shell around a report pipeline, not a real agent society. - -### 5. Workspace mode - -Workspace handling exists, but it is narrower than it first appears. - -The orchestrator calls `discoverRepositoryTargets()`. If the root path itself looks like a repository, the system immediately treats it as a single target. That means a normal monorepo root with `.git` or `package.json` is not expanded into package-level analysis. Workspace mode only activates when the root directory is a container of sibling repositories. - -That is ecosystem discovery, not true monorepo discovery. - -## Agent architecture in practice - -Agents are implemented as subclasses of `BaseAgent`. - -What they share: - -- one `evaluate(context)` contract -- one markdown report output contract -- access to the same `ProjectContext` - -What they do not have: - -- independent memory -- event subscriptions -- message consumption -- tool planning -- model-backed reasoning -- mutation rights - -Most agents are simple rule sets over `context.discovery`. `DevAgent` is the exception: it runs dependency-cruiser, ts-prune, ESLint, snapshot analysis, and git-history heuristics to produce a deeper architecture report. - -So the system is not “many intelligent agents”. It is “many report generators” plus one stronger static-analysis agent. - -## Memory architecture - -There are two very different memory layers. - -### AI context memory - -`memory/context_store/index.ts` writes markdown summaries such as: - -- `PROJECT_MODEL.md` -- `ARCHITECTURE_MAP.md` -- `API_MAP.md` -- `DEPENDENCY_GRAPH.md` -- `STACK_PROFILE.md` -- `ARCHITECTURE.md` -- `CONTEXT.md` -- `TASKS.md` -- `LEARNINGS.md` -- `ERRORS.md` - -This is mostly durable documentation plus append-only logs. - -### Learning memory - -`memory/learnings/index.ts` stores JSON `LearningRecord` objects with: - -- `agentId` -- `taskId` -- `context` -- `detectedProblem` -- `actionTaken` -- `outcome` -- `confidenceScore` -- timestamps - -This is the only structured learning store in the system. - -### What this memory does not do - -It does not: - -- feed directly into agent evaluation logic -- alter prompts or heuristics -- support semantic retrieval -- connect outcomes to patches or diffs -- maintain a causal model of what changed and whether it worked - -The only implemented behavioral feedback is task prioritization: if prior learnings contain `MISSED_ISSUE` or `FALSE_POSITIVE`, the council can boost an agent from `normal` to `high` priority. - -That is weak operational memory, not learning. - -## Learning behavior - -The project claims learning, but most records are synthetic. - -`deriveLearnings()` generates records automatically from the current cycle using report findings and evaluator scores. In practice this means: - -- architecture findings create `ARCHITECTURAL_INSIGHT` -- repeated strings create `REPEATED_BUG_PATTERN` -- scores below 0.7 become `MISSED_ISSUE` -- scores at or above 0.7 become `PENDING_REVIEW` - -Those outcomes are inferred from internal heuristics, not from validated external results. - -Real outcome learning only happens when a human later runs the `feedback` CLI command. - -So the architecture has a feedback input, but not a closed learning loop. - -## Governance model - -The governance layer is conservative in intent. - -Implemented protections: - -- agents are declared as `analyze`, `propose`, `report` only -- proposals touching sensitive keywords or high risk are routed to `REQUIRES_HUMAN_REVIEW` -- proposal files explicitly state that no code is modified automatically -- feedback can move tasks into `APPROVED`, `REJECTED`, or `ARCHIVED` - -Limits of those protections: - -- safety is enforced at descriptor level, not through sandboxing or syscall/file policy -- keyword matching is the main approval heuristic -- the system never executes code changes anyway, so governance mostly classifies text artifacts -- there is no real approval workflow beyond file status and manual CLI feedback - -This is safe as a recommendation engine, but not yet a governable autonomous executor. - -## Observability and telemetry - -The system does have observability artifacts: - -- structured JSON log events when `--verbose` is enabled -- per-cycle telemetry JSON files -- a generated `runtime_observability.md` -- execution records in governance summaries - -But the implementation is coarse: - -- no persisted structured logs unless verbose mode is used interactively -- no per-agent latency breakdowns in telemetry -- no durable failure dashboard -- no tracing, queue metrics, or cycle retry accounting -- risk types are just `high`, `medium`, `low`, not actual categories - -There is also a concrete state leak: `AgentSupervisor` keeps execution records on an instance field, and `AgentSelfGovernanceSystem` reuses the same supervisor across runs. Because the orchestrator owns a long-lived governance instance, execution records accumulate between cycles. I verified this by running the same built orchestrator twice against the same fixture: the first run reported 4 execution records and the second reported 8, even though both cycles executed only 4 agents. - -That directly weakens telemetry correctness. - -## Repository discovery and analysis quality - -The discovery stack is practical but mostly heuristic. - -Strengths: - -- stack-portable manifest parsing across multiple ecosystems -- useful basic signals for CI, APIs, infra, logging, and metrics -- stronger static-analysis path inside `DevAgent` -- ecosystem-level aggregation for sibling repositories - -Hard limits: - -- file walking stops at 8000 files -- workspace discovery only checks immediate child directories -- monorepo roots are treated as single repositories if they contain `.git` or a manifest -- API, logging, and metrics detection are dependency-name and filename heuristics -- there is no AST-level cross-language semantic model except the TypeScript/JavaScript-heavy DevAgent path - -This is enough for broad repository scanning. It is not enough for trustworthy autonomous improvement decisions at scale. - -## Does the current architecture support autonomous evolution? - -Short answer: no. - -It supports: - -- detecting issues -- generating recommendations -- scoring agent outputs -- writing proposal artifacts -- storing basic historical records - -It does not support: - -- modifying repository code autonomously -- testing proposed fixes automatically -- learning from execution outcomes automatically -- revising agent logic or prompts automatically -- promoting successful strategies into new agent behavior -- continuous multi-cycle planning until a goal is satisfied - -The phrase “autonomous improvement engine” does not match the implemented behavior. - -## CI/CD readiness - -For a development environment, the current implementation is reasonably safe to run continuously as an analyzer because it is non-destructive to source code. - -Local validation during this audit: - -- `npm run build`: passed -- `npm run typecheck`: passed -- `npm test`: passed, including smoke and integration coverage - -Operational caveats: - -- output is written into the target directory by default, so it still mutates the repository tree with generated artifacts -- proposal directories are cleared and regenerated each cycle, so historical proposal tracking is not durable -- task board files are overwritten with current-cycle state, so this is not a persistent backlog engine -- telemetry can be inflated by leaked execution records across repeated runs - -## Bottom line - -`project-brain` v1 is a competent governed repository analysis pipeline. - -It is not yet an autonomous engineering system. - -The strongest implemented ideas are: - -- clean end-to-end orchestration -- practical multi-language repository scanning -- non-destructive governance posture -- useful filesystem-based artifacts -- a meaningful DevAgent static-analysis path - -The weakest architectural truths are: - -- no closed execution loop -- no real learning loop -- no real inter-agent collaboration -- no true scheduler -- no agent self-evolution -- partial modularity only -- state leakage across cycles - -## Evidence anchors - -Primary implementation files reviewed: - -- `cli/project-brain.ts` -- `core/orchestrator/main.ts` -- `core/context_builder/index.ts` -- `core/discovery_engine/index.ts` -- `analysis/workspace_discovery/index.ts` -- `analysis/repo_scanner/index.ts` -- `analysis/dependency_scanner/index.ts` -- `analysis/api_scanner/index.ts` -- `analysis/metrics/metrics_collector.ts` -- `agents/base-agent.ts` -- `agents/catalog.ts` -- `agents/*/index.ts` -- `governance/self-governance-system.ts` -- `governance/agent-registry.ts` -- `governance/agent-council.ts` -- `governance/agent-supervisor.ts` -- `governance/message-center.ts` -- `governance/task-board.ts` -- `memory/context_store/index.ts` -- `memory/learnings/index.ts` -- `memory/learning_store/index.ts` diff --git a/docs/assessments/weak-points.md b/docs/assessments/weak-points.md deleted file mode 100644 index 7a70034..0000000 --- a/docs/assessments/weak-points.md +++ /dev/null @@ -1,164 +0,0 @@ -# Weak Points - -## 1. There is no real learning loop - -The system stores learning records, but agents do not consume them during evaluation. - -What actually happens: - -- previous learnings are loaded by governance -- the council can boost task priority for agents associated with `MISSED_ISSUE` or `FALSE_POSITIVE` -- agents still run against the same `ProjectContext` only -- no heuristic, threshold, prompt, or tool policy changes as a result of those learnings - -Consequence: - -The architecture remembers outcomes, but it does not improve decision quality from them. - -## 2. There is no real autonomous scheduler - -`AutonomousScheduler` selects agents for a trigger, but it does not schedule future runs. `describeCycles()` is descriptive metadata, not an active scheduling engine. `WeeklyScheduler` only formats a time window and a suggested next run timestamp. - -Consequence: - -The system supports repeated manual or externally triggered cycles, not autonomous recurring operation. - -## 3. The multi-agent story is overstated - -Messages are written, not consumed. Agents do not negotiate, delegate, re-plan, or react to each other during a cycle. - -Consequence: - -The framework behaves like centralized fan-out/fan-in orchestration, not like a collaborative agent network. - -## 4. Governance state leaks across cycles - -`AgentSupervisor` stores execution records on an instance field. `AgentSelfGovernanceSystem` keeps one supervisor instance. `ProjectBrainOrchestrator` keeps one self-governance instance. - -Consequence: - -Execution records accumulate across runs, which corrupts per-cycle metrics and workspace summaries. This is not theoretical; it reproduces in runtime. - -## 5. Proposal history is not durable - -Proposal directories are cleared and rewritten during initialization and proposal generation. - -Consequence: - -The system does not maintain a trustworthy historical chain of proposals across cycles. It keeps the latest proposal set, not a proposal ledger. - -## 6. Task history is also shallow - -The task board persists only the current task snapshot into `backlog.json`, `active.json`, and `completed.json`. Each run replaces those files with the latest state set. - -Consequence: - -This is not a real backlog engine. Longitudinal planning and auditing are weak. - -## 7. Workspace discovery is not monorepo-aware - -If the root path itself looks like a repository, the system immediately stops and analyzes it as one target. Workspace mode only activates for directories that contain sibling repositories. - -Consequence: - -Typical monorepos are treated as single repositories rather than ecosystems of packages or services. - -## 8. Repository scanning is capped and heuristic-heavy - -The file walker stops at 8000 files. API, logging, metrics, and much of the stack detection are inferred from filenames and dependency names. - -Consequence: - -Large repositories can be partially scanned without a hard warning, and many findings are only as good as the naming conventions in the repo. - -## 9. The architecture is only partially modular - -The base agent abstraction is clean, but the governance layer hardcodes agent identities in: - -- priority mapping -- documented schedule descriptions -- follow-up routing -- affected-file defaults -- expected-benefit templates - -Consequence: - -Adding a new agent without changing orchestrator code is possible, but integrating it properly still requires editing multiple governance modules. - -## 10. Documentation and runtime have drifted apart - -Examples: - -- docs and README still present `ChiefAgent` as the coordinator, but the runtime path uses `AgentSelfGovernanceSystem` -- `ProductAgent` exists, but the live catalog uses `ProductOwnerAgent` -- docs describe self-improvement proposal artifacts that are not what the code actually writes -- README points to a CI workflow path that does not match the actual workflow filename - -Consequence: - -Architectural understanding from documentation is unreliable unless verified against the code. - -## 11. Guardrails are declarative, not enforceable runtime policy - -The supervisor verifies allowed action labels in agent descriptors and classifies proposals using keyword heuristics. - -It does not provide: - -- sandboxed execution -- file write restrictions -- branch isolation -- patch validation pipelines -- policy-as-code enforcement beyond string matching - -Consequence: - -The system is safe today mainly because it does not try to execute changes, not because the governance layer can safely supervise execution. - -## 12. Learning outcomes are mostly self-authored - -`deriveLearnings()` infers outcomes like `MISSED_ISSUE` and `PENDING_REVIEW` from internal scoring rather than real-world results. - -Consequence: - -The learning store can accumulate noisy self-judgments that look authoritative but are not grounded in validated outcomes. - -## 13. There is no code-change loop - -The system never moves from: - -issue detection -> patch proposal -> patch generation -> validation -> rollout -> post-change learning - -It stops at report and proposal generation. - -Consequence: - -Calling it an autonomous improvement engine is premature. It is currently an autonomous analysis and recommendation engine. - -## 14. Observability is present but shallow - -Telemetry tracks cycle duration, counts, and proposal statuses. Structured logs are only emitted in verbose mode. There is no durable log sink, retry telemetry, queue depth, agent runtime breakdown, or failure trend tracking. - -Consequence: - -The system can be monitored at a coarse level, but diagnosing agent quality drift or orchestration bottlenecks will be hard at scale. - -## 15. Trigger support is inconsistent - -The CLI converts `security-advisory` into `security-audit`. - -Consequence: - -The public API exposes more trigger nuance than the runtime actually honors. - -## Summary verdict - -The biggest structural weaknesses are not cosmetic. They are foundational: - -- memory does not drive behavior -- governance does not supervise execution, only text artifacts -- orchestration is single-pass and centrally hardcoded -- agent collaboration is mostly simulated -- state isolation is weak -- scaling model is shallow for monorepos and large repositories - -That combination is why the system cannot yet scale into a true autonomous improvement engine. diff --git a/docs/backlog-commercial-hardening.md b/docs/backlog-commercial-hardening.md deleted file mode 100644 index 0a0d001..0000000 --- a/docs/backlog-commercial-hardening.md +++ /dev/null @@ -1,31 +0,0 @@ -# Commercial hardening backlog - -## P0 - before public beta - -- CI remote green on Node 20 and Node 22. -- Dependabot has no high/moderate fixable alerts. -- Installation path is clear and tested from a clean clone. -- Validation matrix covers 3-5 representative project types. -- Output contract is documented. -- Non-technical user test is completed with at least one external user. - -## P1 - before broad use or paid pilots - -- Optional local telemetry or metrics for command duration, cache hits, and model calls. -- Better error messages for missing models, invalid paths, and stale memory. -- More real fixtures for backend, frontend, mobile, and monorepo cases. -- Compatibility validated on macOS, Linux, and Windows. -- Advanced docs for provider routing, model profiles, and output consumption. -- Isolated sandbox for validation runs. -- Exportable report bundle. - -## P2 - commercial maturity - -- Higher-polish interactive onboarding. -- Plugin/skill marketplace. -- Visual report dashboard. -- Enterprise policy packs. -- Multi-user/team collaboration. -- GitHub/GitLab native integrations. -- Signed releases and provenance. -- Commercial support process. diff --git a/docs/external-repository-integration.md b/docs/external-repository-integration.md deleted file mode 100644 index 4a8ef31..0000000 --- a/docs/external-repository-integration.md +++ /dev/null @@ -1,44 +0,0 @@ -# External repository integration - -`project-brain` is designed to work as an external intelligence layer for repositories it does not own. - -## Primary use cases - -- generate analysis prompts for coding agents -- generate implementation backlog from real repository structure -- generate review-only patch proposals -- build durable context before any manual code change - -## What it should do - -- analyze frontend, backend, and workspace repositories -- produce `AI_CONTEXT` artifacts -- generate UX, architecture, QA, and optimization reports -- generate task backlogs and review-only diffs - -## What it must not do - -- modify target code automatically -- change production logic without explicit human direction -- push code to remote repositories -- bypass review or validation - -## Recommended workflow - -1. Run `project-brain analyze` against the target repository. -2. Review `BRAIN/AI_CONTEXT`, `BRAIN/reports`, and `BRAIN/tasks` unless you passed a custom `--output`. -3. Select the relevant prompt template from `prompts/context_templates/`. -4. Provide the generated context plus the template to the downstream coding agent. -5. Review any patch proposals manually before implementation. - -By default, external runs write generated output under `BRAIN/` in the target repo. This keeps project-brain artifacts distinguishable from the application source while preserving the same internal layout (`AI_CONTEXT/`, `memory/`, `reports/`, `docs/`, `tasks/`). - -## Common artifacts - -- `UX_IMPLEMENTATION_TASKS.md` -- `NAVIGATION_RESTRUCTURE.md` -- `FORM_SIMPLIFICATION_TASKS.md` -- `WORKSPACE_IMPROVEMENTS.md` -- `patch_proposals/*.diff` - -These artifacts are intended to accelerate human-reviewed implementation work, not to replace it. diff --git a/docs/first-analysis-5-min.md b/docs/first-analysis-5-min.md deleted file mode 100644 index 3f7db1e..0000000 --- a/docs/first-analysis-5-min.md +++ /dev/null @@ -1,56 +0,0 @@ -# First analysis in 5 minutes - -This path is for a user who only wants to understand a repository safely. - -## 1. Install and build - -```bash -cd /path/to/project-brain -npm ci -npm run build -npm link -``` - -## 2. Run the guided entry point - -```bash -project-brain go "understand this project and suggest the next safe step" /path/to/target-repo -``` - -Default output goes to `/path/to/target-repo/BRAIN/`. Use `--output /tmp/project-brain-first-run` if you want the artifacts outside the repository. - -## 3. Read the executive summary - -Open: - -```text -/path/to/target-repo/BRAIN/AI_CONTEXT/EXECUTIVE_SUMMARY.md -``` - -## 4. Ask a factual question without spending model tokens - -```bash -project-brain fact-query "what frameworks does this repo use" /path/to/target-repo -``` - -Read: - -```text -/path/to/target-repo/BRAIN/reports/fact_query.md -``` - -## 5. Continue from the latest checkpoint - -```bash -project-brain resume /path/to/target-repo -``` - -## 6. Optional review-only swarm - -Start cheap. Move to balanced only when the summary shows unresolved areas. - -```bash -project-brain swarm "review the main risks without modifying files" /path/to/target-repo --preset cheap -``` - -`project-brain` is review-only by default: it writes analysis artifacts to `BRAIN/` or the selected output directory and does not modify application source files. diff --git a/docs/github-hardening.md b/docs/github-hardening.md deleted file mode 100644 index 3c11b61..0000000 --- a/docs/github-hardening.md +++ /dev/null @@ -1,70 +0,0 @@ -# GitHub hardening - -`project-brain` can be published openly now, but the repository settings still need a final hardening pass in GitHub. - -## What is already committed in-repo - -- `CODEOWNERS` for sensitive paths -- local git hooks for commit, push, and commit-message gates -- CI quality gates -- dependency review workflow -- security baseline workflow -- Dependabot config - -## GitHub settings to enable manually - -Apply these rules to the `main` branch: - -1. Require a pull request before merging. -2. Require at least one approval. -3. Require review from code owners. -4. Dismiss stale approvals when new commits are pushed. -5. Require conversation resolution before merge. -6. Require these status checks: - - `quality-gates` - - `dependency-review` - - `security-baseline` -7. Block force pushes. -8. Block branch deletion. - -If a repository clone stays private on a limited GitHub plan, branch protection may be unavailable. -In that case, either make the repository public before opening contributions or upgrade the plan that owns the repository. - -Recommended repository-wide settings: - -1. Enable private vulnerability reporting. -2. Enable Dependabot alerts and Dependabot security updates. -3. Enable secret scanning and push protection if your GitHub plan supports them. -4. Require approval for first-time workflow runs from forks. -5. Keep GitHub Actions permissions at the lowest level that still lets CI pass. -6. Prefer squash merge so history stays reviewable. - -On limited private plans, secret scanning may stay unavailable until the repository is public or the plan is upgraded. - -## Local contributor flow - -After cloning: - -```bash -npm install -npm run hooks:install -npm run verify -``` - -## What the local gates block - -- weak commit messages like `wip` or `tmp` -- accidental secrets in staged changes -- generated or local-only paths like `dist/`, `sample-output/`, `pb-output/`, and `.env*` -- pushes that fail lint, typecheck, or build - -## Limits - -No committed file can fully enforce: - -- branch protection -- repository permissions -- secret scanning policy -- who can merge or bypass checks - -Those still have to be turned on in GitHub settings by a repository admin. diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index c59b3a4..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,89 +0,0 @@ -# Installation - -## Requirements - -- Node.js `>=20` -- npm -- Git -- macOS or Linux for the current beta validation path -- Ollama optional for local model-heavy analysis -- Cloud/API provider optional for planner or advanced model-heavy workflows - -## Install from source - -```bash -git clone -cd project-brain -npm ci -npm run build -``` - -Run the CLI from source: - -```bash -node dist/cli/project-brain.js --help -``` - -Optionally link it as a local command: - -```bash -npm link -project-brain --help -``` - -## Validate the install - -```bash -npm run lint -npm test -npm run build -npm audit --audit-level=high -project-brain --help -brain --help -project-brain go --help -``` - -## Mode without external models - -Deterministic workflows still work without Ollama or cloud/API access: - -- `go` -- `status` -- `resume` -- `runbook` -- `fact-query` -- `code-graph` -- `harness-audit` -- `doctor` - -These commands use repository scanning, generated memory, fact graph, and preflight facts before any model-heavy path. - -## Mode with Ollama - -Check whether Ollama is available: - -```bash -ollama list -project-brain models -``` - -Typical local models are configured by role. If a model is missing, model-heavy commands should degrade with a clear error or fallback rather than breaking deterministic commands. - -Use memory-first commands before model-heavy swarm to reduce cost: - -```bash -project-brain go "understand this project" /path/to/repo --output /path/to/output -project-brain fact-query "what framework does this repo use" /path/to/repo --output /path/to/output -project-brain swarm "review risky areas" /path/to/repo --output /path/to/output --preset cheap -``` - -## Mode with cloud/API providers - -Cloud/API providers are optional and should be reserved for ambiguous planning or synthesis that deterministic memory cannot answer. Configure provider environment variables according to your local router setup, then run: - -```bash -project-brain models -project-brain swarm "review architecture risks" /path/to/repo --output /path/to/output --preset balanced -``` - -Always prefer `go`, `status`, `resume`, `runbook`, and `fact-query` before broad model-heavy analysis. diff --git a/docs/output-contract.md b/docs/output-contract.md deleted file mode 100644 index 4f026eb..0000000 --- a/docs/output-contract.md +++ /dev/null @@ -1,85 +0,0 @@ -# Output contract - -This document defines the beta output contract for `project-brain` 0.2.3. - -## Policy - -- JSON artifacts are canonical for tools. -- Markdown artifacts are human-readable projections. -- Runtime outputs are rooted at `BRAIN/` inside the target repository by default. -- Runtime outputs must not dirty the target repository root when `--output` points outside it. -- Stale memory is evidence of work to refresh, not current truth. -- Public stable contracts cannot remove or rename required fields without a major version. -- Additive optional fields are allowed in minor releases. - -## Classification - -- `STABLE_PUBLIC`: safe for external tools to consume during the 0.x beta line with additive changes only. -- `STABLE_INTERNAL`: stable for project-brain commands, but not guaranteed for external consumers. -- `EXPERIMENTAL`: may change between minor releases. -- `RUNTIME_ONLY`: generated local output; do not version. -- `TEMPLATE`: source template used to generate runtime outputs. - -## Artifact map - -Paths below are relative to the selected output root. With default CLI settings that root is `/path/to/repo/BRAIN/`; with `--output`, it is the directory passed by the user. - -| Artifact | Path | Format | Class | Producer | Consumer | -|---|---|---|---|---|---| -| Memory brief | `AI_CONTEXT/MEMORY_BRIEF.md` | Markdown | STABLE_PUBLIC | memory brief writer | humans, agents | -| Memory brief JSON | `memory/memory_brief/memory_brief.json` | JSON | STABLE_INTERNAL | memory brief writer | agents, preflight | -| Executive summary | `AI_CONTEXT/EXECUTIVE_SUMMARY.md` | Markdown | STABLE_PUBLIC | status/start/resume/runbook | humans, handoff | -| Executive summary JSON | `memory/executive_summary/executive_summary.json` | JSON | STABLE_INTERNAL | executive summary writer | resume/runbook/preflight | -| Scope memory | `memory/scopes/*.json` | JSON | STABLE_INTERNAL | scope store/swarm | preflight, fact-query | -| Repository fact graph | `memory/knowledge_graph/repository_fact_graph.json` | JSON | STABLE_PUBLIC | code graph/fact graph | fact-query, preflight | -| Repository fact graph report | `reports/repository_fact_graph.md` | Markdown | STABLE_INTERNAL | fact graph | humans | -| Fact query report | `reports/fact_query.md` | Markdown | STABLE_PUBLIC | fact-query | humans, agents | -| Fact query JSON | `AI_CONTEXT/fact_query/fact_query.json` | JSON | STABLE_INTERNAL | fact-query | agents | -| Runbook report | `reports/runbook.md` | Markdown | STABLE_PUBLIC | runbook | humans | -| Runbook JSON | `AI_CONTEXT/runbook/runbook.json` | JSON | STABLE_INTERNAL | runbook | resume, agents | -| Project seed charter | `AI_CONTEXT/PROJECT_CHARTER.md` | Markdown | STABLE_PUBLIC | new | humans, agents | -| Project seed requirements | `AI_CONTEXT/REQUIREMENTS.md` | Markdown | STABLE_PUBLIC | new | humans, agents | -| Project seed blueprint | `AI_CONTEXT/PROJECT_BLUEPRINT.md` | Markdown | STABLE_PUBLIC | new | humans, agents | -| Project seed memory | `memory/project_seed/project_seed.json` | JSON | STABLE_INTERNAL | new | agents, continuity | -| Project seed backlog | `tasks/initial_backlog.md` | Markdown | STABLE_PUBLIC | new | humans, agents | -| Architecture plan blueprint | `docs/architecture_plan/BLUEPRINT.md` | Markdown | STABLE_PUBLIC | architecture-plan | humans | -| Architecture plan state | `docs/architecture_plan/STATE.md` | Markdown | STABLE_PUBLIC | architecture-plan | humans | -| Architecture plan Claude context | `docs/architecture_plan/CLAUDE.md` | Markdown | STABLE_INTERNAL | architecture-plan | agents, developers | -| Architecture plan memory | `memory/architecture_plan/architecture_plan.json` | JSON | STABLE_INTERNAL | architecture-plan | agents, continuity | -| Doctor report | `reports/doctor.md` | Markdown | RUNTIME_ONLY | doctor | humans | -| Doctor template | `reports/templates/doctor.md` | Markdown | TEMPLATE | source | doctor docs | -| Runtime directory | `.project-brain/runtime/` | mixed | RUNTIME_ONLY | local runs | local diagnostics | -| Validation matrix | `reports/validation-matrix.md` | Markdown | STABLE_INTERNAL | release QA | maintainers | -| Validation results | `reports/validation-results.json` | JSON | STABLE_INTERNAL | release QA | maintainers | -| Beta readiness | `reports/beta-readiness.md` | Markdown | STABLE_INTERNAL | release QA | maintainers | -| Release candidate report | `reports/release-candidate-*.md` | Markdown | STABLE_INTERNAL | release QA | maintainers | - -## Minimal examples - -### Repository fact graph - -Schema: `schemas/repository_fact_graph.schema.json` - -```json -{ - "version": 1, - "generatedAt": "2026-05-08T00:00:00.000Z", - "targetPath": "/repo", - "repoName": "repo", - "nodes": [{ "id": "repo:repo", "kind": "repository", "label": "repo" }], - "edges": [], - "stats": { "nodes": 1, "edges": 0, "codeGraphFiles": 0, "codeGraphSymbols": 0, "nodeKinds": { "repository": 1 }, "edgeKinds": {} } -} -``` - -### Preflight facts - -Schema: `schemas/preflight_facts.schema.json` - -`preflightFacts` is currently embedded in workflow results such as `AskResult`. It is the first deterministic gate before model-heavy work. - -### Validation results - -Schema: `schemas/validation_results.schema.json` - -`reports/validation-results.json` records beta validation command outcomes and quality notes. diff --git a/docs/product-blueprint.md b/docs/product-blueprint.md deleted file mode 100644 index 8f37203..0000000 --- a/docs/product-blueprint.md +++ /dev/null @@ -1,755 +0,0 @@ -# Project-Brain Future Commercial Vision - -> This document is future product vision, not current MVP scope. Do not use it to drive near-term CLI architecture, runtime complexity, multi-tenancy, RBAC, billing, dashboards, or marketplace work until the memory-first CLI is stable and validated by real users. - -## Status - -- Document type: commercial product strategy and packaging blueprint -- Product: `project-brain` -- Date baseline for competitive positioning: March 10, 2026 - -## 1. Executive Thesis - -`project-brain` should not be positioned as “another AI coding assistant.” - -It should be positioned as: - -**The engineering intelligence layer for software organizations.** - -It continuously analyzes repositories, architecture, risk, technical debt, and delivery quality across teams, then uses specialized AI agents to generate actionable improvement proposals without directly changing production code. - -This makes `project-brain` a system of record for software improvement, not just a point tool for code generation. - -## 2. Product Positioning - -### 2.1 Problem it solves - -Modern engineering organizations have three persistent problems: - -- they do not actually understand the state of their codebase across repositories -- technical debt, security risk, architectural drift, and operational fragility are detected too late -- AI coding tools help produce code faster, but they do not give leadership or platform teams a continuous intelligence layer for what should be improved next - -`project-brain` solves this by creating a continuously updated model of the software estate and using domain-specific agents to surface: - -- architecture risks -- technical debt hotspots -- insecure patterns -- missing tests -- observability gaps -- performance inefficiencies -- outdated or missing documentation -- prioritized improvement proposals - -### 2.2 Category - -Primary category: - -- Autonomous Engineering Intelligence Platform - -Secondary categories: - -- AI-native software governance -- repository intelligence -- architecture analytics -- technical debt intelligence - -### 2.3 Core value proposition - -For engineering leaders: - -- know what is deteriorating, where, and why - -For platform and staff engineers: - -- receive prioritized, evidence-backed improvement plans across repositories - -For security and compliance teams: - -- monitor software risk continuously, not only at release time - -For developers: - -- get high-signal proposals and documentation without giving an agent direct control of production code - -## 3. Target Customer - -### 3.1 Ideal customer profile - -Best initial ICP: - -- software companies with 50 to 800 engineers -- multi-repository or monorepo environments -- platform teams or DevEx teams already investing in standards and governance -- organizations with rising technical debt, onboarding drag, or repeated production incidents - -Best verticals: - -- B2B SaaS -- fintech -- healthtech -- govtech -- developer tools -- enterprise internal platforms - -### 3.2 Buyer personas - -Primary economic buyer: - -- VP Engineering -- CTO -- Head of Platform Engineering - -Primary champion: - -- Staff Engineer -- Principal Engineer -- Platform Lead -- Developer Experience Lead - -Secondary stakeholders: - -- AppSec lead -- QA lead -- SRE / observability lead -- compliance and architecture governance teams - -### 3.3 Who not to target first - -- solo developers looking only for autocomplete -- small teams with one repo and little process -- customers wanting fully autonomous code changes without review - -## 4. Product Architecture - -The commercial product should be multi-tenant, policy-aware, and repository-native. - -### 4.1 Tenant model - -Hierarchy: - -- Organization -- Workspace -- Repository Group -- Repository -- Project Environment - -This supports: - -- multiple organizations per deployment -- multiple repositories per organization -- separate policies by business unit -- deployment-specific connectors and rules - -### 4.2 Core product services - -- Repository Connectors -- Snapshot & Analysis Engine -- Agent Runtime -- Learning Memory -- Policy & Approval Service -- Reporting & Insights Service -- Dashboard/API Layer -- Billing & Usage Service - -### 4.3 Multi-repository support - -Features: - -- GitHub, GitLab, Bitbucket, and local read-only adapters -- monorepo and polyrepo support -- repository grouping by domain, team, service tier, or business system -- cross-repo architecture map -- portfolio-level technical debt rollups - -### 4.4 Multi-organization support - -Capabilities: - -- tenant isolation -- per-org LLM and deployment policies -- org-specific rule packs -- org-specific dashboards -- enterprise billing and reporting - -### 4.5 Role-based access - -Required roles: - -- `OrgAdmin` -- `SecurityAdmin` -- `PlatformLead` -- `EngineeringManager` -- `Architect` -- `Contributor` -- `Auditor` - -Permission domains: - -- repository access -- analysis configuration -- dashboard visibility -- marketplace installation -- prompt/rule customization -- approval workflow control - -### 4.6 Dashboard surfaces - -The product needs dashboards, not just reports. - -Core dashboards: - -- Executive Overview -- Repository Health -- Architecture Map -- Risk & Security Inbox -- Technical Debt Heatmap -- Documentation Coverage -- Improvement Proposal Board -- Incident & Learnings Timeline -- Agent Performance & Quality -- Marketplace / Rules Management - -## 5. Core Features - -### 5.1 Repository Intelligence - -- stack detection -- dependency inventory -- CI/CD understanding -- ownership and churn analysis -- repository maturity scoring - -### 5.2 Architectural Analysis - -- architecture mapping -- dependency graphing -- boundary violation detection -- architectural drift detection -- service and module hotspot identification - -### 5.3 Technical Debt Detection - -- debt hotspot clustering -- stale module detection -- missing test coverage heuristics -- dependency bloat -- repeated refactor candidates - -### 5.4 Security Scanning - -- secret exposure detection -- vulnerable dependency enrichment -- Docker and IaC hygiene analysis -- auth boundary heuristics -- policy-based risk scoring - -### 5.5 Performance Optimization - -- build-time regressions -- heavy dependency surfaces -- inefficient deployment artifacts -- query and service hotspot heuristics -- runtime risk signals from incidents and telemetry - -### 5.6 Documentation Generation - -- architecture docs -- API docs -- runbooks -- ADR summaries -- onboarding context packs - -### 5.7 Improvement Proposals - -- prioritized recommendation bundles -- effort vs impact ranking -- risk-linked proposals -- proposal tracking across accepted / rejected / ignored -- optional patch suggestions as artifacts only - -## 6. Commercial Deployment Models - -### 6.1 Local self-hosted - -Target: - -- individual developers -- consultants -- small teams -- secure local analysis - -Packaging: - -- CLI -- local dashboard -- local model support -- local storage - -Use case: - -- free / low-cost adoption funnel - -### 6.2 Enterprise on-premise - -Target: - -- regulated industries -- air-gapped environments -- large enterprises with private code and strict governance - -Packaging: - -- Kubernetes deployment -- private model gateway support -- SSO / SCIM -- audit logging -- custom rule packs -- enterprise support - -### 6.3 SaaS cloud - -Target: - -- mid-market and growth engineering orgs - -Packaging: - -- managed control plane -- hosted dashboards -- cloud workers -- usage metering -- agent marketplace access - -Recommended deployment principle: - -- one product, three deployment modes, one common API and policy model - -## 7. Agent Marketplace - -The marketplace should be a strategic product surface, not a side feature. - -### 7.1 Marketplace purpose - -Allow customers and partners to extend `project-brain` with: - -- custom specialist agents -- custom rule packs -- policy packs -- compliance packs -- architecture analyzers -- report templates - -### 7.2 Marketplace items - -- `Agent Packs` -- `Rule Packs` -- `Prompt Packs` -- `Industry Packs` -- `Integration Connectors` -- `Dashboard Modules` - -### 7.3 Enterprise-specific rule sets - -Examples: - -- fintech secure coding rules -- healthcare compliance rules -- government delivery controls -- internal platform conventions -- domain-specific architecture standards - -### 7.4 Marketplace governance - -- signed packages -- versioned distribution -- security review process -- allowlist / denylist controls -- org-scoped private marketplace - -### 7.5 Strategic value - -The marketplace creates: - -- ecosystem lock-in -- implementation partner opportunities -- community-driven adoption -- upsell path for enterprise rule packs - -## 8. Pricing Model - -Pricing should be hybrid, not single-axis. - -### 8.1 Open core - -Free tier: - -- local CLI -- single-user repository analysis -- basic reports -- community agents and rules - -### 8.2 Team SaaS pricing - -Recommended model: - -- per developer seat for dashboards, workflows, approvals, and collaboration -- per repository for continuous analysis and retained intelligence - -Illustrative pricing: - -- `Team`: $39 per active developer / month -- `Repo Intelligence`: $15 per continuously monitored repository / month - -Rationale: - -- aligns with buyer mental models from AI dev tooling -- scales with actual code surface and usage - -### 8.3 Enterprise pricing - -Recommended model: - -- annual enterprise license -- priced by repository estate size, deployment model, and support tier - -Illustrative ranges: - -- `Enterprise Cloud`: starts around $60k ARR -- `Enterprise On-Prem`: starts around $120k ARR -- `Strategic / regulated`: $250k+ ARR with custom deployment, support, and rule packs - -### 8.4 Add-ons - -- private model gateway -- advanced audit and compliance -- enterprise marketplace -- premium rule packs -- incident integrations -- professional services - -## 9. Differentiation - -As of March 10, 2026, the competitive landscape shows strong tools for coding assistance and autonomous task execution, but there is still room for a product focused on organization-wide engineering intelligence. - -### 9.1 Positioning statement - -`project-brain` is not trying to replace the editor or become a generic AI employee. - -It should win by becoming: - -- the always-on intelligence layer across repositories -- the operating system for engineering improvement -- the memory and governance layer above coding agents - -### 9.2 Competitive comparison - -#### Devin - -Current market position: - -- positioned as an autonomous AI software engineer -- optimized around task execution from ticket to tested PR - -Where `project-brain` differs: - -- Devin is execution-first; `project-brain` should be intelligence-first -- Devin focuses on completing work; `project-brain` should focus on understanding what work matters across the estate -- `project-brain` can complement Devin by prioritizing what Devin or humans should tackle next - -#### Cody - -Current market position: - -- strong enterprise codebase assistant with large-codebase context and enterprise security posture - -Where `project-brain` differs: - -- Cody is primarily a developer-assistance surface -- `project-brain` should operate at repo, system, and leadership layers -- `project-brain` should provide cross-run memory, org dashboards, and portfolio-level improvement governance - -#### GitHub Copilot - -Current market position: - -- broad AI coding platform embedded in IDEs, GitHub, mobile, CLI, and coding agent workflows - -Where `project-brain` differs: - -- Copilot is embedded in coding flow -- `project-brain` should be embedded in engineering governance and continuous analysis -- `project-brain` can ingest outputs from Copilot-driven repos and turn them into organizational intelligence - -#### Cursor - -Current market position: - -- AI-native editor with background agents, PR review, memories, and team rules - -Where `project-brain` differs: - -- Cursor is editor-centric -- `project-brain` should be system-centric and dashboard-centric -- Cursor helps write and review code; `project-brain` should explain systemic health, drift, and improvement priorities across many repos - -#### OpenClaw - -Current market position: - -- open-source, self-hosted agent runtime with plugins, skills, persistent memory, and a public registry - -Where `project-brain` differs: - -- OpenClaw is a general agent runtime -- `project-brain` should be domain-specialized for software engineering intelligence -- `project-brain` should prioritize auditability, repo intelligence, enterprise governance, and continuous analysis rather than broad personal automation - -### 9.3 Defensible moat - -The moat is not “we have agents.” - -The moat is: - -- accumulated repository intelligence -- learning memory across runs and incidents -- organization-specific rule packs -- approval-linked improvement history -- cross-repository architecture graph -- high-signal recommendations tuned by real acceptance data - -## 10. Go-to-Market Strategy - -### 10.1 Launch model - -Recommended: - -- open core product -- enterprise extensions -- community ecosystem - -### 10.2 Open core strategy - -Open source: - -- CLI -- discovery engine -- local reporting -- base agents -- community rules and prompts - -Closed / commercial: - -- multi-org dashboards -- hosted control plane -- SSO / SCIM -- audit and policy management -- approvals -- incident integrations -- advanced memory and portfolio analytics -- enterprise marketplace - -### 10.3 Initial wedge - -The initial wedge is not “replace developers.” - -It is: - -**Give engineering leadership and platform teams a live map of technical debt, risk, and architecture quality across repositories.** - -This wedge is easier to buy because it: - -- creates visibility -- avoids threatening developers directly -- complements existing AI editors and agents -- produces measurable ROI through fewer incidents and better prioritization - -### 10.4 Distribution channels - -- open-source adoption via CLI -- content marketing around architecture intelligence and technical debt -- integrations with GitHub/GitLab -- platform engineering community -- DevEx and CTO-led enterprise sales -- implementation partners for regulated industries - -### 10.5 Sales motion - -Bottom-up: - -- developer or platform lead installs local CLI -- team adopts dashboard for one repo group -- expands to multiple repositories - -Top-down: - -- CTO / VP Engineering buys enterprise visibility and governance -- security and platform teams expand usage internally - -### 10.6 Proof of value metrics - -- accepted recommendations per month -- technical debt backlog surfaced and closed -- security issues caught before release -- documentation coverage improvement -- onboarding time reduction -- incident recurrence reduction - -## 11. Technical Roadmap - -### v1: Engineering Intelligence - -Primary promise: - -- understand repositories and continuously surface high-signal improvement insights - -Ship: - -- repository intelligence -- architecture maps -- debt and security analysis -- documentation generation -- web dashboard -- multi-repo support -- approvals and basic memory - -### v2: Autonomous Improvement Suggestions - -Primary promise: - -- turn raw intelligence into high-confidence change proposals - -Ship: - -- proposal tracking lifecycle -- patch suggestions as artifacts -- improved learning loop -- PR and issue integrations -- organization-specific agent/rule packs -- stronger scoring and prioritization - -### v3: Enterprise Knowledge Graph - -Primary promise: - -- become the operational memory layer for the engineering organization - -Ship: - -- cross-repo architecture knowledge graph -- incident-linked software memory -- org-wide best-practice learning -- executive forecasting -- portfolio risk trend analysis -- benchmarking across teams and systems - -## 12. Product Packaging - -### 12.1 CLI - -Purpose: - -- onboarding -- local analysis -- CI scripting -- power-user workflows - -### 12.2 Web dashboard - -Purpose: - -- org visibility -- approvals -- management reporting -- multi-repo health and risk views -- marketplace administration - -### 12.3 API - -Purpose: - -- automation -- third-party integrations -- enterprise embedding -- custom reporting - -### 12.4 Plugin ecosystem - -Purpose: - -- custom agents -- custom rule packs -- custom connectors -- custom report renderers - -## 13. Recommended Product Editions - -### Community - -- local CLI -- basic reports -- community agent packs -- no hosted dashboard - -### Pro Team - -- SaaS dashboard -- team collaboration -- continuous repository monitoring -- approvals -- Slack / Jira / GitHub integrations - -### Enterprise - -- SSO / SCIM -- on-prem or private cloud -- custom model routing -- audit and governance -- private marketplace -- premium support - -## 14. Messaging - -### Homepage message - -**Know what your codebase needs next.** - -`project-brain` continuously analyzes your repositories, maps architecture, detects debt and risk, and turns engineering reality into prioritized AI-guided improvement proposals. - -### Short pitch - -`project-brain` is the engineering intelligence platform that sits above your repositories and AI coding tools, giving your organization a continuous system for understanding software health, risk, architecture drift, and improvement opportunities. - -### Why now - -- AI coding tools accelerate code creation -- faster code creation increases the need for codebase intelligence and governance -- engineering organizations need a control layer, not just more code generation - -## 15. Strategic Recommendation - -Launch `project-brain` as an open-core engineering intelligence platform that complements, rather than competes head-on with, AI coding editors and autonomous coding agents. - -The winning strategy is: - -- own the repository and organization intelligence layer -- integrate with existing coding agents rather than displace them -- monetize governance, memory, dashboards, and enterprise controls -- build an ecosystem around custom agents and rule packs - -If executed well, `project-brain` becomes the platform that tells engineering teams what matters, why it matters, and what to improve next. - -## 16. Competitive Sources - -The competitive comparisons above are informed by the following current sources: - -- Devin official site: [devin.ai](https://devin.ai/) -- Devin docs and release notes: [docs.devin.ai](https://docs.devin.ai/) and [release notes](https://docs.devin.ai/release-notes) -- Sourcegraph Cody official product page: [sourcegraph.com/cody](https://sourcegraph.com/cody) -- GitHub Copilot official product page: [github.com/features/copilot](https://github.com/features/copilot) -- GitHub Copilot coding agent GA: [GitHub changelog, September 25, 2025](https://github.blog/changelog/2025-09-25-copilot-coding-agent-is-now-generally-available/) -- GitHub Copilot agent mode announcement: [GitHub newsroom, February 6, 2025](https://github.com/newsroom/press-releases/agent-mode) -- Cursor official site: [cursor.com](https://cursor.com/en-US) -- Cursor changelog and Bugbot docs: [Cursor changelog](https://www.cursor.com/changelog), [Bugbot docs](https://docs.cursor.com/en/bugbot), [Background agents docs](https://docs.cursor.com/en/background-agents) -- OpenClaw official site: [openclaw.ai](https://openclaw.ai/) -- OpenClaw docs: [docs.openclaw.ai](https://docs.openclaw.ai/index), [ClawHub](https://docs.openclaw.ai/tools/clawhub), [Plugins](https://docs.openclaw.ai/tools/plugin) diff --git a/docs/production-architecture-spec.md b/docs/production-architecture-spec.md deleted file mode 100644 index 631d815..0000000 --- a/docs/production-architecture-spec.md +++ /dev/null @@ -1,1034 +0,0 @@ -# Project-Brain Production Architecture Specification - -## Status - -- Document type: production implementation specification -- Baseline analyzed: current `project-brain` repository -- Target state: autonomous engineering intelligence platform for large-scale continuous software analysis - -## 1. Current State Assessment - -The current repository already has a usable foundation: - -- CLI entrypoints in `cli/` -- repository discovery in `core/discovery_engine/` -- context generation in `core/context_builder/` -- orchestration in `core/orchestrator/` -- persistent Markdown memory in `memory/` -- specialist agents in `agents/` -- report generation into `reports/` and `docs/` - -The current design is intentionally lightweight and non-destructive. It is effective for local execution, but it is not yet production-grade for continuous analysis at scale. - -### Current strengths - -- Clear modular boundaries between discovery, orchestration, memory, and agents -- Non-destructive operating model -- Practical CLI workflow -- Good conceptual mapping for specialist agents - -### Current architectural limits - -- Single-process execution -- Sequential agent execution -- No task graph or retry semantics -- No job queue or distributed workers -- No LLM provider abstraction -- File-only memory model -- No explicit approval workflow -- No sandbox boundary for tool execution -- No multi-tenant API or server mode -- No audit-grade event ledger - -## 2. Production Goals - -The production platform must: - -- continuously analyze large repositories and monorepos -- support local mode and server mode -- run specialist agents in parallel under policy control -- isolate tool execution in read-only sandboxes -- persist durable knowledge and historical decisions -- support multiple LLM providers through a stable adapter layer -- integrate with CI/CD and event triggers -- require human approval for any proposed code change or external side effect -- expose auditable, role-aware workflows - -## 3. Target Architecture Summary - -The production architecture is a layered control-plane and worker-plane system: - -- `Control Plane`: API, scheduler, workflow coordinator, policy engine, approval service -- `Worker Plane`: discovery workers, tool runners, agent workers, report workers -- `Memory Plane`: relational state, object storage, vector knowledge index, audit ledger -- `Integration Plane`: Git providers, CI/CD, webhooks, OpenAPI sources, observability backends -- `LLM Plane`: provider adapters for OpenAI and local/self-hosted models - -The current repo should evolve from a single-package CLI project into a modular monorepo with shared contracts and separately deployable services. - -## 4. Final Directory Structure - -```text -project-brain/ - apps/ - cli/ - api/ - scheduler/ - worker/ - approval-console/ - packages/ - contracts/ - src/ - api/ - events/ - runtime/ - memory/ - agents/ - runtime/ - src/ - engine/ - loop/ - lifecycle/ - sandbox/ - orchestration/ - scheduling/ - agents/ - src/ - chief/ - product-owner/ - qa/ - security/ - optimization/ - observability/ - legal/ - documentation/ - dev/ - common/ - tools/ - src/ - git-analysis/ - openapi-validator/ - dependency-scanner/ - security-scanner/ - architecture-analyzer/ - performance-analyzer/ - common/ - llm/ - src/ - adapters/ - openai/ - ollama/ - vllm/ - lm-studio/ - routing/ - policies/ - prompts/ - memory/ - src/ - short-term/ - long-term/ - decision-log/ - error-history/ - knowledge-base/ - projections/ - governance/ - src/ - policy-engine/ - approvals/ - permissions/ - audit/ - integrations/ - src/ - git/ - github/ - gitlab/ - ci/ - webhooks/ - logs/ - metrics/ - reporting/ - src/ - renderers/ - templates/ - exporters/ - shared/ - src/ - config/ - logging/ - telemetry/ - utils/ - deploy/ - docker/ - helm/ - terraform/ - docs/ - architecture/ - operations/ - security/ - tests/ - integration/ - e2e/ - fixtures/ -``` - -## 5. Component Diagram - -```mermaid -flowchart LR - CLI["CLI / UI / API Clients"] --> API["API Gateway"] - API --> WF["Workflow Orchestrator"] - API --> GOV["Governance & Approval Service"] - API --> MEM["Memory Service"] - API --> KB["Knowledge Base Service"] - SCHED["Scheduler / Triggers"] --> WF - GIT["Git / CI / Webhooks"] --> API - - WF --> SNAP["Repository Snapshot Service"] - WF --> TASKS["Task Graph Planner"] - TASKS --> CHIEF["Chief Agent Runtime"] - - CHIEF --> PA["ProductOwnerAgent"] - CHIEF --> QA["QAAgent"] - CHIEF --> SEC["SecurityAgent"] - CHIEF --> OPT["OptimizationAgent"] - CHIEF --> OBS["ObservabilityAgent"] - CHIEF --> LEG["LegalAgent"] - CHIEF --> DOC["DocumentationAgent"] - CHIEF --> DEV["DevAgent"] - - PA --> TOOLS["Tooling Runtime"] - QA --> TOOLS - SEC --> TOOLS - OPT --> TOOLS - OBS --> TOOLS - LEG --> TOOLS - DOC --> TOOLS - DEV --> TOOLS - - TOOLS --> SBX["Read-only Sandbox"] - CHIEF --> LLM["LLM Abstraction Layer"] - PA --> LLM - QA --> LLM - SEC --> LLM - - WF --> REPORT["Report Generator"] - REPORT --> MEM - REPORT --> KB - GOV --> AUDIT["Audit Ledger"] - MEM --> AUDIT - LLM --> AUDIT -``` - -## 6. Core Runtime - -### 6.1 Runtime responsibilities - -The runtime is the execution kernel of the system. It owns: - -- job intake -- snapshot management -- task graph creation -- agent scheduling -- tool invocation -- policy enforcement -- retries and timeouts -- result persistence -- workflow completion state - -### 6.2 Agent runtime loop - -Each analysis job runs through a deterministic loop: - -1. `INTAKE` - Validate request, tenant policy, repo adapter, and execution profile. -2. `SNAPSHOT` - Create immutable repository snapshot from Git ref, CI artifact, or local path. -3. `DISCOVERY` - Run baseline discovery scanners and produce normalized repository facts. -4. `PLAN` - Chief Agent builds a task graph from discovery facts, risk heuristics, and previous memory. -5. `DISPATCH` - Runtime schedules specialist agent tasks onto worker queues. -6. `EXECUTE` - Agents invoke approved tools inside read-only sandboxes and call LLM providers through policy-aware adapters. -7. `EVALUATE` - Runtime validates outputs against schema, confidence thresholds, and policy constraints. -8. `APPROVAL` - Any side-effecting recommendation or patch proposal is held behind human approval gates. -9. `PERSIST` - Update short-term memory, long-term memory, decision log, error history, and knowledge base projections. -10. `REPORT` - Generate artifacts, API payloads, and CI annotations. -11. `COMPLETE` - Close workflow and emit audit events. - -### 6.3 Task orchestration - -Production orchestration should be workflow-based, not simple in-process sequencing. - -Recommended model: - -- `Workflow Engine`: Temporal -- `Queue`: Temporal task queues or NATS JetStream for externalized worker pools -- `Execution unit`: typed `AnalysisTask` -- `Strategy`: DAG-based scheduling with dependencies, concurrency limits, deadlines, and retry policies - -Task types: - -- snapshot tasks -- discovery tasks -- planner tasks -- specialist agent tasks -- aggregation tasks -- reporting tasks -- approval tasks - -### 6.4 Agent lifecycle management - -Every agent must implement the same lifecycle: - -- `REGISTERED` -- `READY` -- `PLANNED` -- `RUNNING` -- `WAITING_FOR_TOOL` -- `WAITING_FOR_LLM` -- `WAITING_FOR_APPROVAL` -- `COMPLETED` -- `FAILED` -- `QUARANTINED` - -Quarantine is required for: - -- repeated malformed outputs -- policy violations -- excessive cost -- tool misuse -- confidence collapse - -### 6.5 Execution sandbox - -All analyzer and tool execution must occur in an isolated environment. - -Recommended boundary: - -- default: rootless containers -- hardened mode: `gVisor` or `Firecracker` microVM for untrusted repo content -- filesystem: read-only mount of repo snapshot -- temp workspace: scratch volume with TTL -- network: deny by default, allowlist per tool -- secrets: injected per-tool, per-task, never exposed to repo filesystem - -## 7. Agent System - -### 7.1 Chief Agent - -The Chief Agent is not a generic chat orchestrator. It is a policy-bound planner and synthesizer. - -Responsibilities: - -- interpret repository state and historical memory -- create the task graph -- assign priorities and budgets -- select specialist agents -- merge findings -- escalate approval-required items -- publish the consolidated system judgment - -Chief Agent output: - -- execution plan -- task graph -- risk summary -- recommendation bundle -- approval bundle - -### 7.2 Specialist agents - -#### ProductOwnerAgent - -- analyzes backlog signals, feature friction, missing product docs, release readiness, and workflow bottlenecks -- consumes architecture facts, API contracts, issue metadata, and usage heuristics -- produces product improvement proposals - -#### QAAgent - -- analyzes coverage, flaky areas, missing test layers, risky diffs, and contract-test gaps -- consumes repository structure, test inventory, CI history, and changed modules -- produces QA risk findings and test plans - -#### SecurityAgent - -- analyzes secrets exposure, dependency risk, image hygiene, auth surfaces, permission boundaries, and policy drift -- consumes repo snapshot, SBOM, IaC facts, dependency advisories, and secret scanner outputs -- produces security risks and remediations - -#### OptimizationAgent - -- analyzes dependency bloat, build performance, image size, query hot paths, and runtime inefficiencies -- consumes profiling artifacts, CI durations, dependency graphs, and container metadata -- produces optimization backlog - -#### ObservabilityAgent - -- analyzes logs, metrics, traces, alerts, runbooks, and SLO coverage -- consumes telemetry config, dashboards metadata, alert definitions, and instrumentation facts -- produces observability gaps and operational readiness report - -#### LegalAgent - -- analyzes licenses, dependency obligations, notices, privacy-sensitive data paths, and compliance documentation gaps -- consumes dependency manifests, notices, policy rules, and configured regions -- produces compliance updates and action items - -#### DocumentationAgent - -- generates architecture docs, API docs, runbooks, ADR summaries, and onboarding guides -- consumes normalized facts, approved findings, and knowledge base records -- produces human-readable documentation artifacts - -#### DevAgent - -- analyzes code structure, refactor candidates, maintainability risks, dead modules, and architecture drift -- consumes repository graph, complexity metrics, and previous technical decisions -- produces refactor proposals and engineering tasks - -### 7.3 Agent execution policy - -Each agent must declare: - -- allowed tools -- allowed network domains -- maximum runtime -- maximum token budget -- required input schemas -- output schema -- confidence scoring policy -- approval requirements - -## 8. Tooling Layer - -The tooling layer must be explicit, typed, and policy-enforced. Tools are not arbitrary shell access. - -### 8.1 Required production tools - -- `GitAnalysisTool` -- `OpenApiValidatorTool` -- `DependencyScannerTool` -- `SecurityScannerTool` -- `ArchitectureAnalyzerTool` -- `PerformanceAnalyzerTool` - -### 8.2 Tool responsibilities - -#### GitAnalysisTool - -- commit graph inspection -- ownership hotspots -- churn analysis -- risky file concentration -- branch and release metadata - -#### OpenApiValidatorTool - -- schema validation -- breaking change detection -- endpoint inventory -- version drift -- contract completeness scoring - -#### DependencyScannerTool - -- manifest parsing -- transitive dependency graph -- SBOM generation -- outdated packages -- duplicate dependency detection - -#### SecurityScannerTool - -- secret scanning -- vulnerable dependency enrichment -- IaC misconfiguration rules -- container hardening checks -- auth and permission surface heuristics - -#### ArchitectureAnalyzerTool - -- module graph extraction -- cycle detection -- bounded context inference -- layering rule violations -- service boundary mapping - -#### PerformanceAnalyzerTool - -- build-time analysis -- container size analysis -- hotspot heuristics -- query-pattern extraction -- optional profile artifact ingestion - -### 8.3 Tool contract - -```ts -export interface ToolDefinition { - id: string; - version: string; - readOnly: boolean; - networkPolicy: "deny" | "allowlist"; - timeoutMs: number; - inputSchema: JsonSchema; - outputSchema: JsonSchema; - execute(input: I, ctx: ToolExecutionContext): Promise; -} -``` - -### 8.4 Tool execution rules - -- Tool outputs must be schema-validated. -- Tools must return evidence references. -- No tool may write to the repo snapshot. -- Shell tools must run behind wrapper executors, never directly from agent prompts. - -## 9. LLM Abstraction Layer - -### 9.1 Objective - -Separate reasoning logic from model vendor implementation. - -### 9.2 Supported providers - -- OpenAI -- local/self-hosted Llama-family models -- local/self-hosted DeepSeek-family models -- any OpenAI-compatible endpoint via adapter - -### 9.3 Adapter architecture - -Recommended adapters: - -- `OpenAIAdapter` -- `OllamaAdapter` -- `VllmAdapter` -- `LmStudioAdapter` -- `OpenAICompatibleAdapter` - -Provider router responsibilities: - -- model selection by task type -- cost and latency policy -- fallback policy -- prompt redaction -- response schema enforcement -- token accounting - -### 9.4 LLM contract - -```ts -export interface LlmProvider { - providerId: string; - supportsResponsesApi: boolean; - generate(request: LlmRequest): Promise>; - embed(request: EmbeddingRequest): Promise; - health(): Promise; -} -``` - -```ts -export interface LlmRequest { - taskType: "planning" | "analysis" | "synthesis" | "documentation"; - model: string; - responseSchema: JsonSchema; - messages: Array<{ role: "system" | "user" | "tool"; content: string }>; - maxTokens: number; - temperature: number; - metadata: Record; -} -``` - -### 9.5 Provider strategy - -- OpenAI for high-precision planning, synthesis, and approval bundles -- local models for low-cost classification, summarization, and large-batch heuristics -- strict fallback path if a provider is unavailable or violates schema - -## 10. Memory System - -The memory system must move from Markdown-only persistence to a layered knowledge architecture. - -### 10.1 Memory categories - -#### Short-term memory - -- per-job working memory -- task outputs -- intermediate facts -- TTL-based storage - -Recommended storage: - -- Redis for ephemeral state -- object storage for large artifacts - -#### Long-term learning memory - -- recurring patterns -- proven remediations -- project-specific heuristics -- architecture evolution summaries - -Recommended storage: - -- PostgreSQL plus pgvector - -#### Decision log - -- approved decisions -- rejected proposals -- rationale -- approver identity -- links to evidence - -Recommended storage: - -- PostgreSQL append-only table - -#### Error history - -- scanner failures -- policy violations -- tool crashes -- false positives -- reliability incidents - -Recommended storage: - -- PostgreSQL plus searchable log index - -#### Knowledge base - -- normalized architecture facts -- ADRs -- API contracts -- ownership maps -- runbooks -- historical reports - -Recommended storage: - -- object storage for canonical documents -- PostgreSQL metadata index -- pgvector embeddings for retrieval - -### 10.2 Memory contract - -```ts -export interface MemoryService { - putFact(record: FactRecord): Promise; - appendDecision(record: DecisionRecord): Promise; - appendError(record: ErrorRecord): Promise; - queryKnowledge(query: KnowledgeQuery): Promise; - getProjectState(projectId: string): Promise; -} -``` - -### 10.3 Projection model - -Persist raw events first, then build projections: - -- current project profile -- current risk profile -- current architecture map -- current task backlog -- agent reliability scores - -## 11. Continuous Analysis Engine - -### 11.1 Scheduled analysis - -Run recurring workflows: - -- nightly incremental scan -- weekly deep scan -- monthly compliance scan - -### 11.2 Trigger-based analysis - -Supported triggers: - -- Git push -- pull request opened or updated -- release tag -- CI failure -- dependency advisory -- manual run - -### 11.3 CI/CD integration - -Modes: - -- passive mode: publish annotations and reports only -- gate mode: block deployment on configured high-severity findings -- advisory mode: comment recommendations without blocking - -CI contract: - -```ts -export interface CiFinding { - severity: "low" | "medium" | "high" | "critical"; - category: string; - title: string; - file?: string; - line?: number; - recommendation: string; - evidence: string[]; -} -``` - -## 12. Safety and Governance - -### 12.1 Read-only repo adapters - -All repository access must use snapshot adapters: - -- `LocalReadOnlyAdapter` -- `GitCloneAdapter` -- `GitHubArchiveAdapter` -- `GitLabArchiveAdapter` - -Rules: - -- snapshot immutable during analysis -- no direct write access to target repository -- patch proposals generated separately as artifacts - -### 12.2 Human approval gates - -Approval is required for: - -- proposed code patches -- config changes -- policy changes -- outbound integrations beyond approved domains -- any action classified as `write`, `deploy`, or `notify` - -### 12.3 Role-based agent permissions - -Use explicit capabilities: - -- `repo.read` -- `tool.git.read` -- `tool.openapi.validate` -- `tool.dependency.scan` -- `tool.security.scan` -- `tool.performance.analyze` -- `memory.read` -- `memory.write` -- `report.write` -- `approval.request` - -Agents receive only the minimum required set. - -### 12.4 Audit log - -Every job must produce an immutable audit trail: - -- who started the job -- what repo/ref was analyzed -- what tools ran -- what model/provider was used -- what findings were emitted -- what approvals were requested -- who approved or rejected - -Recommended implementation: - -- append-only audit table in PostgreSQL -- event streaming to object storage for long retention - -## 13. Scalable Deployment - -### 13.1 Local mode - -Purpose: - -- single-user analysis -- offline or near-offline execution -- local models supported - -Deployment: - -- CLI -- embedded API optional -- SQLite or local PostgreSQL -- local object storage folder -- Ollama or OpenAI provider - -### 13.2 Server mode - -Purpose: - -- team-based operation -- multi-project scheduling -- approvals and governance - -Deployment components: - -- API service -- scheduler service -- workflow service -- worker pool -- PostgreSQL -- Redis -- object storage -- telemetry stack - -### 13.3 Distributed agent workers - -Worker pools should be specialized: - -- `discovery-workers` -- `tool-workers` -- `agent-workers` -- `report-workers` -- `ingestion-workers` - -Scheduling policies: - -- weighted queues -- project-level concurrency caps -- tenant quotas -- graceful draining - -## 14. Runtime Flow - -```text -Trigger -> Intake API -> Policy Check -> Immutable Snapshot -> Discovery Scan --> Chief Agent Planning -> Parallel Specialist Agent Tasks -> Evidence Validation --> Governance/Approval -> Memory Persistence -> Report Generation -> API/CI Output -``` - -Detailed execution flow: - -1. Trigger received from CLI, API, scheduler, or webhook. -2. API creates `AnalysisJob`. -3. Workflow engine creates immutable repo snapshot. -4. Discovery workers produce normalized repository facts. -5. Chief Agent generates task graph and budgets. -6. Specialist agents execute in parallel with tool and LLM calls. -7. Runtime validates evidence, confidence, and policy compliance. -8. Approval service holds any side-effecting output. -9. Memory service stores events and updates projections. -10. Reporting service emits Markdown, JSON, CI annotations, and API responses. - -## 15. API Contracts Between Modules - -### 15.1 Core job contract - -```ts -export interface AnalysisJob { - jobId: string; - projectId: string; - tenantId: string; - trigger: - | { type: "manual"; actorId: string } - | { type: "schedule"; scheduleId: string } - | { type: "git"; provider: "github" | "gitlab"; event: string } - | { type: "ci"; provider: string; pipelineId: string }; - snapshot: RepoSnapshotRef; - mode: "baseline" | "incremental" | "deep" | "compliance"; - requestedAgents: AgentKind[]; - priority: "low" | "normal" | "high"; - createdAt: string; -} -``` - -### 15.2 Repo snapshot contract - -```ts -export interface RepoSnapshotRef { - snapshotId: string; - source: "local" | "git" | "archive" | "artifact"; - repoUri: string; - ref: string; - commitSha?: string; - mountPath: string; - readOnly: true; -} -``` - -### 15.3 Agent task contract - -```ts -export interface AgentTask { - taskId: string; - jobId: string; - agent: AgentKind; - inputs: string[]; - dependencies: string[]; - budget: { - maxRuntimeMs: number; - maxToolCalls: number; - maxTokens: number; - }; - permissions: string[]; -} -``` - -### 15.4 Finding contract - -```ts -export interface Finding { - findingId: string; - jobId: string; - agent: AgentKind; - severity: "low" | "medium" | "high" | "critical"; - confidence: number; - category: string; - title: string; - summary: string; - recommendation: string; - evidence: Array<{ - type: "file" | "tool-output" | "metric" | "commit" | "policy"; - ref: string; - }>; - approvalRequired: boolean; -} -``` - -### 15.5 External service API - -Minimal HTTP surface: - -- `POST /v1/jobs` -- `GET /v1/jobs/:jobId` -- `GET /v1/jobs/:jobId/findings` -- `GET /v1/projects/:projectId/state` -- `POST /v1/approvals/:approvalId/approve` -- `POST /v1/approvals/:approvalId/reject` -- `POST /v1/webhooks/git` -- `POST /v1/webhooks/ci` - -## 16. Recommended Tech Stack - -### Core platform - -- Language: TypeScript -- Runtime: Node.js 22+ -- Package management: pnpm workspaces -- Build: `tsup` or `tsx` for services, `tsc` for contracts - -### Orchestration - -- Workflow engine: Temporal -- Queue/eventing: NATS JetStream - -### API and services - -- API framework: Fastify -- Validation: Zod -- Internal contracts: TypeScript types plus JSON Schema - -### Storage - -- PostgreSQL for durable state -- Redis for ephemeral state and rate limiting -- S3-compatible object storage for artifacts -- pgvector for retrieval and semantic knowledge lookup - -### Observability - -- OpenTelemetry -- Prometheus -- Grafana -- Loki - -### Security and sandboxing - -- rootless Docker -- gVisor for hardened multi-tenant mode -- Vault or cloud secret manager - -### Local model support - -- Ollama for simple local mode -- vLLM for production self-hosted inference clusters - -### OpenAI support - -- OpenAI Responses API through a dedicated adapter - -## 17. Security Boundaries - -### Boundary A: client to control plane - -- authenticated via OIDC or service tokens -- tenant-scoped authorization -- rate-limited and audited - -### Boundary B: control plane to worker plane - -- signed job payloads -- short-lived worker credentials -- no direct user secret passthrough - -### Boundary C: worker plane to repository snapshot - -- read-only mount only -- no network by default -- no write-back path - -### Boundary D: worker plane to LLM providers - -- prompt redaction for secrets and tokens -- outbound allowlist -- provider-specific policy routing - -### Boundary E: persistence layer - -- separate databases for operational state and audit state preferred -- row-level scoping by tenant/project -- encryption at rest - -### Boundary F: approval operations - -- privileged service path -- dual-control support for high-risk actions -- immutable approval record - -## 18. Evolution Path From Current Repository - -The current modules map cleanly into the target architecture: - -- `core/discovery_engine` -> `packages/tools` plus `packages/runtime/engine` -- `core/context_builder` -> `packages/memory/projections` plus `packages/reporting` -- `core/orchestrator` -> `apps/api`, `apps/scheduler`, and `packages/runtime/orchestration` -- `agents/*` -> `packages/agents/*` -- `memory/*` -> `packages/memory/*` -- `integrations/*` -> `packages/integrations/*` -- `cli/` -> `apps/cli/` - -Recommended implementation phases: - -1. extract shared contracts and domain types -2. introduce workflow runtime and queue-backed workers -3. add LLM adapter layer -4. replace file-only memory with PostgreSQL plus object storage plus vector index -5. add API service and approval service -6. add hardened sandbox runtime -7. add distributed scheduling and multi-project operations - -## 19. Final Architectural Decision - -The production version of `project-brain` should be implemented as a TypeScript monorepo with: - -- a workflow-driven control plane -- isolated read-only analysis workers -- a typed tool runtime -- a provider-agnostic LLM layer -- event-backed memory and knowledge projections -- human approval gates for all side effects -- support for local, server, and distributed execution modes - -This preserves the current repository’s strengths while making the platform suitable for continuous, large-scale, auditable software intelligence. diff --git a/docs/reference-repo-analysis.md b/docs/reference-repo-analysis.md deleted file mode 100644 index b4ff16e..0000000 --- a/docs/reference-repo-analysis.md +++ /dev/null @@ -1,436 +0,0 @@ -# Reference Repo Analysis - -This document compares `project-brain` against the external reference repositories that were cloned for code-level inspection. - -## Scope - -Cloned references used in this review: - -- `gsd-build/get-shit-done` -- `andrewyng/context-hub` -- `tirth8205/code-review-graph` -- `nyldn/claude-octopus` -- `opendataloader-project/opendataloader-pdf` -- `LucidAkshay/kavach` - -`zai-org/GLM-OCR` was assessed as a lower-priority complement rather than a core reference. Its local clone did not complete cleanly during this pass, so it is not part of the deep code comparison below. - -## Executive summary - -What `project-brain` should adopt next: - -1. A control-tower UX: simple intent in, routed workflow out. -2. Task packets and explicit project state inspired by `get-shit-done` and `claude-octopus`. -3. A policy and approval layer for agent execution inspired by `kavach`. -4. A registry-backed `context search/get` layer inspired by `context-hub`. -5. A document ingestion boundary inspired by `opendataloader-pdf`. - -What `project-brain` should not copy directly: - -- automatic execution and commit-heavy workflows from `get-shit-done` -- multi-provider orchestration complexity from `claude-octopus` -- plugin and hook ecosystems tied to a specific host agent -- GPLv3 implementation code from `kavach` - -## Current position of project-brain - -`project-brain` already has strengths that the reference projects do not combine in one place: - -- repository discovery and workspace analysis -- durable `AI_CONTEXT` generation -- specialist analysis agents -- governance and proposal reporting -- non-destructive defaults -- local annotations -- `code-graph-v2` for TS/JS with incremental hashes, symbols, and typed edges -- impact-radius and review-delta support - -Main remaining gaps: - -- context retrieval is still local-only, not registry backed -- CLI still expects the user to know the right command -- project state and task packets are not yet first-class workflow artifacts -- there is no agent firewall or policy engine for approval, tool access, and destructive-action controls -- document ingestion is still out of band - -## 1. get-shit-done - -### What it is - -`get-shit-done` is a workflow system more than a repository intelligence engine. Its core is a file-based planning runtime with commands, templates, state transitions, and agent markdown definitions. - -### Code surfaces inspected - -- `get-shit-done/bin/lib/init.cjs` -- `get-shit-done/bin/lib/state.cjs` -- `docs/ARCHITECTURE.md` - -### What is strong - -- Compound `init` commands return structured workflow context. -- State is explicit, readable, and file-backed. -- Templates and workflow assets are productized and easy to extend. -- Brownfield onboarding is treated as a first-class path. - -### What maps well to project-brain - -- A guided planning layer after analysis. -- Explicit state artifacts for improvement execution. -- Templates for common repo outcomes such as roadmap, concerns, decisions, and implementation tracks. - -### What does not map well - -- Automatic execution assumptions. -- Commit-centric workflow progression. -- Large markdown-command/plugin surface that is tied to host agents. - -### Decision - -Adopt the planning and state ideas, not the execution model. - -### Recommended follow-up - -Build a `plan-improvements` command that turns findings, annotations, impact data, and governance output into: - -- `docs/improvement_plan/ROADMAP.md` -- `docs/improvement_plan/STATE.md` -- `docs/improvement_plan/TRACKS.md` - -## 2. context-hub - -### What it is - -`context-hub` is a registry and retrieval CLI for curated docs and skills. It has a clean split between discovery, cache, annotations, source trust, and content fetch. - -### Code surfaces inspected - -- `cli/src/commands/search.js` -- `cli/src/commands/get.js` -- `cli/src/lib/registry.js` -- `cli/src/lib/annotations.js` -- `docs/design.md` - -### What is strong - -- Registry-backed retrieval instead of hardcoded context. -- Multiple sources merged into one search surface. -- Trust filtering via `official | maintainer | community`. -- BM25 search index for low-cost retrieval. -- Clear `search` and `get` separation. - -### What maps well to project-brain - -- A `context search` command for frameworks, providers, and stack patterns. -- A `context get` command for selective retrieval. -- Source metadata and trust policy. -- Cached registries and lightweight local index. - -### What project-brain already took - -- Persistent annotations. - -### What project-brain is still missing - -- search -- get -- source registry -- trust policy -- cached retrieval for external context - -### Decision - -High-value adaptation target. - -### Recommended follow-up - -Add: - -- `project-brain context-search ` -- `project-brain context-get ` -- `project-brain context-sources` - -With a local registry under `memory/context_registry/` and trust-aware metadata. - -## 3. code-review-graph - -### What it is - -`code-review-graph` is the strongest reference for the missing analytical core in `project-brain`: a persistent, incremental, symbol-aware code graph. - -### Code surfaces inspected - -- `code_review_graph/graph.py` -- `code_review_graph/incremental.py` -- `code_review_graph/parser.py` -- `code_review_graph/tools.py` - -### What is strong - -- Tree-sitter parsing across multiple languages. -- SQLite-backed graph store with nodes and typed edges. -- File hashes and incremental re-parse. -- Blast-radius and review-context queries over graph data. -- MCP tool layer that exposes graph operations cleanly. - -### Where project-brain is currently behind - -`project-brain` already closed part of this gap with `code-graph-v2` for TS/JS: - -- symbol-aware graph extraction via the TypeScript compiler -- incremental refresh keyed by file hash -- typed edges such as `imports`, `contains`, and `calls` - -Remaining gaps versus this reference: - -- JSON artifact instead of a richer queryable store -- TS/JS-first scope instead of multi-language support -- no first-class test coverage edges yet -- no graph query surface such as callers/callees/tests-for commands yet - -### What maps well to project-brain - -- Persistent graph store in `memory/code_graph/` -- Symbol-aware parsing for TypeScript/JavaScript first -- Incremental updates keyed by file hash -- Queries such as: - - callers of symbol - - files importing module - - tests covering symbol - - review context for changed file set - -### Decision - -Highest-priority technical reference. - -### Recommended follow-up - -Evolve `code-graph-v2` with a staged v3: - -1. SQLite store -2. symbol table -3. typed edges -4. incremental updater -5. graph queries for callers/callees/tests -6. richer `review-delta` context builder - -## 4. claude-octopus - -### What it is - -`claude-octopus` is a workflow and orchestration product. Its strength is not repository analysis depth but multi-agent coordination, provider routing, and quality gates. - -### Code surfaces inspected - -- `scripts/orchestrate.sh` -- `hooks/quality-gate.sh` -- `agents/config.yaml` -- `docs/ARCHITECTURE.md` - -### What is strong - -- Phase model with explicit defaults. -- Agent registry defined in config instead of hardcoded logic only. -- Provider routing by phase. -- Quality gate hooks. -- Worktree isolation for concurrent writers. -- Strong operational hardening around shell execution. - -### What maps well to project-brain - -- intent router -- explicit workflow phases -- configurable agent registry and role metadata -- stronger quality gate model for proposal promotion - -### What project-brain already took - -- a consensus gate for proposals - -### What does not map well - -- provider sprawl and plugin-heavy architecture -- shell-first orchestration -- worktree execution before `project-brain` even needs write-mode execution - -### Decision - -Useful product/UX reference, but not the next core engine dependency. - -### Recommended follow-up - -Add a high-level command such as: - -`project-brain ask "review the latest backend changes"` - -That routes to: - -- `map-codebase` -- `review-delta` -- `impact-radius` -- `analyze` - -Depending on intent. - -## 5. GLM-OCR - -### Position - -Useful as a future ingestion extension, not as a core reference for the current engine. - -### Why it is lower priority - -It helps with: - -- PDFs -- screenshots -- architecture documents -- scanned artifacts - -It does not materially improve: - -- code graph depth -- impact analysis -- governance quality -- review-delta context - -### Decision - -Do not prioritize now. Treat it as a future `ingest-doc` module. - -## 6. opendataloader-pdf - -### What it is - -`opendataloader-pdf` is a document ingestion engine for turning PDFs into structured Markdown, JSON, HTML, and image-aware artifacts with optional OCR and hybrid AI enrichment. - -### Code surfaces inspected - -- `opendataloader-pdf/README.md` -- `opendataloader-pdf/options.json` -- `opendataloader-pdf/schema.json` -- `opendataloader-pdf/package.json` - -### What is strong - -- Clean output contract via a published JSON schema. -- Deterministic local mode plus optional hybrid mode for harder pages. -- Multiple output formats that map well to LLM context pipelines. -- Explicit safety and sanitization options such as `content-safety-off` and `sanitize`. -- Multi-language packaging that makes embedding easier later. - -### What maps well to project-brain - -- A future `ingest-doc` command for PDFs, ADRs, audits, diagrams, and scanned runbooks. -- Normalized artifacts under `AI_CONTEXT` or `docs/ingested/`. -- A structured JSON contract that can be indexed and summarized. -- Sanitization before document artifacts enter prompt context. - -### What does not map well - -- It does not improve repository graph depth, review-delta quality, or proposal governance directly. -- Java and hybrid backend requirements would add operational overhead if pulled into the core engine too early. - -### Decision - -Useful future ingestion boundary. Keep it out of the core orchestrator for now. - -### Recommended follow-up - -Add a separate module later: - -- `project-brain ingest-doc ` -- `project-brain ingest-doc --format markdown,json` - -That stores normalized output in `docs/ingested/` and registers it as optional context. - -## 7. kavach - -### What it is - -`kavach` is a local AI workspace monitor and defensive control layer. It watches file operations, classifies risk, quarantines changes, tracks rollback material, and exposes kill-switch style controls. - -### Code surfaces inspected - -- `kavach/README.md` -- `kavach/src-tauri/src/lib.rs` -- `kavach/src-tauri/src/clipboard.rs` -- `kavach/src-tauri/src/honeypot.rs` - -### What is strong - -- Risk classification is explicit and tied to actual file-system events. -- Quarantine and temporal rollback give a practical recovery path. -- Honeypots and clipboard monitoring extend beyond simple file watching. -- PID chokehold and timeout termination show how approvals can be enforced, not just suggested. -- The product is local-first and designed around hostile or unstable agent behavior. - -### What maps well to project-brain - -- An `Agent Firewall` layer for task risk scoring, tool permissions, and destructive-action approvals. -- Approval gates before write, delete, shell, network, or deploy operations. -- Recovery artifacts such as snapshots, rollback bundles, and audit logs. -- Policy packs such as `safe-readonly`, `review`, `edit-limited`, and `deploy`. - -### What does not map well - -- `kavach` is GPLv3, so `project-brain` should not copy code from it unless you are willing to accept GPL obligations. -- It is an EDR/workspace monitor, not a repository intelligence engine. -- Its Tauri desktop app and OS-specific controls are heavier than what `project-brain` needs in the first pass. - -### Decision - -High-value architectural reference. Adapt the control model, not the code. - -### Recommended follow-up - -Build an internal policy layer with: - -- task risk classifier -- tool access matrix -- approval gates -- audit trail -- rollback hooks for controlled write mode - -## Prioritized roadmap from this comparison - -### Priority 1 - -Intent router and task packets so the user can give simple instructions. - -### Priority 2 - -Agent firewall and approval model inspired by `kavach`. - -### Priority 3 - -Registry-backed context retrieval inspired by `context-hub`. - -### Priority 4 - -Planning and roadmap artifacts inspired by `get-shit-done`. - -### Priority 5 - -Optional OCR/document ingestion inspired by `opendataloader-pdf` and `GLM-OCR`. - -## Copy / adapt / ignore matrix - -| Reference | Copy directly | Adapt carefully | Ignore for now | -| --- | --- | --- | --- | -| get-shit-done | almost nothing | file-based planning state, templates, guided workflow | auto-commits, plugin command sprawl | -| context-hub | annotation simplicity | registry, BM25 search, trust policy, fetch model | broad content universe before curated scope exists | -| code-review-graph | almost nothing line-for-line | graph store model, incremental updates, graph queries | embeddings first, visualization-first workflow | -| claude-octopus | almost nothing line-for-line | intent router, phases, config-driven roles, quality gates | multi-provider complexity, shell-heavy plugin layer | -| opendataloader-pdf | almost nothing line-for-line | document ingestion boundary, schema-backed artifacts, sanitization before context load | pulling OCR/hybrid runtime into the core engine too early | -| kavach | nothing line-for-line | policy engine, approvals, rollback model, audit trail, task risk levels | GPL code reuse, full desktop EDR scope in the first pass | -| GLM-OCR | nothing right now | document ingestion boundary later | OCR in the core engine | - -## Concrete next implementation order - -1. `ask` -2. `task-packet-builder` -3. `agent-firewall` -4. `context-search` -5. `context-get` -6. `plan-improvements` -7. `ingest-doc` diff --git a/docs/reference-repo-analysis/claude-mem-comparison.md b/docs/reference-repo-analysis/claude-mem-comparison.md deleted file mode 100644 index 8660c54..0000000 --- a/docs/reference-repo-analysis/claude-mem-comparison.md +++ /dev/null @@ -1,167 +0,0 @@ -# Claude Mem comparison for project-brain - -## Purpose - -This note captures what `project-brain` should learn from [`claude-mem`](https://github.com/thedotmack/claude-mem) without importing its product assumptions wholesale. - -`claude-mem` is primarily a persistent memory system for AI coding assistants. `project-brain` is a repository intelligence engine focused on safe analysis, persisted context, and review-only outputs. The overlap is real, but the product center of gravity is different. - -## Product difference - -### claude-mem - -- preserves context across sessions -- captures observations during assistant use -- summarizes and retrieves prior context -- optimizes continuity for future interactive sessions - -### project-brain - -- analyzes repositories and codebases -- produces governed context and reports -- persists analysis artifacts for later reuse -- remains review-only and non-destructive by default - -## What is worth importing - -### 1. Structured fact storage - -The strongest transferable idea is not "memory" in the abstract. It is storing small, attributable observations before synthesis. - -For `project-brain`, this supports the fact-based roadmap directly: - -- `verified_facts` -- `unknowns` -- `evidence_refs` - -Recommended direction: - -- add a compact fact store for extracted observations -- persist facts before writing executive or synthesis reports -- keep every fact tied to concrete evidence - -### 2. Relevance-based retrieval - -`claude-mem` emphasizes retrieving context by relevance instead of only by recency. That pattern is useful for `project-brain` in bounded form. - -Recommended direction: - -- use relevant retrieval during `resume` -- use relevant retrieval during final synthesis -- avoid injecting broad historical context into every step - -This should stay optional and scoped to persisted analysis artifacts, not generalized conversation history. - -### 3. Operational status discipline - -`claude-mem` treats runtime health as a first-class concern. `project-brain` should strengthen the same area for analysis runs. - -Recommended direction: - -- expose active and completed run state clearly -- surface stale artifacts -- surface missing module outputs -- surface context quality status -- surface degraded or skipped subsystems explicitly - -### 4. Explicit run identity - -`claude-mem` benefits from a disciplined session model. `project-brain` should formalize run and artifact identity more clearly. - -Recommended direction: - -- `analysis_run_id` -- `module_run_id` -- `resume_checkpoint` -- `artifact_version` - -This makes resume, merge, and artifact replacement easier to reason about. - -### 5. Graceful degradation - -If a non-critical subsystem fails, the primary workflow should continue with a visible degraded status. That pattern transfers cleanly. - -Recommended direction: - -- do not block analysis if retrieval fails -- do not block reporting if enrichment fails -- mark outputs as degraded when supporting subsystems are unavailable - -## What should not be imported directly - -### 1. Permanent daemon architecture - -`claude-mem` uses a worker service, local web viewer, persistent background behavior, and a broader operational footprint. That makes sense for a memory product. - -For `project-brain`, adopting a daemon-first architecture now would add complexity without improving the core analysis workflow enough to justify the cost. - -### 2. Host-hook dependency - -`claude-mem` is designed around assistant lifecycle hooks and plugin integration. `project-brain` should remain host-agnostic and usable as a direct CLI analysis engine. - -### 3. Broad conversational memory - -`claude-mem` captures assistant observations across interactive work. `project-brain` should stay narrower: - -- remember repository facts -- remember analysis artifacts -- remember run state - -It should not evolve into a general conversation memory layer by default. - -### 4. Heavy memory infrastructure too early - -Vector databases, always-on services, and multi-component memory infrastructure may be justified later, but they should not be the first move. - -Start with simpler building blocks first: - -- structured JSON artifacts -- stable artifact schemas -- optional local indexing -- explicit retrieval points - -## Recommended adoption path - -### Phase 1 - -- add structured fact storage -- strengthen synthesis to consume facts instead of freeform summaries -- add context quality reporting -- strengthen `status` with run and artifact health - -### Phase 2 - -- add optional relevance-based retrieval over persisted facts -- use retrieval in `resume` and final synthesis -- formalize run identity and artifact versioning - -### Phase 3 - -- evaluate a lightweight persistent index if retrieval quality or scale requires it -- keep heavy background infrastructure optional, not foundational - -## Alignment with current project-brain principles - -These imports are consistent with current repository principles: - -- analysis-first -- governed outputs -- bounded execution -- review-only recommendations -- cheap/local-first execution where possible - -They are not a justification to convert `project-brain` into an autonomous memory platform. - -## Bottom line - -`claude-mem` is useful as a source of patterns, not as a target architecture. - -The most valuable ideas to adopt are: - -1. structured fact storage -2. relevance-based retrieval -3. stronger runtime and artifact health visibility -4. explicit run identity -5. graceful degradation - -The wrong move would be to copy the daemon, hook, and generalized memory model into the center of `project-brain`. diff --git a/docs/release-checklist.md b/docs/release-checklist.md deleted file mode 100644 index a8a22ed..0000000 --- a/docs/release-checklist.md +++ /dev/null @@ -1,41 +0,0 @@ -# Release checklist - -Use this checklist before tagging a new release. Replace `` with the release version, for example `0.2.1`. - -## Local gates - -- [ ] `git status` contains only intended release changes. -- [ ] `npm ci` passes. -- [ ] `npm run lint` passes. -- [ ] `npm test` passes. -- [ ] `npm run build` passes. -- [ ] `npm audit --audit-level=high` passes. -- [ ] `npm run security:repo` passes. -- [ ] `npm pack --dry-run` includes `dist/cli/project-brain.js`. - -## CLI smoke - -- [ ] `project-brain --help` works. -- [ ] `project-brain go --help` works. -- [ ] `project-brain status --output ` works. -- [ ] `project-brain resume --output ` works. -- [ ] `project-brain fact-query "" --output ` works. -- [ ] `project-brain runbook "" --output ` works. -- [ ] `project-brain doctor --output ` works when environment allows. -- [ ] `project-brain console --help` works without interaction. - -## Remote gates - -- [ ] GitHub Actions are green on Node 20. -- [ ] GitHub Actions are green on Node 22. -- [ ] Dependency review is green. -- [ ] Dependabot has no high/moderate alerts that are fixable without risky major upgrades. -- [ ] Required branch protections match the release process. - -## Release - -- [ ] `CHANGELOG.md` is updated. -- [ ] `docs/releases/.md` is updated. -- [ ] `reports/release-candidate-.md` is updated. -- [ ] Maintainer approves tag creation. -- [ ] Tag `v` is created and pushed. diff --git a/docs/releases/0.2.0.md b/docs/releases/0.2.0.md deleted file mode 100644 index 7013731..0000000 --- a/docs/releases/0.2.0.md +++ /dev/null @@ -1,67 +0,0 @@ -# project-brain 0.2.0 release notes - -Status: internal beta / release candidate. - -## What changed - -`project-brain` 0.2.0 hardens the product from a power-user MVP into a beta candidate for real project validation. The recommended entry is now: - -```bash -project-brain go "understand this project and suggest the next safe step" /path/to/repo --output /path/to/output -``` - -The release focuses on: - -- memory-first analysis -- deterministic preflight before model spend -- bounded swarm presets -- review-only safety -- local CI/security gates -- documented output contracts -- beta validation across representative project types - -## Installation - -From source: - -```bash -npm ci -npm run build -npm link -project-brain --help -``` - -For npm packaging validation before publishing: - -```bash -npm pack --dry-run -``` - -## Validation summary - -Local validation completed successfully for: - -- `npm test` -- `npm run build` -- `npm run lint` -- `npm audit` -- CLI help -- beta matrix commands across backend, frontend, mobile, monorepo, and low-documentation targets - -## Known limitations - -- This release is not a public/commercial 1.0. -- `swarm thorough` should be run selectively because it can spend more model time. -- Ollama and cloud/API providers are optional; deterministic commands continue to work without them. - -## Suggested release commands - -Do not run tag/push until the maintainer explicitly approves. - -```bash -npm version 0.2.0 --no-git-tag-version -git add package.json package-lock.json CHANGELOG.md docs/ reports/ schemas/ .github/ -git commit -m "chore(release): prepare 0.2.0 beta" -git tag v0.2.0 -git push origin main --tags -``` diff --git a/docs/releases/0.2.2.md b/docs/releases/0.2.2.md deleted file mode 100644 index bc628a3..0000000 --- a/docs/releases/0.2.2.md +++ /dev/null @@ -1,28 +0,0 @@ -# project-brain 0.2.2 release notes - -Release date: 2026-05-12 - -## Summary - -`project-brain` 0.2.2 is a release hygiene update after the architecture-plan workflow. It aligns published package metadata, CLI version reporting, README instructions, and release documentation. - -## Changes - -- `project-brain --version` now reads from package metadata instead of a hardcoded CLI string. -- README now documents how to run from source, from compiled `dist`, and via optional `npm link`. -- Release checklist now uses `` placeholders instead of being fixed to `0.2.0`. -- Package metadata and output contract are aligned with `0.2.2`. - -## Validation - -```bash -npm run verify:quick -npm run test -- tests/unit/cli-command-parsing.test.ts -node dist/cli/project-brain.js --version -``` - -Expected CLI version: - -```text -0.2.2 -``` diff --git a/docs/releases/0.2.3.md b/docs/releases/0.2.3.md deleted file mode 100644 index f084ce9..0000000 --- a/docs/releases/0.2.3.md +++ /dev/null @@ -1,40 +0,0 @@ -# project-brain 0.2.3 release notes - -Release date: 2026-05-12 - -## Summary - -`project-brain` 0.2.3 adds the first context-first new project flow. It adapts the guided blueprint pattern from The Architect into Project Brain's persistent `AI_CONTEXT`, `memory`, `docs`, and `tasks` layout. - -## Changes - -- Added `project-brain new ` for creating new project context before application code exists. -- Added `project-brain scaffold-context ` as an alias for the same flow. -- Added interactive prompts for project name, problem, audience, archetype, stack, features, auth, roles, data entities, integrations, priority, and language. -- Added non-interactive flags such as `--yes`, `--name`, `--problem`, `--audience`, `--type`, `--stack`, `--features`, `--auth`, `--roles`, `--data`, and `--integrations`. -- Added console workflow support for creating a new project context. -- Added project seed integration tests and CLI command parsing coverage. - -## Generated Artifacts - -```text -AI_CONTEXT/PROJECT_CHARTER.md -AI_CONTEXT/REQUIREMENTS.md -AI_CONTEXT/PROJECT_BLUEPRINT.md -AI_CONTEXT/DECISIONS.md -AI_CONTEXT/MEMORY_BRIEF.md -AI_CONTEXT/RUNBOOK.md -docs/architecture_plan/BLUEPRINT.md -docs/architecture_plan/STATE.md -memory/project_seed/project_seed.json -tasks/initial_backlog.md -CLAUDE.md -``` - -## Validation - -```bash -npm run verify:quick -npm run test -- tests/integration/project-seed.test.ts tests/unit/cli-command-parsing.test.ts -node dist/cli/project-brain.js new /tmp/project-brain-new-app --yes --name "Inventory SaaS" --problem "Track workshop inventory" --audience "small repair shops" --type saas-webapp --stack "Next.js + PostgreSQL" -``` diff --git a/docs/releases/0.2.4.md b/docs/releases/0.2.4.md deleted file mode 100644 index c79d86e..0000000 --- a/docs/releases/0.2.4.md +++ /dev/null @@ -1,23 +0,0 @@ -# project-brain 0.2.4 release notes - -Release date: 2026-05-13 - -## Summary - -`project-brain` 0.2.4 hardens local swarm output handling and adds review-safe maintenance coverage for agent behavior and unused export analysis. - -## Changes - -- Added malformed/code-only worker response detection in the bounded swarm runtime. -- Added `npm run review:exports` to generate a categorized `ts-prune` review report without deleting exports. -- Added unit coverage for `QAAgent`, `SecurityAgent`, `ObservabilityAgent`, and `OptimizationAgent`. - -## Validation - -```bash -npm run lint -npm run typecheck -npm run build -npm test -npm run review:exports -``` diff --git a/docs/roadmap/evolution-architecture-v2.md b/docs/roadmap/evolution-architecture-v2.md deleted file mode 100644 index c9e3d13..0000000 --- a/docs/roadmap/evolution-architecture-v2.md +++ /dev/null @@ -1,779 +0,0 @@ -# Evolution Architecture V2 - -## Purpose - -This document defines the v2 target architecture for turning `project-brain` into a controlled autonomous improvement engine without modifying external repositories automatically or expanding autonomy into unsafe areas. - -The v2 runtime must replace the current report-only pipeline with a bounded execution loop: - -`Analyze -> Propose -> Patch -> Test -> Evaluate -> Learn` - -The scope of this document is the `project-brain` repository only. - -## Non-negotiable constraints - -- Work only inside `project-brain`. -- Do not modify any external repository automatically. -- Preserve the current CLI surface. -- Keep autonomy bounded to safe improvement classes. -- Never push automatically. -- Never deploy automatically. -- Never modify backend critical logic automatically. -- Every generated patch must go through review. - -## Problems v2 must solve - -The audit established that v1 has these blockers: - -- `ChiefAgent` is not the real runtime coordinator. -- proposals stop at markdown and never become validated patch artifacts. -- learning records are weak and do not shape future behavior meaningfully. -- observability is coarse and partially inaccurate. -- governance is descriptive, not execution-enforcing. -- the multi-agent model is centralized and mostly non-interactive. - -V2 must solve those without breaking current command usage. - -## Target runtime - -### Effective runtime flow - -The target runtime flow is: - -`CLI -> ChiefAgent -> Agents -> EvolutionEngine` - -For backward compatibility, the existing `ProjectBrainOrchestrator` class should remain as a compatibility facade during migration, but it must delegate into `ChiefAgent` rather than own the end-to-end flow itself. - -### Updated architecture diagram - -```mermaid -flowchart TD - CLI["CLI Commands"] --> COMPAT["ProjectBrainOrchestrator (Compatibility Facade)"] - COMPAT --> CHIEF["ChiefAgent Runtime"] - - CHIEF --> DISC["Discovery Engine"] - CHIEF --> CTX["Context Builder"] - CHIEF --> REG["Agent Registry"] - CHIEF --> PLAN["Task + Proposal Planner"] - CHIEF --> GOV["Safety Policy Engine"] - CHIEF --> EV["Evolution Engine"] - CHIEF --> TEL["Telemetry Service"] - CHIEF --> MEM["Learning + Memory Services"] - - REG --> AGENTS["Specialist Agents"] - DISC --> AGENTS - CTX --> AGENTS - MEM --> AGENTS - - AGENTS --> PROP["Structured Proposals"] - PLAN --> PROP - GOV --> PROP - - PROP --> EV - EV --> PATCH["Patch Generator"] - EV --> CI["CI Feedback Runner"] - EV --> REVIEW["Review Artifact Writer"] - EV --> OUTCOME["Outcome Evaluator"] - - PATCH --> BRANCH["Local Feature Branch / Worktree"] - PATCH --> DIFF["Patch Artifact"] - CI --> TESTS["Build / Typecheck / Tests / Smoke"] - TESTS --> OUTCOME - OUTCOME --> MEM - OUTCOME --> TEL - REVIEW --> REPORTS["Evolution Reports"] - - GOV --> BLOCK["Unsafe Change Blocked"] -``` - -## Design principles - -### 1. Safety before autonomy - -V2 is not allowed to become a blind auto-fixer. It may generate and validate patch artifacts only within explicit safe scopes. - -### 2. Structured state over markdown state - -Markdown remains a reporting output, not the internal control model. Internal execution must use structured proposal, patch, validation, and learning records. - -### 3. Deterministic tools before model inference - -Discovery, patch generation, validation, and policy enforcement should be deterministic first. Any future model integration must be grounded by these outputs. - -### 4. ChiefAgent is the real control plane - -V2 must make `ChiefAgent` the actual runtime coordinator, not a vestigial abstraction. - -### 5. Learning must come from outcomes - -Proposal quality cannot improve from self-authored scores alone. It must improve from validated patch results and explicit review outcomes. - -## New module structure - -```text -project-brain/ - core/ - chief_agent/ - index.ts - cycle-controller.ts - proposal-merger.ts - safety-gate.ts - priority-engine.ts - evolution_engine/ - index.ts - evolution-cycle.ts - proposal-normalizer.ts - patch-planner.ts - outcome-evaluator.ts - review-stage.ts - ci_feedback/ - index.ts - local-validator.ts - result-parser.ts - feedback-recorder.ts - tools/ - patch_generator/ - index.ts - branch-manager.ts - patch-builder.ts - scope-classifier.ts - file-guards.ts - memory/ - learning_store/ - index.ts - outcome-memory.ts - agent-adjustments.ts - analysis/ - metrics/ - metrics_collector.ts - evolution_metrics.ts - reports/ - templates/ - evolution_cycle.md - patch_results.md - agent_performance.md -``` - -## Core responsibilities - -## 1. `core/chief_agent/` - -`ChiefAgent` becomes the real coordinator. - -Responsibilities: - -- load repository scope and context -- schedule the correct agents for the trigger -- provide agents with memory-informed context -- collect agent findings and structured proposals -- merge overlapping proposals -- prioritize safe improvements -- block unsafe proposals before patch generation -- hand approved structured proposals to `EvolutionEngine` -- emit cycle-level summaries and metrics - -Required subcomponents: - -- `cycle-controller.ts`: owns per-cycle state and removes the current long-lived mutable runtime problem -- `proposal-merger.ts`: deduplicates or merges similar agent proposals -- `safety-gate.ts`: blocks unsafe operations before patch generation -- `priority-engine.ts`: ranks proposals by value, safety, confidence, and past success rate - -### ChiefAgent cycle contract - -Input: - -- trigger -- repository target -- output path -- prior learning summary - -Output: - -- structured cycle summary -- proposal set -- blocked proposal set -- evolution outcomes -- new reports - -## 2. `core/evolution_engine/` - -This is the missing execution loop. - -Responsibilities: - -- accept structured proposals from `ChiefAgent` -- transform proposals into patch plans -- invoke patch generation only for safe scopes -- run validation through `core/ci_feedback` -- classify outcomes -- record learnings -- write review artifacts and reports - -### EvolutionEngine main flow - -1. accept normalized proposals -2. classify each proposal by scope and risk -3. reject or defer unsafe proposals -4. create patch jobs for safe proposals -5. generate local branch or worktree and patch artifact -6. run build, typecheck, tests, and smoke checks -7. evaluate outcome -8. update learning store -9. produce review-ready results - -### Required records - -The engine must emit durable records for: - -- proposal -- patch -- validation result -- outcome -- review status - -## 3. `tools/patch_generator/` - -This module should be deterministic and policy-restricted. - -Responsibilities: - -- create local feature branches or isolated worktrees -- generate git patch artifacts -- stage safe modifications -- refuse unsafe file classes -- never push -- never open remote pull requests automatically - -### Safe scopes for automatic patch generation - -Allowed: - -- frontend UI changes -- documentation changes -- configuration updates -- test additions -- non-critical observability improvements - -Blocked by default: - -- database schema changes -- authentication and authorization logic -- financial logic -- deployment pipelines -- infrastructure provisioning -- backend critical business rules - -### Required helpers - -- `scope-classifier.ts`: determines whether a proposal is `frontend`, `docs`, `config`, `tests`, `backend-critical`, or `blocked` -- `file-guards.ts`: prevents writes to blocked paths or blocked diff classes -- `branch-manager.ts`: creates predictable local branches such as `codex/evolution//` -- `patch-builder.ts`: writes patch files and staged working tree changes - -## 4. `core/ci_feedback/` - -This module closes the validation gap. - -Responsibilities: - -- run local validation commands -- capture build, typecheck, test, lint, and smoke status -- normalize results into structured records -- feed outcomes into learning and telemetry - -### Validation policy - -Minimum checks for safe patch classes: - -- `build` if available -- `typecheck` if available -- unit or integration tests if available -- smoke tests when supported by the repo - -Validation should be capability-driven. If a repo lacks a tool, that absence must be recorded, not silently ignored. - -### Required outputs - -- command run list -- exit codes -- stdout/stderr artifact references -- tests passed / tests failed -- validation summary per patch - -## 5. `memory/learning_store/` - -The current learning store is too weak. V2 must upgrade it from general observations to outcome-linked execution memory. - -### Required learning entry shape - -Each learning entry must store: - -- `proposal_id` -- `patch_id` -- `result` (`success` or `failure`) -- `tests_passed` -- `impact_estimate` -- `confidence_adjustment` - -It should also retain: - -- `agent_id` -- `task_id` -- `scope` -- `risk_level` -- `blocked_reason` when relevant -- `created_at` - -### New responsibilities - -- maintain outcome history by agent and proposal class -- compute confidence adjustments from actual outcomes -- expose agent-level success rates by scope -- expose failure clusters by file type, repo type, and validation step - -### Agent behavior adjustment - -Agents must adapt future proposals using outcome memory: - -- reduce confidence for proposal classes with repeated failures -- increase confidence for proposal classes with repeated success -- deprioritize scopes with low validation pass rate -- highlight proposal templates that perform well for a given repo type - -This should begin with deterministic adjustment rules before any future model tuning. - -## Proposal, patch, and outcome model - -V2 should add typed records in shared types or a new evolution types module. - -### `StructuredProposal` - -Required fields: - -- `proposalId` -- `sourceAgentIds` -- `title` -- `summary` -- `scope` -- `targetFiles` -- `riskLevel` -- `safeToPatch` -- `blockedReason` -- `expectedBenefit` -- `confidence` -- `patchStrategy` - -### `PatchArtifact` - -Required fields: - -- `patchId` -- `proposalId` -- `branchName` -- `patchFilePath` -- `targetFiles` -- `scope` -- `createdAt` -- `appliedLocally` -- `reviewStatus` - -### `ValidationResult` - -Required fields: - -- `patchId` -- `buildPassed` -- `typecheckPassed` -- `testsPassed` -- `smokePassed` -- `failedSteps` -- `commandResults` -- `durationMs` - -### `EvolutionOutcome` - -Required fields: - -- `proposalId` -- `patchId` -- `result` -- `testsPassed` -- `impactEstimate` -- `confidenceAdjustment` -- `agentAccuracyScoreDelta` -- `recordedAt` - -## Safety guardrails - -V2 must enforce these as runtime policy, not report text. - -### Hard blocks - -Agents may not automatically: - -- modify database schemas -- modify authentication or authorization -- modify financial logic -- deploy code -- push directly to `main` -- push to any remote automatically - -### Review stage rules - -Every patch, even successful ones, must pass through a review stage. - -Review artifacts must include: - -- proposal summary -- affected files -- patch path -- validation summary -- safety classification -- blocked reasons if applicable - -### Allowed autonomous classes - -Initial v2 autonomous patch classes: - -- docs -- frontend presentation -- configuration hardening -- test baseline additions -- low-risk observability changes outside critical backend logic - -### Required policy behavior - -If a proposal touches both safe and unsafe areas, the entire patch job is blocked and emitted as review-only. - -## Observability upgrade - -Extend `analysis/metrics/metrics_collector.ts` with v2 metrics: - -- `proposals_generated` -- `patches_applied` -- `patch_success_rate` -- `agent_accuracy_score` -- `learning_updates` - -Additional recommended metrics: - -- blocked proposal count -- safe proposal count -- validation duration -- validation failure rate by step -- patch success rate by scope -- agent proposal acceptance rate by scope - -### Metric ownership - -- `ChiefAgent`: cycle-level planning metrics -- `EvolutionEngine`: proposal-to-patch and patch-to-outcome metrics -- `CI Feedback`: validation metrics -- `LearningStore`: learning update metrics - -## New report types - -V2 must generate: - -- `reports/evolution_cycle.md` -- `reports/patch_results.md` -- `reports/agent_performance.md` - -### `evolution_cycle.md` - -Should contain: - -- cycle trigger and target -- proposals received -- proposals blocked -- patches generated -- validations run -- success/failure summary -- learning updates applied - -### `patch_results.md` - -Should contain: - -- patch ID to proposal ID mapping -- branch names -- files changed -- validation results -- blocked patches -- review status - -### `agent_performance.md` - -Should contain: - -- proposals per agent -- successful patch rate per agent -- blocked rate per agent -- confidence adjustments -- agent accuracy trend - -## Backward compatibility with the current CLI - -The current command surface must remain intact: - -- `init` -- `analyze` -- `agents` -- `weekly` -- `report` -- `feedback` - -### Compatibility strategy - -#### Phase 1 - -Keep `ProjectBrainOrchestrator` as a facade, but change its responsibilities: - -- delegate planning and execution to `ChiefAgent` -- stop owning governance-heavy flow directly -- continue returning the current result shapes where possible - -#### Phase 2 - -Extend result types with optional v2 fields: - -- `evolutionReportPath` -- `patchResultsPath` -- `agentPerformancePath` -- `proposalCount` -- `patchCount` -- `blockedCount` - -#### Phase 3 - -Add new CLI output lines, but do not remove existing ones. Example: - -- `Evolution report: ...` -- `Patch results: ...` -- `Agent performance: ...` - -This keeps scripts and current human usage stable. - -## Migration from current architecture - -### Current runtime - -`CLI -> ProjectBrainOrchestrator -> AgentSelfGovernanceSystem` - -### Target runtime - -`CLI -> ProjectBrainOrchestrator (compat) -> ChiefAgent -> EvolutionEngine` - -### Migration steps - -#### Step 1. Stabilize cycle boundaries - -- move per-cycle state out of long-lived governance instances -- eliminate accumulated execution records -- make each cycle allocate fresh state containers - -#### Step 2. Promote `ChiefAgent` into the real runtime coordinator - -- move scheduling, selection, and cycle control into `core/chief_agent/` -- make orchestrator delegate to it -- keep old method names for compatibility - -#### Step 3. Introduce structured proposals - -- stop treating recommendations as the only proposal representation -- normalize agent outputs into `StructuredProposal` - -#### Step 4. Add `EvolutionEngine` - -- convert safe structured proposals into patch jobs -- defer unsafe ones to review-only - -#### Step 5. Add `PatchGenerator` - -- create local branch/worktree support -- build patch files -- enforce path and scope guards - -#### Step 6. Add `CI Feedback` - -- run local validations -- capture structured results -- store outcomes - -#### Step 7. Upgrade learning store - -- add proposal-to-patch-to-outcome linkage -- add confidence adjustment logic -- surface agent accuracy metrics - -#### Step 8. Extend observability and reporting - -- add new evolution metrics -- add evolution reports -- keep existing reports during transition - -#### Step 9. Retire or demote v1 governance-only paths - -- keep `AgentSelfGovernanceSystem` only as a legacy adapter during transition -- eventually split reusable parts into `ChiefAgent` and policy modules - -## Implementation plan - -## Phase A. Architecture alignment - -Goal: - -Make runtime architecture honest before adding execution. - -Tasks: - -- create `core/chief_agent/` -- move cycle orchestration into `ChiefAgent` -- make `ProjectBrainOrchestrator` a facade -- isolate per-run mutable state -- define new evolution types - -Exit criteria: - -- runtime path effectively goes through `ChiefAgent` -- current CLI still works -- telemetry is per-cycle accurate - -## Phase B. Structured proposal pipeline - -Goal: - -Convert agent recommendations into executable-safe proposal records. - -Tasks: - -- define `StructuredProposal` -- add proposal normalization in `ChiefAgent` -- add proposal merging and prioritization -- add safety classification and blocked-reason handling - -Exit criteria: - -- every candidate change is typed and classified before patch generation - -## Phase C. Safe patch generation - -Goal: - -Generate local, review-only patch artifacts for safe scopes. - -Tasks: - -- create `tools/patch_generator/` -- add branch or worktree creation -- generate patch files -- refuse blocked scopes -- support frontend/docs/config/test-safe changes - -Exit criteria: - -- safe proposals become local patch artifacts -- unsafe proposals never become applied changes - -## Phase D. CI feedback loop - -Goal: - -Validate generated patches and capture outcomes. - -Tasks: - -- create `core/ci_feedback/` -- run build/typecheck/test/smoke pipelines -- normalize results -- attach results to patches and proposals - -Exit criteria: - -- every generated patch has a validation record - -## Phase E. Learning loop - -Goal: - -Make agents adapt future confidence from outcomes. - -Tasks: - -- upgrade `memory/learning_store/` -- store proposal and patch IDs -- compute confidence adjustments -- expose agent accuracy and success rates to `ChiefAgent` - -Exit criteria: - -- proposal ranking changes based on actual prior success/failure - -## Phase F. Evolution reports and metrics - -Goal: - -Make autonomous improvement behavior observable. - -Tasks: - -- extend metrics collector with v2 metrics -- generate `evolution_cycle.md` -- generate `patch_results.md` -- generate `agent_performance.md` - -Exit criteria: - -- each cycle explains what was proposed, attempted, blocked, validated, and learned - -## Safe initial rollout policy - -V2 should launch in three operating modes: - -### `analyze-only` - -- current behavior -- no patch jobs -- no validation loop - -### `review-ready` - -- generates structured proposals and patch artifacts -- runs validation -- never applies staged changes to external repos outside local review artifacts - -### `bounded-autonomous` - -- only for explicitly allowed safe scopes -- still review-gated -- no remote actions - -The default should remain the equivalent of `analyze-only` until Phases A through F are complete. - -## Recommended first implementation slice - -The smallest useful v2 slice is: - -1. make `ChiefAgent` real -2. add `StructuredProposal` -3. add `EvolutionEngine` as review-only -4. add `CI Feedback` validation records -5. upgrade learning entries with proposal and patch outcomes - -That produces a real improvement loop without yet expanding autonomy too far. - -## Final architectural verdict - -`project-brain` should not jump directly from report generation to self-directed repo modification. - -The correct v2 path is: - -- centralize control in `ChiefAgent` -- formalize proposal state -- generate only safe local patches -- validate every patch -- learn from outcomes -- require review for every patch - -If implemented in that order, `project-brain` can evolve from a repository analysis framework into a controlled autonomous improvement engine without abandoning its current safety posture. diff --git a/docs/roadmap/evolution-plan.md b/docs/roadmap/evolution-plan.md deleted file mode 100644 index d0d84e0..0000000 --- a/docs/roadmap/evolution-plan.md +++ /dev/null @@ -1,440 +0,0 @@ -# Evolution Plan - -# PROJECT-BRAIN v2 - -## Design goal - -Turn `project-brain` from a governed analysis pipeline into a safe autonomous improvement system that can: - -- detect issues -- propose changes -- generate patches in isolation -- validate them automatically -- learn from accepted and rejected outcomes -- evolve agent behavior over time without losing control - -## Guiding principle - -Do not jump directly from report generation to unrestricted autonomy. - -The realistic path is: - -1. make orchestration explicit -2. make memory actionable -3. make proposals executable in sandboxes -4. make validation mandatory -5. make learning outcome-driven -6. only then allow bounded autonomous improvement - -## v2 target architecture - -### 1. Split the runtime into control plane and execution plane - -Create explicit services instead of one large governance runtime. - -Control plane: - -- `CycleCoordinator` -- `PolicyEngine` -- `AgentRegistry` -- `TaskPlanner` -- `EvaluationEngine` -- `MemoryService` -- `TelemetryService` - -Execution plane: - -- `DiscoveryWorker` -- `AgentRunner` -- `PatchRunner` -- `ValidationRunner` -- `RepoSandboxManager` - -Why: - -The current `AgentSelfGovernanceSystem` mixes planning, execution, scoring, learning, proposal rendering, and persistence. That centralization is already a bottleneck. - -### 2. Replace static agent classes with manifest-driven agents - -Each agent should declare a manifest containing: - -- `agentId` -- version -- supported languages and repo types -- trigger policies -- required memory views -- required tools -- output schema -- safety classification -- evaluation rubric - -Keep a narrow runtime contract: - -- `prepare()` -- `analyze()` -- `propose()` -- `review()` -- `learn()` - -Why: - -This removes governance hardcoding from the central runtime and lets new agents plug in without editing priority matrices and helper switches everywhere. - -### 3. Introduce typed shared state instead of markdown-only coordination - -Use a shared cycle state object with schemas for: - -- discovery facts -- repository map -- issue hypotheses -- ranked risks -- candidate proposals -- validation jobs -- human decisions -- outcome records - -Keep markdown as an output artifact, not as the system's primary internal state. - -Why: - -Right now agents mostly exchange text and the system mostly persists text. That prevents strong planning and validation loops. - -### 4. Build a real memory architecture - -#### Episodic memory - -Store every cycle as a structured episode: - -- repo snapshot hash -- trigger -- agents run -- issues found -- proposals generated -- approvals -- patches attempted -- validations passed or failed -- final outcome - -#### Semantic memory - -Add a retrievable knowledge layer keyed by: - -- stack -- framework -- issue pattern -- module type -- repository class - -This can be a vector-backed or embedding-backed store later, but v2 only needs a retrieval abstraction. - -#### Outcome memory - -Track what actually happened after proposals: - -- accepted -- rejected -- false positive -- patch failed tests -- patch regressed behavior -- patch reduced incident count - -#### Agent memory - -Keep per-agent behavior histories: - -- precision by issue class -- acceptance rate by proposal type -- validation success rate -- false-positive clusters -- best-performing heuristics by repo archetype - -Why: - -Without outcome-linked memory, the system cannot improve agent quality in a meaningful way. - -### 5. Add a real self-improvement loop for agents - -This loop must target agent behavior, not just repository code. - -For each agent: - -1. collect validated outcomes from previous runs -2. identify failure patterns such as false positives, missed issues, or low-value proposals -3. generate candidate heuristic or prompt revisions in a sandbox branch of the agent configuration, not the target repository -4. replay the agent against benchmark repositories and historical episodes -5. compare precision, recall proxy metrics, approval rate, and validation pass rate -6. promote the new agent version only if it beats the current version under policy constraints - -Outputs needed: - -- agent version registry -- benchmark suite -- evaluation harness -- rollout policy -- rollback path - -Why: - -This is the missing capability that separates a static analyzer from an evolving autonomous system. - -### 6. Add bounded LLM integration where it matters - -Current v1 has no LLM layer. v2 should introduce one carefully. - -Use LLMs for: - -- hypothesis generation from discovery and code evidence -- patch planning -- proposal summarization -- risk explanation -- postmortem synthesis -- agent self-improvement proposal drafting - -Do not use LLMs for: - -- unconstrained repository writes -- final approval decisions -- validation truth - -LLM outputs must always be grounded by: - -- repository facts -- tool outputs -- diff context -- validation results -- policy rules - -Recommended runtime pattern: - -- deterministic tools first -- LLM planning second -- validation tools last -- policy gate before any patch promotion - -Why: - -The current system's heuristics are too weak for a true engineering agent, but unrestricted LLM execution would be unsafe and noisy. - -### 7. Add a repository sandbox and patch validation pipeline - -To become an improvement engine, v2 needs a safe execution path: - -1. create isolated working copy or ephemeral branch -2. generate patch candidate -3. run repository-specific validations -4. record exact command outputs and diff summary -5. classify result -6. only surface successful candidates for human review or bounded auto-merge policy - -Validation gates should include: - -- build -- typecheck -- tests -- linters -- smoke checks -- security checks where relevant - -Why: - -Without a patch-validation loop, the system cannot progress beyond recommendations. - -### 8. Replace message logging with an event bus - -Messages should become actionable events with subscribers. - -Examples: - -- `IssueDetected` -- `ProposalCreated` -- `ValidationRequested` -- `ValidationFailed` -- `HumanApproved` -- `FalsePositiveConfirmed` -- `AgentVersionPromoted` - -Agents and services should be able to subscribe to those events and react in later phases of the same cycle or in future cycles. - -Why: - -The current message center records narrative communication, but nothing consumes it. - -### 9. Make scheduling real - -Add a scheduler service that persists: - -- cycle definitions -- repository subscriptions -- next run time -- retry policy -- cooldown windows -- suppression rules -- concurrency limits - -Supported schedules should include: - -- repository-change hooks -- daily hygiene scans -- weekly architecture reviews -- incident-triggered deep dives -- post-merge validation sweeps - -Why: - -The current scheduler only selects agents after a run has already been triggered. - -### 10. Strengthen governance into policy-as-code - -Move from keyword heuristics to explicit policy rules: - -- which repos allow autonomous patch generation -- which directories are writable -- which agents can propose code vs docs vs tests -- required validations by proposal type -- required human approval by risk class -- promotion rules for agent-version changes - -Add immutable audit artifacts: - -- proposal record -- diff record -- validation record -- approval record -- rollback record - -Why: - -A true autonomous engineering system needs enforceable policy, not just descriptive safety text. - -## Proposed v2 phases - -## Phase 1: Stabilize v1 into a trustworthy control plane - -Ship first: - -- isolate cycle state per run -- eliminate supervisor execution-record leakage -- make proposal history append-only -- make task board historical instead of overwrite-only -- turn messages into typed events even before full subscriptions -- make workspace discovery monorepo-aware -- remove dead runtime abstractions or wire them properly - -Success criteria: - -- clean per-cycle metrics -- repeatable historical audit trail -- accurate repository targeting -- no stale architectural abstractions in docs vs runtime - -## Phase 2: Introduce structured memory and evaluation - -Ship next: - -- episodic cycle store -- proposal outcome store -- per-agent quality metrics -- benchmark repositories for agent regression tests -- validation corpus for false-positive and missed-issue tracking - -Success criteria: - -- agent quality can be measured over time -- historical outcomes are queryable -- learnings become operational inputs, not just records - -## Phase 3: Add sandboxed patch generation - -Ship next: - -- patch planner -- sandbox working copies -- validation runner -- result classifier -- diff artifact storage - -Success criteria: - -- system can produce validated candidate changes safely -- failed patches do not contaminate repo state -- successful candidates are grounded by tool-based evidence - -## Phase 4: Add bounded autonomous improvement - -Ship next: - -- repo-level autonomy policies -- low-risk autopilot for docs/tests/config-only scopes -- human-required review for medium and high risk -- automatic rollback from failed post-merge signals - -Success criteria: - -- real improvements can be made continuously in development environments -- governance remains enforceable -- outcome learning improves future patch precision - -## Phase 5: Add agent self-evolution - -Ship last: - -- agent version registry -- evaluation harness for agent revisions -- replay engine over historical episodes -- promotion and rollback rules for agent versions - -Success criteria: - -- agents improve based on validated outcomes -- regressions in agent quality are measurable and reversible -- the platform evolves itself under control - -## PROJECT-BRAIN v2 reference architecture - -Suggested top-level modules: - -- `runtime/` -- `policy/` -- `events/` -- `agents/` -- `memory/episodes/` -- `memory/outcomes/` -- `memory/retrieval/` -- `execution/sandbox/` -- `execution/validation/` -- `evaluation/` -- `benchmarks/` -- `integrations/repository/` -- `integrations/models/` - -## What must remain from v1 - -Keep these v1 strengths: - -- non-destructive default posture -- filesystem-readable artifacts -- trigger-based execution model -- clear agent descriptors -- useful repository discovery baseline -- strong static-analysis orientation in DevAgent -- simple CLI entrypoints - -## What must be removed or demoted - -- reliance on markdown as internal state -- hardcoded agent IDs across governance logic -- single giant governance runtime class -- pseudo-communication with no consumers -- synthetic learning outcomes as if they were validated truth -- stale abstractions such as unused coordinator layers - -## Final recommendation - -Do not market v1 as an autonomous improvement engine. - -Ship v2 in two labels: - -- `project-brain analyze`: stable analyzer mode -- `project-brain improve`: sandboxed autonomous improvement mode, only after Phases 1 through 4 are complete - -That keeps the architecture honest and gives the system a realistic path from analysis tooling to autonomous engineering. diff --git a/docs/roadmap/fact-based-context-roadmap.md b/docs/roadmap/fact-based-context-roadmap.md deleted file mode 100644 index 6ccaec3..0000000 --- a/docs/roadmap/fact-based-context-roadmap.md +++ /dev/null @@ -1,262 +0,0 @@ -# Fact-Based Context Roadmap - -## Purpose - -Improve `project-brain` so repository context is built from verifiable facts instead of narrative inference. - -This roadmap is intentionally general. It does not assume any specific workspace shape such as frontend/backend/mobile/panel. It focuses on product-wide improvements that apply to single-repository analysis first, with optional multi-repository support later. - -## Core rule - -Anything presented as a fact must be backed by one or more verifiable sources: - -- source files -- configuration files -- manifests and lockfiles -- route definitions -- OpenAPI or Swagger artifacts -- SQL schema or schema dumps when explicitly provided -- generated tool outputs with stable provenance - -If evidence is missing, the system should emit: - -- `unknown` -- `not detected` -- `not verified` - -It should not fill the gap with architectural guesses. - -## Product goals - -The improved context layer should answer these questions reliably: - -1. What is this repository and what stack does it actually use? -2. Which modules or surfaces are visibly implemented? -3. Which integrations are confirmed by code or configuration? -4. Which gaps or drift signals are directly observable? -5. Which outputs are canonical and which are historical? - -## Non-goals - -- Do not make cross-repository comparison mandatory for every run. -- Do not make SQL or schema ingestion mandatory when no schema exists. -- Do not rely on LLM-only summarization for endpoints, auth, contracts, or integration compatibility. - -## Design principles - -### 1. Facts first - -Every summary should be structured around: - -- `verified_facts` -- `unknowns` -- `evidence_refs` - -### 2. Extract, then summarize - -For contracts, routes, auth, schema, headers, uploads, and manifests, use deterministic extractors first. Let the LLM summarize extracted data rather than inventing structure from raw code. - -### 3. Unknown is better than wrong - -When the system cannot confirm a relationship, it should say so explicitly instead of presenting a likely-but-unverified explanation. - -### 4. Single-repo quality before workspace intelligence - -The default mode should become more trustworthy for one repository before adding a broader workspace alignment mode. - -## Backlog - -## Phase 1 - -Focus: strengthen evidence discipline in current outputs. - -### 1. Fact-evidence policy - -Define a shared internal contract for analysis outputs: - -- `verified_facts: string[]` -- `unknowns: string[]` -- `evidence_refs: string[]` - -This applies to summaries, executive outputs, and synthesis artifacts. - -### 2. Prompt hardening - -Update swarm and summary prompts so they must: - -- cite real files, routes, or config when possible -- avoid stating unverified relationships as facts -- downgrade uncertainty to `unknown` or `not verified` - -### 3. Synthesis schema upgrade - -Extend synthesizer outputs to carry: - -- `headline` -- `summary` -- `verified_facts` -- `unknowns` -- `priorities` -- `next_steps` -- `evidence_refs` - -### 4. Context quality gate - -Add a new report: - -- `reports/context_quality_gate.md` - -The gate should verify at minimum: - -- stack was identified from evidence -- outputs contain verifiable facts -- unknowns are called out explicitly -- file references are present where expected -- historical or archived outputs are not presented as active - -## Phase 2 - -Focus: add deterministic repo-level extractor outputs. - -### 5. Technical contract minimum - -Add a structured contract artifact: - -- `reports/technical_contract_minimum.md` - -It should summarize, when visible: - -- stack -- entrypoints -- API prefix or base path -- visible endpoints or route surfaces -- auth mechanism -- required headers -- file upload surfaces -- evidence references - -### 6. Integration drift report - -Add a repo-level drift report: - -- `reports/integration_drift_report.md` - -This should be based on extractor output, not LLM freeform reasoning. - -Examples of drift signals: - -- client-visible paths that do not match exposed paths -- auth/header mismatch across visible surfaces -- endpoints referenced but not exposed -- schema-visible entities with no visible service wiring - -### 7. Maturity profile - -Add: - -- `analysis/maturity_profile.md` - -Classify visible modules or surfaces using evidence-backed states: - -- `active` -- `partial` -- `skeleton` -- `legacy` - -The classification should depend on signals such as: - -- executable entrypoints -- wiring and route usage -- tests -- config presence -- data flow visibility - -### 8. Extractor layer - -Build or formalize deterministic extractors for: - -- manifests and lockfiles -- environment and config -- routes and endpoints -- auth middleware or guards -- uploads and files -- SQL schema, when explicitly provided - -These extractors should emit machine-readable JSON under a dedicated output area such as: - -- `AI_CONTEXT/extractors/` - -## Phase 3 - -Focus: optional advanced capabilities. - -### 9. Schema ingestion - -Add optional schema-aware analysis when SQL or schema dumps are explicitly provided. - -Suggested outputs: - -- `reports/schema_context.md` -- `AI_CONTEXT/schema/schema_index.json` - -### 10. Workspace mode - -Introduce an optional workspace-level mode for analyzing multiple related repositories together. - -This should not replace the existing single-repo flow. - -Suggested outputs: - -- `reports/workspace_alignment.md` -- `reports/domain_coverage.md` - -### 11. Cross-repo drift - -Inside workspace mode, compare verified integration surfaces across repositories: - -- route prefixes -- visible clients vs exposed APIs -- shared auth assumptions -- schema vs service visibility - -## Prioritization - -### High priority - -- fact-evidence policy -- prompt hardening -- synthesis schema upgrade -- context quality gate - -### Medium priority - -- technical contract minimum -- integration drift report -- maturity profile -- extractor layer - -### Lower priority - -- schema ingestion -- workspace mode -- cross-repo drift - -## Acceptance criteria - -A phase is only complete if: - -1. the output artifact exists in a stable location -2. the artifact cites verifiable evidence -3. the system uses `unknown` or `not verified` instead of unsupported claims -4. the output works for a single repository without depending on workspace-specific assumptions - -## Suggested first implementation slice - -The highest-value first slice is: - -1. fact-evidence policy -2. prompt hardening -3. synthesis schema upgrade -4. context quality gate - -That combination improves trustworthiness immediately without forcing a deeper architectural rewrite. diff --git a/docs/roadmap/token-aware-orchestration.md b/docs/roadmap/token-aware-orchestration.md deleted file mode 100644 index 1e6095a..0000000 --- a/docs/roadmap/token-aware-orchestration.md +++ /dev/null @@ -1,158 +0,0 @@ -# Token-Aware Orchestration - -## Purpose - -Make `project-brain` more powerful while spending fewer model tokens. - -The core principle is simple: deterministic facts and persisted artifacts should be consulted before broad model analysis. Expensive model calls should be used for synthesis, prioritization, and ambiguous judgment, not for rediscovering repository shape on every run. - -## Current useful foundation - -`project-brain` already has several building blocks for this: - -- `map-codebase` creates a structural repository map. -- `code-graph` creates `memory/code_graph/code_graph_v2.json`. -- `code-graph` also creates `memory/knowledge_graph/repository_fact_graph.json`. -- `initTarget` refreshes `AI_CONTEXT/MEMORY_BRIEF.md` and `memory/memory_brief/memory_brief.json`. -- `status` and `resume` track available artifacts. -- `runbook` orders cheap deterministic steps before model-heavy workflows. -- `swarm` has response caching and learned scope boosts. -- `AI_CONTEXT`, `reports`, `docs`, `memory`, and `tasks` give stable artifact locations. - -The main gap is orchestration discipline: every workflow should decide what can be answered from existing facts before sending context to a model. - -## Recommended execution order - -### 1. Bootstrap - -Run cheap and deterministic steps first: - -```bash -project-brain doctor . --output /path/to/output -project-brain map-codebase . --output /path/to/output -project-brain code-graph . --output /path/to/output -project-brain status . --output /path/to/output -``` - -### 2. Reuse - -Before any broad `swarm`, call: - -```bash -project-brain resume . --output /path/to/output -``` - -The resume path should prefer existing factual artifacts: - -- memory brief -- codebase map -- repository fact graph -- previous swarm memory -- improvement plan -- impact radius - -### 3. Target - -Only after reusable context exists, run bounded model analysis: - -```bash -project-brain swarm "" . --output /path/to/output -``` - -Use narrow scopes when possible: - -- smaller `--chunk-size` -- lower `--max-queued-tasks` -- explicit module paths in the user intent -- local budget mode for quick scans - -## Token-saving rules - -### Apply one global token policy - -All model-facing requests should pass through a small shared token policy before execution. The policy should stay short because it is injected into prompts globally. - -The policy should enforce: - -- concise structured output -- no repeated prompt text -- no invented paths, APIs, versions, entities, or relationships -- `UNKNOWN` for missing evidence -- smallest useful task set for planners -- deduplication during synthesis - -### Prefer facts over summaries - -Use structured facts as prompt input: - -- memory brief -- file paths -- manifests -- routes -- symbols -- imports -- graph edges -- generated artifact paths - -Avoid sending long markdown reports when a compact JSON index can answer the same question. - -### Unknown beats guessed - -If a relationship is not visible in code, config, schema, or generated artifacts, the system should emit `unknown` or `not verified`. - -### Models should synthesize, not rediscover - -Broad discovery should be deterministic. Model calls should be reserved for: - -- prioritization -- tradeoff explanation -- synthesis across verified facts -- ambiguous architecture review - -### Cache only stable prompts - -The swarm cache is useful only if prompts are stable. Prompt inputs should depend on: - -- git commit -- target path -- selected model -- intent -- compact factual context - -Avoid putting volatile timestamps or large markdown blobs into cached prompts. - -## Immediate implementation backlog - -### Phase 1 - -- Make `status` and `resume` aware of the repository fact graph. -- Recommend `code-graph` before broad swarm runs. -- Extend swarm worker output with: - - `verified_facts` - - `unknowns` - - `evidence_refs` -- Extend swarm synthesis with the same fact-aware fields. - -### Phase 2 - -- Add a `fact-store` artifact fed by deterministic extractors. -- Add `context_quality_gate` to block low-evidence summaries from becoming canonical. -- Add compact context selection for swarm workers from fact graph and fact store. - -### Phase 3 - -- Add a query command over factual artifacts: - - `project-brain fact-query` - - `project-brain graph-query` -- Add optional semantic retrieval over verified facts. -- Use retrieval in `ask`, `resume`, and synthesis before reading large reports. - -## Target operating model - -The desired flow is: - -```text -Discovery -> Memory Brief -> Fact Graph -> Fact Store -> Quality Gate -> Targeted Swarm -> Synthesis -> Status/Resume -``` - -This keeps `project-brain` powerful because it can still delegate complex analysis, but cheaper because it does not pay model tokens to rediscover stable repository facts repeatedly. diff --git a/docs/self-improvement-framework.md b/docs/self-improvement-framework.md deleted file mode 100644 index acd9afd..0000000 --- a/docs/self-improvement-framework.md +++ /dev/null @@ -1,736 +0,0 @@ -# Project-Brain Self-Improvement Framework - -## Status - -- Document type: production learning architecture specification -- Scope: continuous self-improvement for agent recommendations, analysis quality, and decision logic -- Constraint: no agent may modify production code automatically; all outputs remain proposals - -## 1. Objective - -`project-brain` must improve over time without becoming an uncontrolled self-modifying system. - -The platform should learn from: - -- previous analyses -- detected bugs -- failed recommendations -- human feedback -- production incidents -- repository evolution - -The goal is to make future analyses: - -- more precise -- less noisy -- more context-aware -- more aligned with project-specific architecture -- safer and easier to trust - -This framework treats learning as controlled adaptation of: - -- memory and knowledge -- heuristics -- prompts -- rule weights -- confidence scoring - -It does not allow autonomous code changes or unreviewed behavior changes in production. - -## 2. Learning Architecture - -The self-improvement system adds a dedicated learning plane to the production architecture. - -### Core learning components - -- `Feedback Ingestion Service` -- `Learning Event Store` -- `Pattern Detection Engine` -- `Knowledge Evolution Engine` -- `Prompt & Heuristic Registry` -- `Quality Evaluation Engine` -- `Learning Governance Service` - -### High-level responsibilities - -#### Feedback Ingestion Service - -- receives approval decisions -- receives explicit human feedback -- receives incident reports -- receives post-analysis outcomes -- normalizes all feedback into typed learning events - -#### Learning Event Store - -- stores immutable learning events -- keeps lineage to job, project, agent, finding, and recommendation -- supports replay and retrospective evaluation - -#### Pattern Detection Engine - -- detects repeated failure modes and recurring structural problems -- clusters similar findings across runs and repositories -- produces candidate learning signals - -#### Knowledge Evolution Engine - -- turns validated signals into reusable knowledge -- updates best-practice records, failure patterns, and project-specific architectural insights -- proposes prompt and heuristic refinements - -#### Prompt & Heuristic Registry - -- version-controls prompts, prompt fragments, tool rules, heuristics, thresholds, and scoring logic -- enables staged rollout, rollback, and A/B evaluation - -#### Quality Evaluation Engine - -- measures whether newer reasoning strategies improve analysis quality -- scores precision, usefulness, acceptance rate, and incident prediction value - -#### Learning Governance Service - -- blocks unsafe autonomous behavior changes -- requires approval for high-impact prompt or heuristic changes -- enforces rollout policies and auditability - -## 3. Learning Plane Diagram - -```mermaid -flowchart LR - ANALYSIS["Analysis Jobs"] --> FINDINGS["Findings / Proposals"] - FINDINGS --> FEEDBACK["Feedback Ingestion Service"] - APPROVALS["Approval Decisions"] --> FEEDBACK - INCIDENTS["Incident Reports"] --> FEEDBACK - REPOEVOL["Repository Evolution Signals"] --> FEEDBACK - - FEEDBACK --> EVENTS["Learning Event Store"] - EVENTS --> PATTERN["Pattern Detection Engine"] - EVENTS --> EVAL["Quality Evaluation Engine"] - - PATTERN --> KNOW["Knowledge Evolution Engine"] - EVAL --> KNOW - - KNOW --> KB["Learning Memory / Knowledge Base"] - KNOW --> REG["Prompt & Heuristic Registry"] - - REG --> AGENTS["Agent Runtime"] - KB --> AGENTS - - GOV["Learning Governance"] --> REG - GOV --> KNOW - GOV --> AUDIT["Audit Log"] -``` - -## 4. Self-Learning Loop - -The system uses a controlled five-stage loop: - -1. `ANALYZE` - Agents inspect a repository and produce findings, scores, and proposals. -2. `PROPOSE` - The system emits recommendations and optional patch proposals as non-executable artifacts. -3. `EVALUATE` - Human approval, outcome tracking, and incident correlation determine whether the output was useful or harmful. -4. `LEARN` - The learning subsystem extracts reusable lessons, patterns, and reliability signals. -5. `REFINE` - Prompts, heuristics, thresholds, and rule sets are updated through governed versioned changes. - -This loop is recursive across time, not within a single run. Learning updates affect future runs only after validation. - -## 5. Feedback Ingestion - -### 5.1 Feedback sources - -The system must ingest five feedback classes: - -#### Human feedback - -- thumbs up / thumbs down -- free-text comments on findings -- edits to recommendations -- classification of noise vs useful insight -- reason for approval or rejection - -#### Recommendation outcome feedback - -- accepted proposal -- rejected proposal -- accepted but modified proposal -- ignored proposal -- proposal later linked to real defect prevention - -#### Incident feedback - -- postmortem -- Sev1/Sev2 incidents -- deployment rollback -- production outage -- security incident - -#### Bug feedback - -- bug introduced despite “clean” analysis -- bug predicted correctly by prior finding -- false-positive bug warnings -- missed regressions - -#### Repository evolution feedback - -- architecture changed significantly -- module ownership changed -- new framework introduced -- API style changed -- testing strategy improved or regressed - -### 5.2 Feedback ingestion contracts - -```ts -export interface FeedbackEvent { - eventId: string; - projectId: string; - jobId?: string; - findingId?: string; - recommendationId?: string; - agentId?: string; - source: - | "human" - | "approval-workflow" - | "incident-system" - | "ci" - | "repo-evolution" - | "post-analysis-evaluator"; - type: - | "recommendation.accepted" - | "recommendation.rejected" - | "recommendation.modified" - | "finding.useful" - | "finding.noisy" - | "incident.linked" - | "incident.missed" - | "architecture.changed" - | "bug.detected" - | "bug.missed"; - severity: "low" | "medium" | "high" | "critical"; - payload: Record; - createdAt: string; - actorId?: string; -} -``` - -### 5.3 Ingestion rules - -- all feedback becomes immutable append-only events -- free-text feedback is preserved, but normalized labels are also required -- incident imports must preserve source system identifiers -- feedback must be linkable to the original analysis output -- missing linkage is allowed but marked as low-confidence - -## 6. Learning Memory - -The learning memory extends the memory system with explicit reusable knowledge categories. - -### 6.1 Memory categories - -#### Lessons learned - -- concise statements of what worked or failed -- project-specific or global scope -- linked to evidence and outcomes - -#### Failure patterns - -- recurrent false positives -- recurrent false negatives -- bad prompt behaviors -- weak tool heuristics -- brittle repository assumptions - -#### Architectural insights - -- stable module boundaries -- known hotspot areas -- repeated coupling problems -- dependency concentration zones -- service ownership realities - -#### Best practices discovered - -- patterns correlated with lower incident rate -- testing strategies that reduced regressions -- observability practices that improved diagnosis -- secure defaults validated by real outcomes - -### 6.2 Learning memory schema - -```ts -export interface LearningRecord { - learningId: string; - projectId?: string; - scope: "global" | "organization" | "project" | "repository"; - category: - | "lesson" - | "failure-pattern" - | "architecture-insight" - | "best-practice" - | "prompt-adaptation" - | "heuristic-adaptation"; - topic: string; - statement: string; - evidenceRefs: string[]; - derivedFromEventIds: string[]; - confidence: number; - supportCount: number; - contradictionCount: number; - applicableAgents: string[]; - applicableTools: string[]; - tags: string[]; - status: "candidate" | "validated" | "deprecated" | "rejected"; - createdAt: string; - updatedAt: string; -} -``` - -### 6.3 Learning memory requirements - -- candidate learnings are separated from validated learnings -- validated learnings can influence future runs -- deprecated learnings remain queryable for audit -- every learning must be traceable to source events - -## 7. Pattern Detection - -The pattern engine identifies recurring signals across runs, agents, and projects. - -### 7.1 Pattern classes - -#### Recurring code smells - -- giant service files -- low-test core modules -- circular dependencies -- dead abstractions -- duplicated business logic - -#### Recurring architectural issues - -- boundary violations -- hidden coupling across modules -- unstable interfaces -- service ownership ambiguity -- infra drift from documented architecture - -#### Recurring security mistakes - -- secret files committed -- missing lockfiles -- weak container hygiene -- unsafe auth defaults -- repeated vulnerable dependency families - -#### Recurring operational incidents - -- missing alerts for critical paths -- repeated rollback-triggering modules -- known hotspot services with poor telemetry -- recurring deploy failures -- insufficient runbook coverage - -### 7.2 Pattern detection methods - -- rule-based aggregation -- similarity clustering on findings and incidents -- temporal recurrence analysis -- diff-to-incident correlation -- acceptance/rejection ratio analysis - -### 7.3 Pattern schema - -```ts -export interface DetectedPattern { - patternId: string; - scope: "global" | "organization" | "project"; - patternType: - | "code-smell" - | "architecture" - | "security" - | "operations" - | "feedback" - | "recommendation-failure"; - signature: string; - summary: string; - frequency: number; - impactedProjects: string[]; - firstSeenAt: string; - lastSeenAt: string; - confidence: number; - evidenceRefs: string[]; - recommendedResponse: string; -} -``` - -## 8. Knowledge Evolution - -Learning is useful only if it changes future reasoning in a controlled way. - -### 8.1 Evolvable artifacts - -- agent prompts -- shared prompt fragments -- severity thresholds -- confidence calibration curves -- tool rules -- project-specific heuristics -- ranking logic for recommendations - -### 8.2 Evolution strategy - -Knowledge evolution should happen in three steps: - -1. `CANDIDATE CHANGE` - Generated from learning records and pattern summaries. -2. `SHADOW EVALUATION` - New prompts or heuristics run against historical jobs without affecting user-visible output. -3. `CONTROLLED ROLLOUT` - If quality metrics improve, the new version is activated for a subset of projects or agents. - -### 8.3 Prompt update model - -Prompts must be modular and versioned. - -Structure: - -- base system prompt -- agent role prompt -- organization policy prompt -- project memory prompt -- learned heuristics prompt fragment -- execution guardrails prompt fragment - -Learned prompt fragments are the only autonomously proposed prompt modifications. Promotion to active status requires evaluation and governance checks. - -### 8.4 Tool rule update model - -Tool rules should evolve through versioned policy packs: - -- security ruleset versions -- architecture lint rule packs -- recommendation ranking rules -- confidence thresholds per agent - -Rule changes are data-driven and reversible. They are not silently applied. - -## 9. Model Improvement - -The system should improve agent quality even when the underlying LLM does not change. - -### 9.1 What gets improved - -- prompt wording -- evidence selection strategy -- tool selection sequence -- reasoning depth by task type -- confidence estimation -- recommendation ranking -- false-positive suppression - -### 9.2 Output evaluation dimensions - -Each agent output must be scored on: - -- precision -- recall proxy -- usefulness -- actionability -- clarity -- evidence quality -- acceptance rate -- incident correlation quality - -### 9.3 Agent quality schema - -```ts -export interface AgentQualityScore { - scoreId: string; - projectId: string; - jobId: string; - agentId: string; - version: string; - precisionScore: number; - recallProxyScore: number; - usefulnessScore: number; - actionabilityScore: number; - clarityScore: number; - evidenceScore: number; - acceptanceRateScore: number; - incidentPredictionScore: number; - overallScore: number; - evaluator: "human" | "rule-engine" | "offline-benchmark"; - createdAt: string; -} -``` - -### 9.4 Adaptation policy - -- low precision drives stricter evidence requirements -- low usefulness drives ranking and framing changes -- low acceptance drives prompt refinement and project-context weighting -- repeated misses drive new tool steps or mandatory checks -- repeated false positives reduce heuristic weight or increase threshold - -## 10. Storage Model - -The self-improvement framework uses a multi-store design. - -### Operational storage - -- PostgreSQL - - learning events - - learning records - - pattern summaries - - prompt versions - - heuristic versions - - quality scores - - approval decisions - -### Ephemeral processing - -- Redis - - event queues - - temporary clustering state - - evaluation jobs - -### Artifact storage - -- S3-compatible object storage - - incident attachments - - exported reports - - benchmark datasets - - shadow evaluation outputs - -### Retrieval and semantic memory - -- PostgreSQL + pgvector - - learned insights - - architectural knowledge snippets - - prior incidents - - accepted recommendation exemplars - -### Audit storage - -- append-only audit tables -- optional cold archive in object storage - -## 11. Improvement Feedback Loop - -The production improvement loop should operate as follows: - -### Step 1: capture - -- record every finding, recommendation, approval decision, and incident link - -### Step 2: correlate - -- link outcomes to prior jobs, agents, prompts, heuristics, repository versions, and affected components - -### Step 3: score - -- compute agent quality scores -- classify false positives, false negatives, and high-value recommendations - -### Step 4: detect patterns - -- aggregate repeated outcomes -- generate candidate learnings and failure patterns - -### Step 5: propose refinements - -- create candidate updates for prompts, heuristics, thresholds, and tool rules - -### Step 6: validate offline - -- replay historical jobs -- compare old vs candidate configurations -- block regressions before rollout - -### Step 7: rollout safely - -- enable by project, agent, or tenant -- monitor acceptance and precision -- rollback immediately on degradation - -### Step 8: memorialize - -- persist validated lessons learned and best practices into the long-term knowledge base - -## 12. Learning Governance and Safety Guardrails - -### Absolute guardrails - -- agents cannot modify production code automatically -- agents cannot commit, push, merge, or deploy autonomously -- all suggestions remain proposals or artifacts -- prompt or heuristic changes cannot bypass governance -- unsafe rules cannot be promoted from candidate to active without evaluation - -### Safety controls - -- versioned prompt registry with rollback -- approval-required promotion for high-impact prompt changes -- tenant/project scoping for learned behavior -- quarantine for low-performing prompt versions -- audit log for every learning-derived change - -### Recommended rollout policy - -- `candidate`: not used in live runs -- `shadow`: evaluated silently against live or historical workloads -- `limited`: enabled for a subset of projects -- `active`: default for eligible projects -- `rollback`: automatically disabled due to degradation - -## 13. Example Learning Events - -### Example 1: accepted security recommendation - -```json -{ - "eventId": "evt_001", - "projectId": "workflow-suite", - "jobId": "job_182", - "findingId": "find_sec_88", - "recommendationId": "rec_sec_17", - "agentId": "security-agent", - "source": "approval-workflow", - "type": "recommendation.accepted", - "severity": "high", - "payload": { - "reason": "Confirmed exposed secret in tracked .env file", - "component": "payments-service", - "fixAppliedByHuman": true - }, - "createdAt": "2026-03-10T12:00:00Z", - "actorId": "eng_manager_1" -} -``` - -Learning extracted: - -- secret-file detection in this repository has high precision -- increase confidence weight for `.env` findings in the same project - -### Example 2: rejected noisy architecture recommendation - -```json -{ - "eventId": "evt_002", - "projectId": "sample-platform", - "jobId": "job_221", - "findingId": "find_dev_31", - "recommendationId": "rec_dev_09", - "agentId": "dev-agent", - "source": "human", - "type": "recommendation.rejected", - "severity": "medium", - "payload": { - "reason": "Suggested split was invalid because module boundary is intentional", - "label": "false_positive_architecture_refactor" - }, - "createdAt": "2026-03-10T12:05:00Z", - "actorId": "staff_engineer_4" -} -``` - -Learning extracted: - -- current refactor heuristic over-penalizes intentional shared modules -- architecture refactor suggestions require stronger ownership evidence - -### Example 3: missed operational incident - -```json -{ - "eventId": "evt_003", - "projectId": "cashcalculator", - "source": "incident-system", - "type": "incident.missed", - "severity": "critical", - "payload": { - "incidentId": "inc_44", - "service": "pricing-api", - "summary": "Latency spike caused by missing query timeout and absent alert", - "relatedCommit": "ab12cd3" - }, - "createdAt": "2026-03-10T12:20:00Z" -} -``` - -Learning extracted: - -- observability and optimization agents missed a recurring latency risk pattern -- introduce stronger checks for query timeout configuration and alert presence - -### Example 4: repository evolution signal - -```json -{ - "eventId": "evt_004", - "projectId": "workflow-suite", - "source": "repo-evolution", - "type": "architecture.changed", - "severity": "low", - "payload": { - "change": "monolith_to_modular_monolith", - "newModules": ["billing", "identity", "procurement"], - "frameworksDetected": ["NestJS", "NextJS"] - }, - "createdAt": "2026-03-10T12:40:00Z" -} -``` - -Learning extracted: - -- previous architecture assumptions are stale -- reset architecture-specific prompt fragments for this project - -## 14. Recommended Package Layout - -This framework maps naturally onto the production monorepo design: - -```text -packages/ - learning/ - src/ - ingestion/ - events/ - evaluation/ - patterns/ - evolution/ - governance/ - registry/ - memory/ - src/ - learnings/ - projections/ - retrieval/ - contracts/ - src/ - learning/ - feedback/ - quality/ -``` - -## 15. Final Decision - -The self-improvement framework for `project-brain` should be implemented as a governed learning subsystem that: - -- ingests human and operational feedback as immutable events -- detects recurring patterns across analyses and incidents -- converts validated outcomes into reusable learning memory -- improves prompts, heuristics, and rule sets through versioned controlled rollout -- evaluates quality continuously with replay and shadow testing -- never turns learning into autonomous production code modification - -This gives `project-brain` the benefits of continuous adaptation without losing auditability, safety, or human control. diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 63407c5..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,586 +0,0 @@ -# usage - -## Recommended beta path - -Start here for normal use: - -```bash -project-brain go "understand this project and suggest the next safe step" /path/to/repo -``` - -If `--output` is omitted, project-brain writes all generated artifacts under `/path/to/repo/BRAIN/`. That directory contains the generated `AI_CONTEXT/`, runtime `memory/`, `reports/`, `tasks/`, and generated docs/proposals. Pass `--output /path/to/output` to place the same layout somewhere else. - -Use the guided console when the user should not remember command names: - -```bash -project-brain console --target /path/to/repo -``` - -Read these docs for release-candidate operation: - -- `docs/installation.md` -- `docs/first-analysis-5-min.md` -- `docs/output-contract.md` -- `docs/release-checklist.md` -- `docs/user-test-script.md` - -## Build - -```bash -npm install -npm run build -``` - -## Validate locally - -```bash -npm run hooks:install -npm run lint -npm run typecheck -npm run verify -``` - -## Repository hardening - -Before opening the repo to public contributions, install the local gates: - -```bash -npm run hooks:install -``` - -The repository now provides: - -- `pre-commit`: blocks staged secrets, weak local-only paths, and runs `npm run lint` -- `commit-msg`: blocks placeholder commit messages like `wip` -- `pre-push`: runs `npm run verify:quick` -- GitHub CI: runs lint, typecheck, build, tests, smoke tests, and repository safety scan -- GitHub dependency review and security baseline workflows - -For GitHub-side settings such as branch protection and secret scanning, follow `docs/github-hardening.md`. - -## Analyze a repository - -Map an existing repository into structured onboarding docs: - -```bash -project-brain map-codebase /path/to/repo -``` - -Start with plain language instead of choosing a command manually: - -```bash -project-brain start "quiero analizar y mejorar este proyecto" /path/to/repo -project-brain ask "identifica este proyecto" /path/to/repo -project-brain ask "dime que le falta criticamente" /path/to/repo -project-brain ask "revisa los cambios recientes" /path/to/repo -``` - -`start` is the simple path for non-technical users. It runs cheap deterministic preflight first: doctor, codebase map, code graph, fact query, runbook, harness audit, and firewall. It does not run the model-heavy swarm unless you pass `--with-swarm`. - -`go` is the preferred alias for beta users. Use `status`, `resume`, `runbook`, and `fact-query` before broad swarm analysis. - -`ask` routes the request into the current best workflow and writes `reports/ask_brief.md` with artifacts and suggested next prompts. - -## Create a new project context - -Use `new` when there is no repository to analyze yet: - -```bash -project-brain new ./my-new-project -``` - -For non-interactive use: - -```bash -project-brain new ./my-new-project \ - --yes \ - --name "Inventory SaaS" \ - --problem "Track workshop inventory" \ - --audience "small repair shops" \ - --type saas-webapp \ - --stack "Next.js + PostgreSQL" \ - --features "inventory dashboard,order tracking" \ - --auth yes \ - --roles "owner,technician" \ - --data "User,Workshop,InventoryItem,Order" \ - --integrations "email,object storage" -``` - -This writes directly into the new project directory: - -- `AI_CONTEXT/PROJECT_CHARTER.md` -- `AI_CONTEXT/REQUIREMENTS.md` -- `AI_CONTEXT/PROJECT_BLUEPRINT.md` -- `AI_CONTEXT/DECISIONS.md` -- `AI_CONTEXT/MEMORY_BRIEF.md` -- `AI_CONTEXT/RUNBOOK.md` -- `docs/architecture_plan/BLUEPRINT.md` -- `docs/architecture_plan/STATE.md` -- `memory/project_seed/project_seed.json` -- `tasks/initial_backlog.md` -- `CLAUDE.md` - -This command is context-only for now. It uses a guided blueprint pattern inspired by The Architect, adapted to Project Brain's persistent `AI_CONTEXT` layout. - -Persist a stateful improvement plan: - -```bash -project-brain plan-improvements /path/to/repo --trigger repository-change --output /path/to/output -``` - -This writes: - -- `docs/improvement_plan/SUMMARY.md` -- `docs/improvement_plan/STATE.md` -- `docs/improvement_plan/KNOWN_RISKS.md` -- `docs/improvement_plan/ROADMAP.md` -- `docs/improvement_plan/TRACKS.md` - -Generate a bounded architecture plan before restructuring: - -```bash -project-brain architecture-plan /path/to/repo --output /path/to/output -``` - -This writes: - -- `docs/architecture_plan/BLUEPRINT.md` -- `docs/architecture_plan/STATE.md` -- `docs/architecture_plan/CLAUDE.md` -- `memory/architecture_plan/architecture_plan.json` - -Search the curated local context registry: - -```bash -project-brain context-search "express observability" /path/to/repo --output /path/to/output -project-brain context-search "vitest testing" /path/to/repo --trust official --output /path/to/output -project-brain context-sources /path/to/repo --output /path/to/output -``` - -Discover ecosystem repos from GitHub and feed them into the same local context registry: - -```bash -project-brain ecosystem-radar /path/to/repo --output /path/to/output -project-brain ecosystem-radar /path/to/repo --bucket memory --limit 4 --output /path/to/output -project-brain ecosystem-radar /path/to/repo --seed-only --output /path/to/output -``` - -Materialize one entry into reusable project context: - -```bash -project-brain context-get node-express-api /path/to/repo --output /path/to/output -``` - -This writes: - -- `reports/context_search.md` -- `reports/context_sources.md` -- `reports/ecosystem_radar.md` -- `memory/context_registry/` -- `AI_CONTEXT/EXTERNAL_CONTEXT/.md` - -If `GITHUB_TOKEN` is set, `ecosystem-radar` uses authenticated GitHub API requests. Without it, the command still works against public repositories but hits tighter rate limits. - -This produces `docs/codebase_map/` with: - -- `SUMMARY.md` -- `STACK.md` -- `INTEGRATIONS.md` -- `ARCHITECTURE.md` -- `STRUCTURE.md` -- `CONVENTIONS.md` -- `TESTING.md` -- `CONCERNS.md` - -Persist local repo notes so future runs do not forget them: - -```bash -project-brain annotate /path/to/repo "The payments area has risky legacy behavior" --output /path/to/output -project-brain annotate /path/to/repo --list --output /path/to/output -``` - -Annotations are written to `AI_CONTEXT/ANNOTATIONS.md` and also appear in the generated codebase map summary. - -Compute impact radius for a targeted set of files: - -```bash -project-brain impact-radius /path/to/repo --files src/core/service.ts,src/api/router.ts --output /path/to/output -``` - -This writes a persistent symbol-aware graph to `memory/code_graph/code_graph_v2.json` and an actionable review set to `reports/impact_radius.md`. - -Build or refresh the code graph without running impact analysis: - -```bash -project-brain code-graph /path/to/repo --output /path/to/output -``` - -This now writes: - -- `memory/code_graph/code_graph_v2.json` -- `memory/knowledge_graph/repository_fact_graph.json` -- `reports/repository_fact_graph.md` - -The repository fact graph is intentionally factual only. It reuses verified discovery and code-graph relations, and does not add inferred or ambiguous edges. - -Query compact factual memory without calling an AI model: - -```bash -project-brain fact-query "swarm runtime token cache" /path/to/repo --output /path/to/output -``` - -This writes: - -- `reports/fact_query.md` -- `AI_CONTEXT/fact_query/fact_query.json` - -Use this before giving another AI a broad task. It returns a short deterministic answer plus matching memory lines, graph nodes, graph edges, evidence refs, and unknowns. - -Create a token-aware runbook before expensive analysis: - -```bash -project-brain runbook "optimize analysis and cost" /path/to/repo --output /path/to/output -``` - -This writes: - -- `reports/runbook.md` -- `AI_CONTEXT/runbook/runbook.json` - -The runbook orders cheap deterministic steps before model-heavy work: doctor, map, code graph, fact query, harness audit, firewall, bounded swarm, planning, resume. - -Audit the agent harness before model-heavy analysis: - -```bash -project-brain harness-audit /path/to/repo --output /path/to/output -``` - -This writes: - -- `reports/harness_audit.md` -- `AI_CONTEXT/harness_audit/harness_audit.json` - -The harness audit is deterministic and model-free. It checks whether progressive memory exists before broad analysis: compact memory index, factual graph, filtered context query, execution controls, and deep analysis memory. This adapts the useful parts of memory-first and harness-optimization systems without making project-brain Claude-specific. - -Review the latest git delta instead of naming files manually: - -```bash -project-brain review-delta /path/to/repo --base HEAD~1 --head HEAD --output /path/to/output -``` - -`review-delta` computes: - -- changed files from git -- direct and transitive dependents -- related tests -- a minimal review set - -Inspect the agent firewall before running a full cycle: - -```bash -project-brain firewall /path/to/repo --trigger repository-change --output /path/to/output -``` - -This writes: - -- `reports/agent_firewall.md` -- `memory/firewall/agent_firewall.json` -- `tasks/packets/*.md` - -```bash -project-brain analyze /path/to/repo -``` - -Write generated output outside the target repository: - -```bash -project-brain analyze /path/to/repo --output /path/to/output -``` - -Use a longer Ollama timeout for local AI analysis: - -```bash -project-brain analyze /path/to/repo --ollama-timeout 240000 -``` - -## Model roles - -`project-brain models` now shows both Ollama residency and task profiles. - -Default runtime split: - -- `worker`: `qwen2.5-coder:7b` -- `reviewer`: `deepseek-coder:6.7b` -- `reasoning`: `llama3.1:8b` -- `planner`: `kimi-k2.5:cloud` -- `synthesizer`: `llama3.1:8b` - -Use that split to keep discovery, review, and day-to-day analysis cheap and local while reserving the planner for strategic or ambiguous asks. - -## Swarm presets - -Use bounded swarm only after deterministic memory and facts have been checked. - -```bash -project-brain swarm "review risky areas without modifying files" /path/to/repo --output /path/to/output --preset cheap -project-brain swarm "review architecture risks" /path/to/repo --output /path/to/output --preset balanced -project-brain swarm "deep review of critical modules" /path/to/repo --output /path/to/output --preset thorough -``` - -- `cheap`: fastest and most economical, smaller queue and lower timeout budget. -- `balanced`: recommended default for meaningful coverage. -- `thorough`: slower and more expensive, use only when cost/time is justified. - -## Runtime artifact policy - -Version source docs, templates, contracts, and schemas. - -Release and validation reports under `reports/validation-*.md`, `reports/validation-results.json`, `reports/beta-readiness.md`, and `reports/release-candidate-*.md` are deliberate versioned evidence. Ad hoc runtime reports such as `reports/doctor.md` remain ignored. - -Do not version generated local runtime outputs: - -- `.claude/` -- `.project-brain/runtime/` -- `AI_CONTEXT/doctor/` -- `reports/doctor.md` - -Use `--output` outside the target repository when validating real projects. - -## Typical repository workflow - -Frontend usability cycle: - -```bash -project-brain analyze \ - /path/to/frontend-repo \ - --output /path/to/output \ - --trigger repository-change \ - --ollama-timeout 240000 \ - --verbose -``` - -Workspace-wide analysis: - -```bash -project-brain analyze \ - /path/to/workspace \ - --output /path/to/output \ - --trigger repository-change -``` - -## Prompt template usage - -The templates in `prompts/context_templates/` are intended for external repositories. Use them when a coding agent needs high-quality context before proposing frontend, UX, architecture, or performance changes. - -Recommended templates: - -- `context_bootstrap_master.md`: create or refresh `AI_CONTEXT/` from the real repository state -- `frontend_analysis.md`: analyze operational frontend surfaces -- `ux_improvement.md`: produce UX-focused improvement tasks -- `architecture_review.md`: review module boundaries and structural risk -- `performance_review.md`: find low-risk performance wins - -Recommended process: - -1. Run `project-brain map-codebase` against the target repository. -2. Run `project-brain analyze` for specialist-agent reports and proposals. -3. Run `project-brain review-delta` when you need a bounded review surface for a recent change. -4. Collect the generated `AI_CONTEXT`, codebase map, reports, and task artifacts. -5. Combine those artifacts with one of the prompt templates. -6. Use the resulting context in the downstream coding agent. - -## Common commands - -```bash -project-brain init /path/to/repo -project-brain map-codebase /path/to/repo -project-brain annotate /path/to/repo "Known legacy hotspot" --output /path/to/output -project-brain code-graph /path/to/repo -project-brain impact-radius /path/to/repo --files src/core/service.ts -project-brain review-delta /path/to/repo -project-brain start "quiero analizar y mejorar este proyecto" /path/to/repo -project-brain ask "identifica este proyecto" /path/to/repo -project-brain ask "ayudame a definir el stack y el alcance" /path/to/repo -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --preset cheap -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --preset balanced -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --preset thorough -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --parallel 3 -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --parallel 3 --chunk-size 1 -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --parallel 3 --chunk-size 1 --task-timeout-ms 12000 --max-retries 1 -project-brain swarm "ayudame a mejorar este repo" /path/to/repo --parallel 2 --chunk-size 1 --planner-timeout-ms 8000 --synthesis-timeout-ms 8000 --run-timeout-ms 30000 --max-queued-tasks 8 -project-brain self-improve /path/to/repo -project-brain plan-improvements /path/to/repo --trigger repository-change -project-brain architecture-plan /path/to/repo --output /path/to/output -project-brain context-search "express observability" /path/to/repo -project-brain context-get node-express-api /path/to/repo -project-brain context-sources /path/to/repo -project-brain firewall /path/to/repo --trigger repository-change -project-brain doctor /path/to/repo -project-brain status /path/to/repo --output /path/to/output -project-brain agents /path/to/repo -project-brain weekly /path/to/repo -project-brain report /path/to/output -project-brain models -``` - -## Safety - -`project-brain` analyzes and proposes. It does not modify target code automatically. Generated patch proposals remain review-only, weakly corroborated proposals are downgraded to human review by the consensus gate, and the agent firewall classifies each task before execution into a bounded policy pack. - -## Doctor - -`project-brain doctor` is the environment and runtime health check inspired by orchestration-first tools like Agent Orchestrator. - -It validates: - -- Node runtime compatibility -- `git` availability and target repo status -- `ollama` availability -- model inventory and configured profiles -- local swarm readiness -- `config/models.json` -- built CLI artifact presence -- output-path writability - -Artifacts land in `reports/doctor.md` and `AI_CONTEXT/doctor/doctor.json`. - -Doctor reports now include suggested follow-up commands, prioritized from high to low. - -## Status - -`project-brain status` is the operational snapshot view inspired by the “single command status” idea from Agent Orchestrator, but adapted to artifact-based analysis instead of live agent sessions. - -It summarizes: - -- git repo and branch -- latest doctor state -- presence of swarm/improvement-plan/codebase-map/firewall/impact/ask artifacts -- artifact timestamps in the current output path - -Artifacts land in `reports/status.md` and `AI_CONTEXT/status/status.json`. - -Status reports also include suggested follow-up commands derived from the current artifact state. - -## Resume - -`project-brain resume` is the state-recovery view for the control tower. - -It reads the current output path, finds the latest useful artifact, identifies the stage where the project last stopped, and suggests the next command to continue from there. - -Typical resume stages: - -- `doctor` -- `map-codebase` -- `ask` -- `swarm` -- `plan-improvements` -- `review-delta` -- `firewall` - -Artifacts land in `reports/resume.md` and `AI_CONTEXT/resume/resume.json`. - -`project-brain ask` now routes continuation prompts like `continua con el proyecto` or `retoma donde nos quedamos` into this recovery flow automatically. -If the next move is clear and bounded, `ask` will also execute one guided continuation step automatically instead of only suggesting it. - -## Swarm mode - -`project-brain swarm` is the first bounded delegation layer: - -- `planner`: splits the user intent into small tasks -- `worker`: scans scope and implementation details -- `reviewer`: stresses risks and weak spots -- `reasoning`: turns findings into decisions and next steps -- `synthesizer`: merges the delegated outputs into one report - -Artifacts land in `reports/swarm_run.md` and `AI_CONTEXT/swarm/swarm_run.json`. -Use `--preset cheap`, `--preset balanced`, or `--preset thorough` before tuning low-level runtime flags manually. -If you do not pass `--parallel`, `project-brain` picks a bounded worker count from local CPU, load average, and free memory. -If you do not pass `--chunk-size`, `project-brain` picks a repo-slice size from repository size and then enqueues smaller scope chunks so local workers inspect only a few top-level areas at a time. -The worker queue is round-robin, so a short queue budget samples multiple parent tasks before going deeper into any single one. -If a worker exceeds `--task-timeout-ms`, the swarm retries or splits the scope chunk into smaller ones before giving up, capped by `--max-retries`. -Use `--planner-timeout-ms`, `--synthesis-timeout-ms`, and `--run-timeout-ms` to keep the whole run bounded, and `--max-queued-tasks` to stop the queue from growing beyond a fixed budget. When you do not pass `--max-queued-tasks`, `project-brain` derives queue pressure from CPU load and free memory. When those budgets are short enough, `project-brain` will also keep planner and synthesis on local Ollama models instead of reaching for a remote planner, and it will clamp auto-selected concurrency so the local run stays bounded. -If a single large scope like `agents/` or `core/` times out, the swarm now splits it into immediate child scopes such as `agents/security_agent` or `core/orchestrator` before retrying. -If the user intent names a path like `core/swarm_runtime`, the swarm now treats that as a scope hint and pulls the matching project area to the front of the queue. -If a local model returns labeled Markdown or plain text instead of strict JSON, the swarm now recovers `summary`, `findings`, `recommendations`, `priorities`, and `next_steps` before degrading to an empty result. - -## Self-improve - -`project-brain self-improve` is a thin wrapper around the swarm with defaults tuned for local repo self-analysis: - -- `chunk-size=1` -- `task-timeout-ms=12000` -- `planner-timeout-ms=8000` -- `synthesis-timeout-ms=8000` -- `run-timeout-ms=45000` -- `max-retries=1` - -`parallelism` and queue budget are left adaptive on purpose, so `self-improve` can shrink itself automatically when the machine is already under pressure. It also uses a `source-first` scope bias so the first queued chunks prefer product code areas over `tests/` and top-level config files. -Use it when you want `project-brain` to inspect a repository, including itself, without hand-tuning the swarm flags first. - -## Recommended release workflows - -Use `go` as the main entry point when you do not want to remember individual -commands: - -```bash -project-brain go "understand this project and recommend the next step" /path/to/repo --output /path/to/output -``` - -Use these deterministic commands before model-heavy work: - -```bash -project-brain status /path/to/repo --output /path/to/output -project-brain runbook "what should I do next?" /path/to/repo --output /path/to/output -project-brain fact-query "known fact or module name" /path/to/repo --output /path/to/output -``` - -Use swarm presets only when memory/fact checks are insufficient: - -```bash -project-brain swarm --preset cheap "inspect this module" /path/to/repo --output /path/to/output -project-brain swarm --preset balanced "review critical risks" /path/to/repo --output /path/to/output -project-brain swarm --preset thorough "deep review" /path/to/repo --output /path/to/output -``` - -Preset meanings: - -- `cheap`: fast/economic, fewer tasks. -- `balanced`: recommended default for better coverage. -- `thorough`: slower/costlier, maximum coverage. - -## Progressive memory - -The current memory stack is: - -- `AI_CONTEXT/MEMORY_BRIEF.md`: compact agent/human handoff. -- `AI_CONTEXT/EXECUTIVE_SUMMARY.md`: project status, risks, scopes, and next actions. -- `memory/scopes/*.json`: per-scope facts, coverage, freshness, and evidence. -- `memory/knowledge_graph/repository_fact_graph.json`: structural repository facts. -- `preflightFacts`: read-only factual preflight before ask/model flows. - -Fresh and complete scope memory may reduce queued swarm work. Stale scope memory -is reported as stale and is not used as current factual evidence. - -## Runtime artifact policy - -Project Brain versions source documentation, templates, contracts, and curated -`AI_CONTEXT/*.md` memory files. Runtime diagnostics and local agent state are -generated per machine/session and are ignored by git. - -Versioned examples: - -- `AI_CONTEXT/*.md` curated project memory -- `reports/templates/*.md` report templates -- `docs/**` source documentation - -Ignored runtime examples: - -- `.claude/` -- `AI_CONTEXT/doctor/` -- `reports/doctor.md` -- `.project-brain/runtime/` - -Generated doctor output can include absolute local paths, local model inventory, -runtime versions, and branch-specific diagnostics. Use -`reports/templates/doctor.md` as the stable source contract instead of tracking -the generated report. diff --git a/docs/user-test-script.md b/docs/user-test-script.md deleted file mode 100644 index d657fb1..0000000 --- a/docs/user-test-script.md +++ /dev/null @@ -1,51 +0,0 @@ -# Non-technical user test script - -## User profile - -A product owner, technical lead, analyst, or manager who needs to understand a project but does not know internal `project-brain` commands. - -The only instruction given to the user is: - -```bash -project-brain go -``` - -## Tasks - -1. Start `project-brain go`. -2. Analyze a project. -3. Find the executive summary. -4. Search for one factual answer about the project. -5. Review pending or recommended next steps. -6. Choose a cheap/review-only analysis path if offered. -7. Exit without needing help. - -## Observation questions - -- Did the user understand what to do first? -- Did the user know where output was written? -- Did the user find the executive summary? -- Did any text feel too technical? -- Did the user understand cheap/balanced/thorough? -- Did the user trust the result? Why or why not? -- Did the user expect project files to be modified? -- Did the user know how to stop or exit? - -## Success criteria - -- Completes first analysis without help. -- Finds `AI_CONTEXT/EXECUTIVE_SUMMARY.md`. -- Runs or understands factual search. -- Understands cheap/balanced/thorough at a basic cost level. -- Knows that output is review-only and generated separately. -- Does not need to read the full README before starting. - -## Backlog classification - -- P0: blocks first analysis or causes fear of data loss. -- P1: confuses the user but allows progress. -- P2: improves clarity or polish. - -## Notes for facilitator - -Do not explain internal commands. Record where the user hesitates and the exact text that caused confusion. diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 6b76dc7..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import tsParser from "@typescript-eslint/parser"; - -const sourceFiles = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"]; -const ignoredPaths = [ - "dist/**", - "node_modules/**", - "coverage/**", - "build/**", - "cache/**", - ".cache/**", - "tmp/**", - ".tmp/**", - "sample-output/**", - "pb-output/**", - "project-brain/pb-output/**" -]; - -export default [ - { - ignores: ignoredPaths - }, - { - files: sourceFiles, - languageOptions: { - parser: tsParser, - ecmaVersion: "latest", - sourceType: "module" - }, - rules: { - "no-debugger": "error", - "no-eval": "error", - "no-implied-eval": "error", - "no-new-func": "error", - "no-unreachable": "error", - "no-unsafe-finally": "error", - "valid-typeof": "error" - } - } -]; diff --git a/governance/agent-council.ts b/governance/agent-council.ts deleted file mode 100644 index 8928909..0000000 --- a/governance/agent-council.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { uniqueSorted } from "../shared/fs-utils"; -import type { AgentTask, GovernanceTrigger, LearningRecord } from "../shared/types"; - -import type { RegisteredAgent } from "./agent-registry"; - -const PRIORITY_ORDER: Record = { - critical: 4, - high: 3, - normal: 2, - low: 1 -}; - -function createTaskId(agentId: string, index: number): string { - return `task_${agentId}_${Date.now()}_${index}`; -} - -function priorityFor(trigger: GovernanceTrigger, agentId: string): AgentTask["priority"] { - const matrix: Partial>>> = { - "security-advisory": { - "security-agent": "critical", - "auth-agent": "high", - "infra-agent": "high", - "dependency-agent": "high", - "observability-agent": "normal" - }, - "security-audit": { - "security-agent": "critical", - "auth-agent": "critical", - "infra-agent": "high", - "dependency-agent": "high", - "qa-agent": "high", - "observability-agent": "high", - "dev-agent": "normal" - }, - "architecture-review": { - "architecture-agent": "critical", - "dev-agent": "high", - "optimization-agent": "high", - "observability-agent": "normal", - "documentation-agent": "normal" - }, - "dependency-update": { - "dependency-agent": "critical", - "security-agent": "high", - "qa-agent": "normal" - }, - "incident-detection": { - "observability-agent": "critical", - "qa-agent": "high", - "architecture-agent": "high", - "optimization-agent": "high" - }, - "repository-change": { - "dev-agent": "high", - "qa-agent": "high", - "ux-agent": "high", - "ux-improvement-agent": "high", - "documentation-agent": "normal", - "security-agent": "normal" - }, - "weekly-review": { - "product-owner-agent": "high", - "qa-agent": "high", - "ux-agent": "high", - "ux-improvement-agent": "high", - "dev-agent": "high", - "architecture-agent": "high", - "optimization-agent": "high", - "documentation-agent": "high", - "legal-agent": "normal" - }, - manual: { - "security-agent": "high", - "qa-agent": "high" - } - }; - - return matrix[trigger]?.[agentId] ?? "normal"; -} - -export class AgentCouncil { - planTasks( - agents: RegisteredAgent[], - trigger: GovernanceTrigger, - previousLearnings: LearningRecord[] - ): AgentTask[] { - const learnedAgents = new Set( - previousLearnings - .filter((learning) => ["MISSED_ISSUE", "FALSE_POSITIVE"].includes(learning.outcome)) - .map((learning) => learning.agentId) - ); - - const tasks = agents.map((entry, index) => { - const learnedPriorityBoost = learnedAgents.has(entry.descriptor.agentId); - const priority = priorityFor(trigger, entry.descriptor.agentId); - - return { - taskId: createTaskId(entry.descriptor.agentId, index + 1), - agentId: entry.descriptor.agentId, - title: `${entry.descriptor.displayName} analysis`, - description: `Run ${entry.descriptor.displayName} for trigger ${trigger} and return recommendations.`, - trigger, - priority: learnedPriorityBoost && priority === "normal" ? "high" : priority, - state: "NEW", - createdAt: new Date().toISOString(), - rationale: learnedPriorityBoost - ? "Previous learnings indicate this agent needs closer follow-up." - : `Selected by AgentCouncil for ${trigger}.` - } satisfies AgentTask; - }); - - return tasks.sort((left, right) => PRIORITY_ORDER[right.priority] - PRIORITY_ORDER[left.priority]); - } - - resolveConflicts(tasks: AgentTask[], reports: Array<{ agentId: string; recommendations: string[] }>): string[] { - const structuralAgents = uniqueSorted( - reports - .filter((report) => - report.recommendations.some((recommendation) => - /architecture|structural|refactor|boundary/i.test(recommendation) - ) - ) - .map((report) => report.agentId) - ); - - const conflicts: string[] = []; - - if (structuralAgents.length > 1) { - conflicts.push( - `Multiple agents proposed structural changes (${structuralAgents.join(", ")}); human architecture review is required.` - ); - } - - if (tasks.some((task) => task.priority === "critical") && reports.length === 0) { - conflicts.push("Critical tasks completed without agent reports; investigate runtime failures."); - } - - return conflicts; - } -} diff --git a/governance/agent-evaluator.ts b/governance/agent-evaluator.ts deleted file mode 100644 index f9cf360..0000000 --- a/governance/agent-evaluator.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { AgentEvaluationScore, AgentReport, AgentTask, RiskLevel } from "../shared/types"; - -function clamp(value: number): number { - return Math.max(0, Math.min(1, Number(value.toFixed(2)))); -} - -function riskWeight(riskLevel: RiskLevel): number { - if (riskLevel === "high") { - return 1; - } - if (riskLevel === "medium") { - return 0.75; - } - return 0.5; -} - -export class AgentEvaluator { - evaluate(task: AgentTask, report: AgentReport): AgentEvaluationScore { - const findingsWeight = Math.min(report.findings.length, 4) / 4; - const recommendationWeight = Math.min(report.recommendations.length, 4) / 4; - const outputQuality = clamp(0.35 + findingsWeight * 0.35 + recommendationWeight * 0.3); - const proposalQuality = clamp(report.recommendations.length === 0 ? 0.3 : 0.5 + recommendationWeight * 0.5); - const signalStrength = clamp(0.3 + findingsWeight * 0.4 + riskWeight(report.riskLevel) * 0.3); - const riskAlignment = clamp( - report.riskLevel === "high" - ? 0.6 + findingsWeight * 0.4 - : report.riskLevel === "medium" - ? 0.55 + findingsWeight * 0.3 - : 0.5 + recommendationWeight * 0.2 - ); - const overallScore = clamp((outputQuality + proposalQuality + signalStrength + riskAlignment) / 4); - const notes: string[] = []; - - if (report.findings.length === 0) { - notes.push("Low finding density; monitor for missed issues."); - } - if (report.recommendations.length === 0) { - notes.push("No improvement proposals generated."); - } - if (report.riskLevel === "high") { - notes.push("High-risk output should be prioritized for human review."); - } - - return { - agentId: report.agentId, - taskId: task.taskId, - outputQuality, - proposalQuality, - signalStrength, - riskAlignment, - overallScore, - rank: 0, - notes - }; - } - - rank(scores: AgentEvaluationScore[]): AgentEvaluationScore[] { - const sorted = [...scores].sort((left, right) => right.overallScore - left.overallScore); - return sorted.map((score, index) => ({ - ...score, - rank: index + 1 - })); - } -} diff --git a/governance/agent-firewall.ts b/governance/agent-firewall.ts deleted file mode 100644 index 27085d9..0000000 --- a/governance/agent-firewall.ts +++ /dev/null @@ -1,517 +0,0 @@ -import path from "node:path"; - -import { ensureDir, relativeTo, writeFileEnsured, writeJsonEnsured } from "../shared/fs-utils"; -import type { - AgentDescriptor, - AgentTask, - AgentTaskPacket, - FirewallDecision, - FirewallPolicyPack, - FirewallSummary, - FirewallToolRule, - GovernanceTrigger, - ProjectContext, - RiskLevel -} from "../shared/types"; - -const SENSITIVE_PATTERNS = [ - /security/i, - /auth/i, - /secret/i, - /infra/i, - /deploy/i, - /compliance/i, - /architecture/i, - /structural/i, - /dependency/i, - /incident/i -]; - -const WRITE_INTENT_PATTERNS = [ - /refactor/i, - /implementation/i, - /rewrite/i, - /migration/i, - /maintainability/i, - /frontend information architecture/i, - /structural changes/i -]; - -function matchesAny(value: string, patterns: RegExp[]): boolean { - return patterns.some((pattern) => pattern.test(value)); -} - -function detectScopePaths(agentId: string, context: ProjectContext): string[] { - const manifests = context.discovery.manifests.slice(0, 2); - const infraFiles = context.discovery.infraFiles.slice(0, 2); - const apiFiles = context.discovery.apiFiles.slice(0, 2); - const topLevel = context.discovery.structure.topLevelDirectories.slice(0, 4); - - if (agentId === "qa-agent") { - return ["tests/", ...manifests]; - } - - if (agentId === "security-agent" || agentId === "dependency-agent") { - return [...manifests, ...infraFiles]; - } - - if (agentId === "auth-agent") { - return ["src/lib/auth/", "app/api/auth/", "middleware.", ...apiFiles]; - } - - if (agentId === "infra-agent") { - return [...infraFiles, ".github/", "config/"]; - } - - if (agentId === "documentation-agent" || agentId === "product-owner-agent") { - return ["docs/", "README.md", ...apiFiles]; - } - - if (agentId === "observability-agent") { - return [...infraFiles, "integrations/", "reports/"]; - } - - if (agentId === "ux-agent" || agentId === "ux-improvement-agent") { - return ["src/", "docs/", "README.md"]; - } - - return topLevel.length > 0 ? topLevel : ["src/", "docs/"]; -} - -function detectContextPaths(context: ProjectContext): string[] { - return [ - path.join(context.memoryDir, "PROJECT_MODEL.md"), - path.join(context.memoryDir, "ARCHITECTURE.md"), - path.join(context.memoryDir, "STACK_PROFILE.md"), - path.join(context.memoryDir, "ANNOTATIONS.md"), - path.join(context.memoryDir, "RULES.md") - ]; -} - -function expectedOutputsFor(descriptor: AgentDescriptor): string[] { - return [ - `${descriptor.displayName} markdown report with findings and recommendations.`, - "Proposal artifacts only if the agent emits actionable recommendations.", - "No direct target-repository mutation." - ]; -} - -function classifyRisk( - descriptor: AgentDescriptor, - task: AgentTask, - context: ProjectContext -): { score: number; riskLevel: RiskLevel; reasons: string[] } { - let score = 0; - const reasons: string[] = []; - const descriptorText = `${descriptor.capabilities.join(" ")} ${descriptor.requiresHumanApprovalFor.join(" ")}`; - const taskText = `${task.title} ${task.description} ${task.rationale}`; - - const triggerWeights: Record = { - manual: 1, - "repository-change": 2, - "weekly-review": 1, - "security-audit": 4, - "security-advisory": 4, - "architecture-review": 3, - "incident-detection": 4, - "dependency-update": 3 - }; - - score += triggerWeights[task.trigger]; - reasons.push(`trigger=${task.trigger}`); - - if (matchesAny(descriptorText, SENSITIVE_PATTERNS) || matchesAny(taskText, SENSITIVE_PATTERNS)) { - score += 2; - reasons.push("sensitive-domain"); - } - - if (matchesAny(descriptorText, WRITE_INTENT_PATTERNS)) { - score += 1; - reasons.push("change-heavy-domain"); - } - - if (context.discovery.infrastructure.length > 0 || context.discovery.infraFiles.length > 0) { - score += 1; - reasons.push("infrastructure-present"); - } - - if (context.discovery.git.isGitRepo) { - score += 1; - reasons.push("git-governed"); - } - - const riskLevel: RiskLevel = score >= 7 ? "high" : score >= 4 ? "medium" : "low"; - return { score, riskLevel, reasons }; -} - -function selectPolicyPack( - descriptor: AgentDescriptor, - task: AgentTask, - riskLevel: RiskLevel -): { policyPack: FirewallPolicyPack; rationale: string } { - const descriptorText = `${descriptor.capabilities.join(" ")} ${descriptor.requiresHumanApprovalFor.join(" ")}`.toLowerCase(); - - if (descriptorText.includes("deploy") || task.title.toLowerCase().includes("deploy")) { - return { - policyPack: "deploy", - rationale: "Task touches deploy-adjacent responsibilities and must stay behind explicit approvals." - }; - } - - if ( - riskLevel === "high" || - task.trigger === "incident-detection" || - task.trigger === "security-audit" || - task.trigger === "security-advisory" - ) { - return { - policyPack: "review", - rationale: "High-risk or incident/security-triggered tasks stay in review mode." - }; - } - - if (matchesAny(descriptorText, WRITE_INTENT_PATTERNS)) { - return { - policyPack: "edit-limited", - rationale: "Task influences implementation strategy, so future writes must remain tightly scoped." - }; - } - - if (matchesAny(descriptorText, SENSITIVE_PATTERNS)) { - return { - policyPack: "review", - rationale: "Sensitive domains require extra controls even in analysis mode." - }; - } - - return { - policyPack: "safe-readonly", - rationale: "Task is analysis-first and can remain in a readonly operating posture." - }; -} - -function buildToolRules(policyPack: FirewallPolicyPack): FirewallToolRule[] { - const shared: FirewallToolRule[] = [ - { - tool: "read-repository", - mode: "allow", - rationale: "Repository inspection is required for every governed task." - }, - { - tool: "read-generated-context", - mode: "allow", - rationale: "Agents may consume generated project context and prior artifacts." - }, - { - tool: "write-generated-artifacts", - mode: "allow", - rationale: "Agents may write reports and proposals into output artifacts." - }, - { - tool: "read-git", - mode: "allow", - rationale: "Git metadata is safe to inspect for review and impact analysis." - } - ]; - - if (policyPack === "safe-readonly") { - return [ - ...shared, - { tool: "run-tests", mode: "deny", rationale: "Readonly tasks should not expand into active execution." }, - { tool: "run-build", mode: "deny", rationale: "Readonly tasks avoid build-side effects." }, - { tool: "write-target-files", mode: "deny", rationale: "Target files remain immutable in readonly mode." }, - { tool: "delete-target-files", mode: "deny", rationale: "Destructive file operations are blocked." }, - { tool: "write-git", mode: "deny", rationale: "Git writes are blocked by default." }, - { tool: "network-egress", mode: "deny", rationale: "Network access is denied unless explicitly approved." }, - { tool: "deploy", mode: "deny", rationale: "Deployment is outside readonly scope." } - ]; - } - - if (policyPack === "review") { - return [ - ...shared, - { tool: "run-tests", mode: "allow", rationale: "Review-mode tasks may validate assumptions with tests." }, - { tool: "run-build", mode: "approval-required", rationale: "Build execution is allowed only with explicit approval." }, - { tool: "write-target-files", mode: "deny", rationale: "Review mode remains non-destructive." }, - { tool: "delete-target-files", mode: "deny", rationale: "Destructive file operations remain blocked." }, - { tool: "write-git", mode: "deny", rationale: "Git writes are not allowed in review mode." }, - { tool: "network-egress", mode: "approval-required", rationale: "External access requires explicit approval." }, - { tool: "deploy", mode: "deny", rationale: "Deployment remains blocked." } - ]; - } - - if (policyPack === "edit-limited") { - return [ - ...shared, - { tool: "run-tests", mode: "allow", rationale: "Scoped edits should be validated with tests." }, - { tool: "run-build", mode: "allow", rationale: "Scoped edits may require local build validation." }, - { tool: "write-target-files", mode: "approval-required", rationale: "Target writes require a human decision and explicit scope." }, - { tool: "delete-target-files", mode: "deny", rationale: "Deletion stays blocked even in edit-limited mode." }, - { tool: "write-git", mode: "deny", rationale: "Git writes remain blocked until a higher trust mode exists." }, - { tool: "network-egress", mode: "approval-required", rationale: "External access remains gated." }, - { tool: "deploy", mode: "deny", rationale: "Deployment is outside edit-limited scope." } - ]; - } - - return [ - ...shared, - { tool: "run-tests", mode: "allow", rationale: "Deploy-class tasks need validation." }, - { tool: "run-build", mode: "allow", rationale: "Deploy-class tasks need build verification." }, - { tool: "write-target-files", mode: "approval-required", rationale: "Production-adjacent changes require approval." }, - { tool: "delete-target-files", mode: "approval-required", rationale: "Destructive actions require explicit approval." }, - { tool: "write-git", mode: "approval-required", rationale: "Git writes require review." }, - { tool: "network-egress", mode: "approval-required", rationale: "Network access requires review." }, - { tool: "deploy", mode: "approval-required", rationale: "Deployment always requires explicit approval." } - ]; -} - -function buildConstraints( - descriptor: AgentDescriptor, - task: AgentTask, - policyPack: FirewallPolicyPack -): string[] { - const constraints = [ - "Do not modify the target repository automatically.", - "Constrain work to the approved scope paths and generated artifacts.", - "Escalate ambiguity instead of assuming permission." - ]; - - if (policyPack === "safe-readonly") { - constraints.push("Stay readonly: analyze, summarize, and propose only."); - } - - if (policyPack === "review") { - constraints.push("Treat findings as review material; no target writes are permitted."); - } - - if (policyPack === "edit-limited") { - constraints.push("If the task is ever promoted to edit mode, writes must stay within the scoped files only."); - } - - for (const item of descriptor.requiresHumanApprovalFor) { - constraints.push(`Human approval required for ${item}.`); - } - - constraints.push(`Current trigger: ${task.trigger}.`); - return constraints; -} - -function decide( - descriptor: AgentDescriptor, - policyPack: FirewallPolicyPack, - riskLevel: RiskLevel -): { decision: FirewallDecision; requiresHumanApproval: boolean; requiredApprovals: string[]; rationale: string } { - const requiredApprovals = [...descriptor.requiresHumanApprovalFor]; - const unsafeAction = descriptor.allowedActions.find((action) => !["analyze", "propose", "report"].includes(action)); - - if (unsafeAction) { - return { - decision: "BLOCKED", - requiresHumanApproval: true, - requiredApprovals, - rationale: `Blocked because descriptor declared unsupported action ${unsafeAction}.` - }; - } - - if (policyPack === "deploy") { - return { - decision: "ALLOW_WITH_REVIEW", - requiresHumanApproval: true, - requiredApprovals: [...requiredApprovals, "deploy approval"], - rationale: "Deploy-class tasks may proceed only behind explicit human review." - }; - } - - if (policyPack === "edit-limited" || riskLevel === "high" || requiredApprovals.length > 0) { - return { - decision: "ALLOW_WITH_REVIEW", - requiresHumanApproval: true, - requiredApprovals, - rationale: "Task may run in analysis mode, but any promotion beyond readonly work requires review." - }; - } - - return { - decision: "ALLOW", - requiresHumanApproval: false, - requiredApprovals, - rationale: "Task fits the current non-destructive operating mode." - }; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderToolRules(toolRules: FirewallToolRule[]): string { - return toolRules - .map((rule) => `- ${rule.tool}: ${rule.mode} (${rule.rationale})`) - .join("\n"); -} - -interface PlannedAgentTask { - task: AgentTask; - descriptor: AgentDescriptor; -} - -export class AgentFirewall { - async assessPlan( - context: ProjectContext, - trigger: GovernanceTrigger, - plannedTasks: PlannedAgentTask[] - ): Promise { - const packetDir = path.join(context.taskBoardDir, "packets"); - const policyDir = path.join(context.runtimeMemoryDir, "firewall"); - const reportPath = path.join(context.reportsDir, "agent_firewall.md"); - const policyPath = path.join(policyDir, "agent_firewall.json"); - - await ensureDir(packetDir); - await ensureDir(policyDir); - - const packets: AgentTaskPacket[] = []; - - for (const planned of plannedTasks) { - const { score, riskLevel, reasons } = classifyRisk(planned.descriptor, planned.task, context); - const { policyPack, rationale: policyRationale } = selectPolicyPack(planned.descriptor, planned.task, riskLevel); - const toolRules = buildToolRules(policyPack); - const decision = decide(planned.descriptor, policyPack, riskLevel); - const packetPath = path.join(packetDir, `${planned.task.taskId}.md`); - const scopePaths = detectScopePaths(planned.task.agentId, context); - const contextPaths = detectContextPaths(context).map((filePath) => relativeTo(context.outputPath, filePath)); - const packet: AgentTaskPacket = { - taskId: planned.task.taskId, - agentId: planned.task.agentId, - trigger, - goal: planned.task.description, - scopePaths, - contextPaths, - constraints: buildConstraints(planned.descriptor, planned.task, policyPack), - expectedOutput: expectedOutputsFor(planned.descriptor), - policyPack, - riskLevel, - decision: decision.decision, - decisionRationale: `${decision.rationale} Policy=${policyPack}. Risk score=${score} (${reasons.join(", ")}). ${policyRationale}`, - requiresHumanApproval: decision.requiresHumanApproval, - requiredApprovals: decision.requiredApprovals, - toolRules, - packetPath - }; - - await writeFileEnsured( - packetPath, - `# Task Packet: ${planned.task.taskId} - -## Agent - -- Agent: ${planned.descriptor.displayName} -- Trigger: ${trigger} -- Goal: ${packet.goal} - -## Firewall decision - -- Decision: ${packet.decision} -- Policy pack: ${packet.policyPack} -- Risk level: ${packet.riskLevel} -- Requires human approval: ${packet.requiresHumanApproval ? "yes" : "no"} -- Rationale: ${packet.decisionRationale} - -## Scope paths - -${renderList(packet.scopePaths)} - -## Context paths - -${renderList(packet.contextPaths)} - -## Constraints - -${renderList(packet.constraints)} - -## Expected output - -${renderList(packet.expectedOutput)} - -## Tool rules - -${renderToolRules(packet.toolRules)} - -## Required approvals - -${renderList(packet.requiredApprovals)} -` - ); - - packets.push(packet); - } - - const stats: FirewallSummary["stats"] = { - allowed: packets.filter((packet) => packet.decision === "ALLOW").length, - reviewRequired: packets.filter((packet) => packet.decision === "ALLOW_WITH_REVIEW").length, - blocked: packets.filter((packet) => packet.decision === "BLOCKED").length, - lowRisk: packets.filter((packet) => packet.riskLevel === "low").length, - mediumRisk: packets.filter((packet) => packet.riskLevel === "medium").length, - highRisk: packets.filter((packet) => packet.riskLevel === "high").length, - byPolicyPack: { - "safe-readonly": packets.filter((packet) => packet.policyPack === "safe-readonly").length, - review: packets.filter((packet) => packet.policyPack === "review").length, - "edit-limited": packets.filter((packet) => packet.policyPack === "edit-limited").length, - deploy: packets.filter((packet) => packet.policyPack === "deploy").length - } - }; - - const summary: FirewallSummary = { - generatedAt: new Date().toISOString(), - trigger, - reportPath, - policyPath, - packetDir, - packets, - stats - }; - - await writeJsonEnsured(policyPath, summary); - await writeFileEnsured( - reportPath, - `# Agent Firewall Report - -## Overview - -- Repository: ${context.repoName} -- Trigger: ${trigger} -- Packets: ${packets.length} -- Allowed: ${stats.allowed} -- Review required: ${stats.reviewRequired} -- Blocked: ${stats.blocked} -- Risk distribution: low=${stats.lowRisk}, medium=${stats.mediumRisk}, high=${stats.highRisk} - -## Policy packs - -- safe-readonly: ${stats.byPolicyPack["safe-readonly"]} -- review: ${stats.byPolicyPack.review} -- edit-limited: ${stats.byPolicyPack["edit-limited"]} -- deploy: ${stats.byPolicyPack.deploy} - -## Task decisions - -${renderList( - packets.map( - (packet) => - `${packet.taskId} | ${packet.agentId} | ${packet.decision} | policy=${packet.policyPack} | risk=${packet.riskLevel} | approvals=${packet.requiredApprovals.join(", ") || "None"}` - ) -)} - -## Blocked operations - -${renderList( - [...new Set( - packets.flatMap((packet) => - packet.toolRules.filter((rule) => rule.mode === "deny").map((rule) => `${packet.agentId}: ${rule.tool}`) - ) - )] -)} -` - ); - - return summary; - } -} diff --git a/governance/agent-registry.ts b/governance/agent-registry.ts deleted file mode 100644 index b4f8544..0000000 --- a/governance/agent-registry.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { AgentCatalogEntry } from "../agents/catalog"; -import { StructuredLogger } from "../shared/logger"; -import type { GovernanceTrigger } from "../shared/types"; - -export interface RegisteredAgent extends AgentCatalogEntry {} - -export class AgentRegistry { - private readonly agents = new Map(); - private readonly logger = new StructuredLogger("agent-registry"); - - register(entry: AgentCatalogEntry): void { - this.agents.set(entry.descriptor.agentId, entry); - this.logger.debug("Registered agent", { - component: "agent", - agent: entry.descriptor.agentId, - action: "register", - version: entry.descriptor.version - }); - } - - registerAll(entries: AgentCatalogEntry[]): void { - for (const entry of entries) { - this.register(entry); - } - } - - list(): RegisteredAgent[] { - return [...this.agents.values()]; - } - - get(agentId: string): RegisteredAgent | undefined { - const entry = this.agents.get(agentId); - this.logger.debug("Resolved agent from registry", { - component: "agent", - agent: agentId, - action: "registry_lookup", - found: Boolean(entry) - }); - return entry; - } - - forTrigger(trigger: GovernanceTrigger): RegisteredAgent[] { - if (trigger === "manual") { - return this.list(); - } - - const selected = this.list().filter((entry) => entry.descriptor.triggers.includes(trigger)); - for (const entry of selected) { - this.logger.info("Selected agent for trigger", { - component: "agent", - agent: entry.descriptor.agentId, - action: "analysis_start", - trigger - }); - } - return selected; - } -} diff --git a/governance/agent-supervisor.ts b/governance/agent-supervisor.ts deleted file mode 100644 index 5a1b338..0000000 --- a/governance/agent-supervisor.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { StructuredLogger } from "../shared/logger"; -import type { - AgentDescriptor, - AgentEvaluationScore, - AgentExecutionRecord, - AgentReport, - AgentTask, - ProposalStatus -} from "../shared/types"; - -const SAFE_ACTIONS = new Set(["analyze", "propose", "report"]); -const SENSITIVE_SIGNALS = ["architecture", "structural", "security", "auth", "infra", "secret", "compliance"]; - -export const GOVERNANCE_RULES = [ - "Agents cannot execute destructive operations.", - "Agents cannot commit code.", - "Agents cannot merge pull requests.", - "Agents cannot deploy infrastructure.", - "Agents can only analyze, propose, and report.", - "Human approval is required for structural changes, architectural decisions, and security-sensitive proposals." -]; - -export class AgentSupervisor { - private readonly logger = new StructuredLogger("agent-supervisor"); - private readonly executionRecords: AgentExecutionRecord[] = []; - - enforceSafety(descriptor: AgentDescriptor): void { - for (const action of descriptor.allowedActions) { - if (!SAFE_ACTIONS.has(action)) { - throw new Error(`Unsafe agent action detected for ${descriptor.agentId}: ${action}`); - } - } - } - - start(task: AgentTask, descriptor: AgentDescriptor): void { - this.enforceSafety(descriptor); - this.executionRecords.push({ - agentId: descriptor.agentId, - taskId: task.taskId, - startedAt: new Date().toISOString(), - status: "running" - }); - this.logger.info("Agent execution started", { - component: "agent", - agent: descriptor.agentId, - action: "agent_start", - taskId: task.taskId - }); - } - - complete(taskId: string): void { - this.executionRecords.forEach((record) => { - if (record.taskId === taskId && record.status === "running") { - record.completedAt = new Date().toISOString(); - record.status = "completed"; - this.logger.info("Agent execution completed", { - component: "agent", - agent: record.agentId, - action: "agent_complete", - taskId: record.taskId - }); - } - }); - } - - fail(taskId: string, error: string): void { - this.executionRecords.forEach((record) => { - if (record.taskId === taskId && record.status === "running") { - record.completedAt = new Date().toISOString(); - record.status = "failed"; - record.error = error; - this.logger.error("Agent execution failed", { - component: "agent", - agent: record.agentId, - action: "agent_failed", - taskId: record.taskId, - error - }); - } - }); - } - - requiresHumanApproval(descriptor: AgentDescriptor, report: AgentReport): boolean { - const combinedText = `${report.findings.join(" ")} ${report.recommendations.join(" ")}`.toLowerCase(); - return ( - descriptor.requiresHumanApprovalFor.length > 0 && - SENSITIVE_SIGNALS.some((signal) => combinedText.includes(signal)) - ); - } - - classifyProposal( - descriptor: AgentDescriptor, - report: AgentReport, - score: AgentEvaluationScore - ): { status: ProposalStatus; rationale: string } { - const combinedText = `${report.findings.join(" ")} ${report.recommendations.join(" ")}`.toLowerCase(); - const hasSensitiveSignal = SENSITIVE_SIGNALS.some((signal) => combinedText.includes(signal)); - - if (report.recommendations.length === 0 || score.overallScore < 0.45) { - return { - status: "REJECTED", - rationale: "Proposal rejected because the agent signal was too weak or no actionable recommendation was produced." - }; - } - - if (this.requiresHumanApproval(descriptor, report) || report.riskLevel === "high" || hasSensitiveSignal) { - return { - status: "REQUIRES_HUMAN_REVIEW", - rationale: "Proposal touches a governed or high-risk area and must be reviewed by a human before backlog approval." - }; - } - - if (score.overallScore >= 0.72) { - return { - status: "APPROVED", - rationale: "Proposal is actionable, low-risk, and strong enough to be approved into the human backlog." - }; - } - - return { - status: "REQUIRES_HUMAN_REVIEW", - rationale: "Proposal is plausible but needs human review because the confidence signal is not yet strong enough for automatic backlog approval." - }; - } - - records(): AgentExecutionRecord[] { - return [...this.executionRecords]; - } -} diff --git a/governance/autonomous-scheduler.ts b/governance/autonomous-scheduler.ts deleted file mode 100644 index 026cdbb..0000000 --- a/governance/autonomous-scheduler.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { AgentRegistry, type RegisteredAgent } from "./agent-registry"; - -import type { GovernanceTrigger } from "../shared/types"; - -export interface ScheduledCycle { - cadence: "event-driven" | "daily" | "weekly"; - trigger: GovernanceTrigger; - agentIds: string[]; - rationale: string; -} - -export class AutonomousScheduler { - constructor(private readonly registry: AgentRegistry) {} - - selectAgents(trigger: GovernanceTrigger): RegisteredAgent[] { - const registered = this.registry.forTrigger(trigger); - return registered.length > 0 ? registered : this.registry.list(); - } - - describeCycles(): ScheduledCycle[] { - return [ - { - cadence: "event-driven", - trigger: "repository-change", - agentIds: ["product-owner-agent", "qa-agent", "ux-agent", "ux-improvement-agent", "dev-agent", "documentation-agent"], - rationale: "Immediate repo-change review focused on regressions, usability friction, frontend follow-up tasks, maintainability, and docs drift." - }, - { - cadence: "daily", - trigger: "security-audit", - agentIds: ["security-agent", "auth-agent", "infra-agent", "dependency-agent", "qa-agent", "observability-agent", "dev-agent"], - rationale: "Structured security audit across auth, infra, dependency hygiene, abuse safety, observability, and code-level remediation." - }, - { - cadence: "weekly", - trigger: "weekly-review", - agentIds: ["product-owner-agent", "qa-agent", "ux-agent", "ux-improvement-agent", "dev-agent", "optimization-agent", "documentation-agent"], - rationale: "Weekly platform review of product friction, UX risk, frontend implementation backlog, quality, performance, and documentation." - }, - { - cadence: "weekly", - trigger: "architecture-review", - agentIds: ["architecture-agent", "dev-agent", "optimization-agent", "observability-agent", "documentation-agent"], - rationale: "Structured architecture drift review across runtime boundaries and operational readiness." - } - ]; - } -} diff --git a/governance/message-center.ts b/governance/message-center.ts deleted file mode 100644 index 72fb0b8..0000000 --- a/governance/message-center.ts +++ /dev/null @@ -1,118 +0,0 @@ -import path from "node:path"; - -import { writeJsonEnsured } from "../shared/fs-utils"; -import type { AgentMessage, AgentPriority, AgentReport, AgentTask } from "../shared/types"; - -function createMessageId(): string { - return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; -} - -export class AgentMessageCenter { - private readonly messages: AgentMessage[] = []; - - send(message: Omit): AgentMessage { - const envelope: AgentMessage = { - ...message, - messageId: createMessageId(), - timestamp: new Date().toISOString() - }; - this.messages.push(envelope); - return envelope; - } - - seedTaskAssignments(tasks: AgentTask[], trigger: string): void { - for (const task of tasks) { - this.send({ - sender: "AgentCouncil", - recipient: task.agentId, - taskId: task.taskId, - type: "QUESTION", - payload: { - instruction: task.description, - trigger, - rationale: task.rationale - }, - priority: task.priority - }); - } - } - - recordAnalysisResult(task: AgentTask, report: AgentReport): void { - this.send({ - sender: task.agentId, - recipient: "AgentCouncil", - taskId: task.taskId, - type: "ANALYSIS_RESULT", - payload: { - title: report.title, - riskLevel: report.riskLevel, - findingsCount: report.findings.length, - recommendationsCount: report.recommendations.length - }, - priority: task.priority - }); - } - - coordinateFollowUps(task: AgentTask, report: AgentReport): void { - if (task.agentId === "security-agent" && report.riskLevel === "high") { - this.send({ - sender: "security-agent", - recipient: "dev-agent", - taskId: task.taskId, - type: "ESCALATION", - payload: { - reason: "High-risk security finding needs engineering follow-up.", - summary: report.summary - }, - priority: "critical" - }); - } - - if (task.agentId === "architecture-agent" && report.findings.length > 0) { - this.send({ - sender: "architecture-agent", - recipient: "documentation-agent", - taskId: task.taskId, - type: "FEEDBACK", - payload: { - reason: "Architecture findings should be reflected in docs.", - findings: report.findings - }, - priority: "high" - }); - } - - if (task.agentId === "qa-agent" && report.riskLevel === "high") { - this.send({ - sender: "qa-agent", - recipient: "product-owner-agent", - taskId: task.taskId, - type: "PROPOSAL", - payload: { - reason: "Quality risk should influence product prioritization.", - recommendations: report.recommendations - }, - priority: "high" - }); - } - } - - escalateToHuman(task: AgentTask, reason: string, priority: AgentPriority): void { - this.send({ - sender: "AgentCouncil", - recipient: "HumanApproval", - taskId: task.taskId, - type: "ESCALATION", - payload: { reason }, - priority - }); - } - - list(): AgentMessage[] { - return [...this.messages]; - } - - async persist(taskBoardDir: string): Promise { - await writeJsonEnsured(path.join(taskBoardDir, "messages.json"), this.messages); - } -} diff --git a/governance/proposal-consensus.ts b/governance/proposal-consensus.ts deleted file mode 100644 index 3fa0dab..0000000 --- a/governance/proposal-consensus.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { - AgentReport, - ProposalConsensusState -} from "../shared/types"; - -export interface ProposalConsensus { - consensusScore: number; - consensusState: ProposalConsensusState; - supportingAgents: string[]; - consensusThemes: string[]; -} - -const THEME_RULES: Array<{ theme: string; pattern: RegExp }> = [ - { theme: "tests", pattern: /\b(test|coverage|regression|smoke|qa)\b/i }, - { theme: "ci", pattern: /\b(ci|pipeline|quality gate|workflow)\b/i }, - { theme: "logging", pattern: /\b(logging|logger|structured log)\b/i }, - { theme: "telemetry", pattern: /\b(metric|telemetry|tracing|alert)\b/i }, - { theme: "api", pattern: /\b(api|openapi|swagger|contract|schema)\b/i }, - { theme: "security", pattern: /\b(security|auth|secret|dependency|vulnerab|permission|compliance)\b/i }, - { theme: "architecture", pattern: /\b(architecture|coupling|boundary|module|refactor|maintainab|drift)\b/i }, - { theme: "documentation", pattern: /\b(doc|runbook|onboarding|readme|guide)\b/i }, - { theme: "performance", pattern: /\b(performance|latency|memory|cpu|bloat|hotspot|optimi)\b/i }, - { theme: "ux", pattern: /\b(ux|ui|usability|workflow|operator|experience)\b/i } -]; - -function extractThemes(text: string): string[] { - const normalized = text.trim(); - if (!normalized) { - return []; - } - - const matches = THEME_RULES - .filter((rule) => rule.pattern.test(normalized)) - .map((rule) => rule.theme); - - return [...new Set(matches)]; -} - -function themesForReport(report: AgentReport): Set { - return new Set( - [...report.findings, ...report.recommendations, report.summary].flatMap((entry) => extractThemes(entry)) - ); -} - -export function assessProposalConsensus( - proposalText: string, - sourceAgentId: string, - agentReports: AgentReport[] -): ProposalConsensus { - const proposalThemes = extractThemes(proposalText); - if (proposalThemes.length === 0) { - return { - consensusScore: 0, - consensusState: "weak", - supportingAgents: [], - consensusThemes: [] - }; - } - - const peers = agentReports.filter((report) => report.agentId !== sourceAgentId); - const supporters: string[] = []; - const overlapThemes = new Set(); - - for (const report of peers) { - const reportThemes = themesForReport(report); - const overlap = proposalThemes.filter((theme) => reportThemes.has(theme)); - - if (overlap.length === 0) { - continue; - } - - supporters.push(report.agentId); - for (const theme of overlap) { - overlapThemes.add(theme); - } - } - - const consensusScore = peers.length === 0 ? 1 : supporters.length / peers.length; - const consensusState: ProposalConsensusState = - consensusScore >= 0.67 || supporters.length >= 2 - ? "strong" - : consensusScore >= 0.34 || supporters.length >= 1 - ? "moderate" - : "weak"; - - return { - consensusScore, - consensusState, - supportingAgents: supporters.sort((left, right) => left.localeCompare(right)), - consensusThemes: [...overlapThemes].sort((left, right) => left.localeCompare(right)) - }; -} diff --git a/governance/self-governance-system.ts b/governance/self-governance-system.ts deleted file mode 100644 index 842b75a..0000000 --- a/governance/self-governance-system.ts +++ /dev/null @@ -1,785 +0,0 @@ -import { promises as fs } from "node:fs"; -import path from "node:path"; - -import { buildAgentCatalog } from "../agents/catalog"; -import { AgentFirewall } from "./agent-firewall"; -import { AgentCouncil } from "./agent-council"; -import { AgentEvaluator } from "./agent-evaluator"; -import { AgentMessageCenter } from "./message-center"; -import { assessProposalConsensus } from "./proposal-consensus"; -import { AgentRegistry } from "./agent-registry"; -import { AgentSupervisor, GOVERNANCE_RULES } from "./agent-supervisor"; -import { AgentTaskBoard } from "./task-board"; -import { AutonomousScheduler } from "./autonomous-scheduler"; -import { AgentLearningStore } from "../memory/learnings"; -import { StructuredLogger } from "../shared/logger"; -import { readTextSafe, writeFileEnsured } from "../shared/fs-utils"; -import type { - AgentEvaluationScore, - AgentReport, - GovernanceSummary, - GovernanceTrigger, - LearningRecord, - ProjectContext, - ProposalArtifact -} from "../shared/types"; - -function sortByScore(scores: AgentEvaluationScore[]): AgentEvaluationScore[] { - return [...scores].sort((left, right) => right.overallScore - left.overallScore); -} - -function createProposalId(agentId: string, index: number): string { - return `proposal_${agentId}_${Date.now()}_${index}_${Math.random().toString(36).slice(2, 8)}`; -} - -function slugify(value: string): string { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") - .slice(0, 40); -} - -function proposalFileName(agentId: string, index: number, title: string): string { - return `proposal_${String(index).padStart(2, "0")}_${agentId.replace(/-/g, "_")}_${slugify(title)}.md`; -} - -function renderGovernanceRules(): string { - return GOVERNANCE_RULES.map((rule) => `- ${rule}`).join("\n"); -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function sanitizeArtifactPath(filePath: string): boolean { - return !filePath.startsWith("tests/fixtures/") && !filePath.startsWith("sample-output/"); -} - -function defaultAffectedFiles(agentId: string, context: ProjectContext): string[] { - const manifests = context.discovery.manifests.filter(sanitizeArtifactPath); - const infraFiles = context.discovery.infraFiles.filter(sanitizeArtifactPath); - const apiFiles = context.discovery.apiFiles.filter(sanitizeArtifactPath); - - if (agentId === "qa-agent") { - return ["tests/", "package.json", ...manifests.slice(0, 2)]; - } - if (agentId === "security-agent") { - return [...manifests.slice(0, 2), ...infraFiles.slice(0, 2)]; - } - if (agentId === "auth-agent") { - return ["src/lib/auth/", "app/api/auth/", ...apiFiles.slice(0, 2)]; - } - if (agentId === "infra-agent") { - return [...infraFiles.slice(0, 3), ".github/", "config/"]; - } - if (agentId === "documentation-agent") { - return ["docs/", ...apiFiles.slice(0, 2)]; - } - if (agentId === "observability-agent") { - return [...infraFiles.slice(0, 2), "integrations/logs/", "integrations/metrics/"]; - } - if (agentId === "optimization-agent") { - return [...manifests.slice(0, 2), ...infraFiles.slice(0, 2)]; - } - if (agentId === "ux-agent") { - return ["README.md", "docs/", ...apiFiles.slice(0, 2)]; - } - if (agentId === "ux-improvement-agent") { - return ["src/", "reports/ux_report.md", "reports/usability_findings.md", "reports/workflow_analysis.md"]; - } - if (agentId === "product-owner-agent") { - return ["README.md", "docs/", "AI_CONTEXT/"]; - } - if (agentId === "architecture-agent" || agentId === "dev-agent") { - return context.discovery.structure.topLevelDirectories.slice(0, 4); - } - - return context.discovery.structure.topLevelDirectories.slice(0, 3); -} - -function extractAffectedFiles(markdown: string): string[] { - const matches = [ - ...markdown.matchAll(/Affected files:\s*(.+)/gi), - ...markdown.matchAll(/files:\s*([^)]+)\)/gi) - ]; - - return [...new Set( - matches - .flatMap((match) => (match[1] ?? "").split(/[,;]/)) - .map((value) => value.trim()) - .map((value) => value.replace(/\b(difficulty|confidence)\b.*$/i, "").trim()) - .filter((value) => Boolean(value) && sanitizeArtifactPath(value)) - .filter((value) => /\/|\.md$|\.json$|\.ya?ml$|\.ts$|\.js$/.test(value)) - )].sort((left, right) => left.localeCompare(right)); -} - -function expectedBenefitFor(agentId: string, riskLevel: AgentReport["riskLevel"]): string { - const suffix = - riskLevel === "high" - ? " It reduces near-term operational and delivery risk." - : riskLevel === "medium" - ? " It improves reliability and maintainability." - : " It keeps the system easier to evolve safely."; - - if (agentId === "qa-agent") { - return `Increase regression protection and confidence in future autonomous proposals.${suffix}`; - } - if (agentId === "security-agent") { - return `Reduce security exposure, dependency risk, and compliance surprises.${suffix}`; - } - if (agentId === "documentation-agent") { - return `Improve discoverability, onboarding, and operational clarity.${suffix}`; - } - if (agentId === "dev-agent" || agentId === "architecture-agent") { - return `Lower architectural drift and reduce the cost of future refactors.${suffix}`; - } - if (agentId === "observability-agent") { - return `Improve diagnosability and shorten incident response time.${suffix}`; - } - if (agentId === "optimization-agent") { - return `Reduce runtime and delivery friction while preserving system safety.${suffix}`; - } - if (agentId === "ux-agent") { - return `Improve usability, workflow clarity, and operator comprehension without changing governed backend logic.${suffix}`; - } - if (agentId === "ux-improvement-agent") { - return `Translate UX findings into actionable frontend implementation tasks without changing governed backend logic.${suffix}`; - } - if (agentId === "product-owner-agent") { - return `Make engineering effort align better with user and operator value.${suffix}`; - } - - return `Provide a safer, more actionable improvement backlog for the repository.${suffix}`; -} - -function implementationSketchFor(recommendation: string, affectedFiles: string[]): string { - return [ - `Validate the issue against the current repository state and the agent report.`, - `Scope the change to: ${affectedFiles.join(", ") || "repository-wide surfaces"}.`, - `Start with: ${recommendation.replace(/\s*\(files:[^)]+\)/i, "").trim()}.`, - "Run build, tests, and smoke checks before any human approval decision." - ].join(" "); -} - -function applyConsensusGate( - decision: { status: ProposalArtifact["status"]; rationale: string }, - consensus: ReturnType, - riskLevel: AgentReport["riskLevel"] -): { status: ProposalArtifact["status"]; rationale: string } { - const rationaleParts = [decision.rationale]; - - if (consensus.consensusState === "weak" && decision.status === "APPROVED") { - rationaleParts.push("Consensus gate downgraded the proposal because peer agents did not corroborate the same concern."); - return { - status: "REQUIRES_HUMAN_REVIEW", - rationale: rationaleParts.join(" ") - }; - } - - if (consensus.consensusState === "weak" && riskLevel === "high" && decision.status !== "REJECTED") { - rationaleParts.push("High-risk proposals without peer corroboration require human review."); - return { - status: "REQUIRES_HUMAN_REVIEW", - rationale: rationaleParts.join(" ") - }; - } - - rationaleParts.push( - `Consensus=${consensus.consensusState} (${consensus.supportingAgents.length} supporting agents, score=${consensus.consensusScore.toFixed(2)}).` - ); - - return { - status: decision.status, - rationale: rationaleParts.join(" ") - }; -} - -export class AgentSelfGovernanceSystem { - private readonly logger = new StructuredLogger("agent-self-governance"); - private readonly registry = new AgentRegistry(); - private readonly firewall = new AgentFirewall(); - private readonly council = new AgentCouncil(); - private readonly supervisor = new AgentSupervisor(); - private readonly evaluator = new AgentEvaluator(); - private readonly learningStore = new AgentLearningStore(); - - constructor() { - this.registry.registerAll(buildAgentCatalog()); - } - - async run(context: ProjectContext, trigger: GovernanceTrigger = "manual"): Promise<{ - agentReports: AgentReport[]; - summary: GovernanceSummary; - }> { - const taskBoard = new AgentTaskBoard(context.taskBoardDir); - const messages = new AgentMessageCenter(); - const previousLearnings = await this.learningStore.loadAll(context.learningDir); - const { selectedAgents, tasks: plannedTasks } = this.planTasks(trigger, previousLearnings); - let tasks = plannedTasks; - const agentReports: AgentReport[] = []; - const evaluationScores: AgentEvaluationScore[] = []; - const firewallSummary = await this.firewall.assessPlan( - context, - trigger, - this.resolvePlannedTasks(tasks, selectedAgents) - ); - - await taskBoard.initialize(); - messages.seedTaskAssignments(tasks, trigger); - await taskBoard.persist(tasks); - - for (const task of tasks) { - const registered = this.registry.get(task.agentId); - if (!registered) { - continue; - } - - const packet = firewallSummary.packets.find((candidate) => candidate.taskId === task.taskId); - if (packet?.decision === "BLOCKED") { - const blockedTask = { - ...task, - state: "REJECTED" as const, - completedAt: new Date().toISOString() - }; - tasks = taskBoard.update(tasks, blockedTask); - messages.escalateToHuman( - task, - `Firewall blocked task execution: ${packet.decisionRationale}`, - task.priority === "critical" ? "critical" : "high" - ); - continue; - } - - if (packet?.decision === "ALLOW_WITH_REVIEW") { - messages.escalateToHuman( - task, - `Firewall review gate: ${packet.decisionRationale}`, - task.priority === "critical" ? "critical" : "high" - ); - } - - tasks = taskBoard.claim(tasks, task.taskId); - await taskBoard.persist(tasks); - this.supervisor.start(task, registered.descriptor); - - try { - const report = await registered.agent.run(context); - agentReports.push(report); - messages.recordAnalysisResult(task, report); - messages.coordinateFollowUps(task, report); - - const requiresHumanApproval = this.supervisor.requiresHumanApproval(registered.descriptor, report); - if (requiresHumanApproval) { - this.logger.warn("Governance escalation required", { - component: "governance", - agent: registered.descriptor.agentId, - action: "governance_escalation", - taskId: task.taskId - }); - messages.escalateToHuman(task, "Proposal touches a governed area and needs human approval.", "high"); - } - - const updatedTask = { - ...task, - state: "PROPOSED" as const, - completedAt: new Date().toISOString(), - reportPath: report.outputPath - }; - tasks = taskBoard.update(tasks, updatedTask); - this.supervisor.complete(task.taskId); - - evaluationScores.push(this.evaluator.evaluate(updatedTask, report)); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - this.supervisor.fail(task.taskId, message); - const failedTask = { - ...task, - state: "REJECTED" as const, - completedAt: new Date().toISOString() - }; - tasks = taskBoard.update(tasks, failedTask); - messages.escalateToHuman(task, `Agent execution failed: ${message}`, "critical"); - } - } - - const rankedScores = this.evaluator.rank(evaluationScores); - const conflictMessages = this.council.resolveConflicts(tasks, agentReports); - - for (const conflict of conflictMessages) { - const firstTask = tasks[0]; - if (firstTask) { - messages.escalateToHuman(firstTask, conflict, "high"); - } - } - - const proposals = await this.writeImprovementProposals(context, rankedScores, agentReports); - tasks = this.applyProposalDecisions(tasks, proposals); - const learnings = await this.deriveLearnings(context, tasks, rankedScores, agentReports, previousLearnings); - const repeatedPatterns = this.learningStore.findRepeatedPatterns([...previousLearnings, ...learnings]); - - await this.learningStore.appendBatch(context.learningDir, learnings); - await taskBoard.persist(tasks); - await messages.persist(context.taskBoardDir); - - const agentActivityReportPath = await this.writeAgentActivityReport( - context, - trigger, - tasks, - rankedScores, - previousLearnings, - proposals, - repeatedPatterns.map((pattern) => `${pattern.detectedProblem} (${pattern.count} cycles)`) - ); - const improvementReportPath = await this.writeImprovementReport( - context, - rankedScores, - agentReports, - proposals, - conflictMessages, - repeatedPatterns.map((pattern) => `${pattern.detectedProblem} | agents=${pattern.agentIds.join(", ")} | count=${pattern.count}`) - ); - - this.logger.info("Self-governance cycle completed", { - component: "governance", - action: "cycle_complete", - repoName: context.repoName, - trigger, - tasks: tasks.length, - proposals: proposals.length - }); - - return { - agentReports, - summary: { - trigger, - tasks, - messages: messages.list(), - evaluations: rankedScores, - learnings, - proposals, - executionRecords: this.supervisor.records(), - agentActivityReportPath, - improvementReportPath, - firewall: firewallSummary - } - }; - } - - async inspectFirewall( - context: ProjectContext, - trigger: GovernanceTrigger = "manual" - ) { - const previousLearnings = await this.learningStore.loadAll(context.learningDir); - const { selectedAgents, tasks } = this.planTasks(trigger, previousLearnings); - return this.firewall.assessPlan(context, trigger, this.resolvePlannedTasks(tasks, selectedAgents)); - } - - async recordFeedback( - context: ProjectContext, - input: { - agentId: string; - taskId: string; - context: string; - detectedProblem: string; - actionTaken: string; - outcome: LearningRecord["outcome"]; - confidenceScore: number; - } - ): Promise { - const record = this.learningStore.createRecord(input); - const taskBoard = new AgentTaskBoard(context.taskBoardDir); - const tasks = await taskBoard.loadAll(); - const nextState = - input.outcome === "SUCCESSFUL_PROPOSAL" - ? ("APPROVED" as const) - : input.outcome === "REJECTED_PROPOSAL" || input.outcome === "FALSE_POSITIVE" - ? ("REJECTED" as const) - : ("ARCHIVED" as const); - const updatedTasks = tasks.map((task) => - task.taskId === input.taskId - ? { - ...task, - state: nextState, - completedAt: new Date().toISOString() - } - : task - ); - - await this.learningStore.appendBatch(context.learningDir, [record]); - await taskBoard.persist(updatedTasks); - return record; - } - - private async deriveLearnings( - context: ProjectContext, - tasks: GovernanceSummary["tasks"], - rankedScores: AgentEvaluationScore[], - agentReports: AgentReport[], - previousLearnings: LearningRecord[] - ): Promise { - return tasks.flatMap((task) => { - const report = agentReports.find((candidate) => candidate.agentId === task.agentId); - const score = rankedScores.find((candidate) => candidate.taskId === task.taskId); - - if (!report || !score) { - return []; - } - - const records: LearningRecord[] = []; - const leadFinding = report.findings[0] ?? "No major issue detected"; - const repeatedCount = previousLearnings.filter( - (learning) => learning.detectedProblem.trim().toLowerCase() === leadFinding.trim().toLowerCase() - ).length; - - if (report.agentId === "architecture-agent" && report.findings.length > 0) { - records.push( - this.learningStore.createRecord({ - agentId: report.agentId, - taskId: task.taskId, - context: `Repository ${context.repoName} architecture review`, - detectedProblem: report.findings.join(" "), - actionTaken: report.recommendations.join(" "), - outcome: "ARCHITECTURAL_INSIGHT", - confidenceScore: Math.max(score.overallScore, 0.7) - }) - ); - } - - if (repeatedCount > 0 && report.findings.length > 0) { - records.push( - this.learningStore.createRecord({ - agentId: report.agentId, - taskId: task.taskId, - context: `Repeated pattern detected on trigger ${task.trigger} for ${context.repoName}`, - detectedProblem: leadFinding, - actionTaken: `Escalate pattern for governance review after ${repeatedCount + 1} consecutive detections.`, - outcome: "REPEATED_BUG_PATTERN", - confidenceScore: Math.max(score.overallScore, 0.75) - }) - ); - } - - records.push( - this.learningStore.createRecord({ - agentId: report.agentId, - taskId: task.taskId, - context: `Trigger ${task.trigger} on ${context.repoName}`, - detectedProblem: leadFinding, - actionTaken: report.recommendations[0] ?? "No proposal generated", - outcome: score.overallScore >= 0.7 ? "PENDING_REVIEW" : "MISSED_ISSUE", - confidenceScore: score.overallScore - }) - ); - - return records; - }); - } - - private async writeImprovementProposals( - context: ProjectContext, - rankedScores: AgentEvaluationScore[], - agentReports: AgentReport[] - ): Promise { - const proposals: ProposalArtifact[] = []; - let proposalIndex = 1; - - try { - const existing = await fs.readdir(context.proposalDir); - await Promise.all( - existing - .filter((entry) => entry.startsWith("proposal_") && entry.endsWith(".md")) - .map((entry) => fs.rm(path.join(context.proposalDir, entry), { force: true })) - ); - } catch { - // Proposal directory is best-effort cleanup; generation continues even if cleanup fails. - } - - for (const score of sortByScore(rankedScores)) { - const report = agentReports.find((candidate) => candidate.agentId === score.agentId); - const registered = this.registry.get(score.agentId); - if (!report || !registered || report.recommendations.length === 0) { - continue; - } - - const rawReportContent = await readTextSafe(report.outputPath); - const reportAffectedFiles = extractAffectedFiles(rawReportContent); - const decision = this.supervisor.classifyProposal(registered.descriptor, report, score); - - for (const recommendation of report.recommendations.slice(0, 2)) { - const title = recommendation.split("->")[0]?.trim() || `${registered.descriptor.displayName} proposal`; - const recommendationAffectedFiles = extractAffectedFiles(recommendation); - const affectedFiles = - recommendationAffectedFiles.length > 0 - ? recommendationAffectedFiles - : reportAffectedFiles.length > 0 - ? reportAffectedFiles - : defaultAffectedFiles(score.agentId, context); - const expectedBenefit = expectedBenefitFor(score.agentId, report.riskLevel); - const implementationSketch = implementationSketchFor(recommendation, affectedFiles); - const proposalId = createProposalId(score.agentId, proposalIndex); - const filePath = path.join(context.proposalDir, proposalFileName(score.agentId, proposalIndex, title)); - const consensus = assessProposalConsensus( - `${title}\n${report.summary}\n${recommendation}`, - score.agentId, - agentReports - ); - const gatedDecision = applyConsensusGate(decision, consensus, report.riskLevel); - const content = `# ${title} - -## Governance decision - -- Status: ${gatedDecision.status} -- Rationale: ${gatedDecision.rationale} -- Source agent: ${registered.descriptor.displayName} -- Task score: ${score.overallScore} - -## Consensus - -- State: ${consensus.consensusState} -- Score: ${consensus.consensusScore} -- Supporting agents: ${consensus.supportingAgents.join(", ") || "None"} -- Shared themes: ${consensus.consensusThemes.join(", ") || "None"} - -## Description - -${report.findings[0] ?? report.summary} - -## Files affected - -${renderList(affectedFiles)} - -## Risk level - -- ${report.riskLevel} - -## Expected benefit - -${expectedBenefit} - -## Implementation sketch - -${implementationSketch} - -## Safety - -- This is a proposal only. -- No production code, infrastructure, or pull request is modified automatically. -- Human approval is required before execution. -`; - await writeFileEnsured(filePath, content); - this.logger.info("Governance proposal generated", { - component: "governance", - agent: score.agentId, - action: "proposal_generated", - proposalId, - decision: gatedDecision.status, - filePath - }); - - proposals.push({ - proposalId, - agentId: score.agentId, - title, - summary: report.summary, - status: gatedDecision.status, - consensusScore: consensus.consensusScore, - consensusState: consensus.consensusState, - supportingAgents: consensus.supportingAgents, - consensusThemes: consensus.consensusThemes, - filePath, - riskLevel: report.riskLevel, - affectedFiles, - expectedBenefit, - implementationSketch, - decisionRationale: gatedDecision.rationale, - sourceReportPath: report.outputPath, - createdAt: new Date().toISOString() - }); - proposalIndex += 1; - - if (proposals.length >= 8) { - return proposals; - } - } - } - - return proposals; - } - - private applyProposalDecisions( - tasks: GovernanceSummary["tasks"], - proposals: ProposalArtifact[] - ): GovernanceSummary["tasks"] { - return tasks.map((task) => { - const taskProposals = proposals.filter((proposal) => proposal.agentId === task.agentId); - if (taskProposals.length === 0) { - return task; - } - - if (taskProposals.some((proposal) => proposal.status === "REQUIRES_HUMAN_REVIEW")) { - this.logger.info("Task marked for human review", { - component: "governance", - agent: task.agentId, - action: "governance_decision", - taskId: task.taskId, - decision: "REQUIRES_HUMAN_REVIEW" - }); - return { - ...task, - state: "PROPOSED" - }; - } - - if (taskProposals.some((proposal) => proposal.status === "APPROVED")) { - this.logger.info("Task approved into backlog", { - component: "governance", - agent: task.agentId, - action: "governance_decision", - taskId: task.taskId, - decision: "APPROVED" - }); - return { - ...task, - state: "APPROVED" - }; - } - - this.logger.warn("Task rejected by governance", { - component: "governance", - agent: task.agentId, - action: "governance_decision", - taskId: task.taskId, - decision: "REJECTED" - }); - return { - ...task, - state: "REJECTED" - }; - }); - } - - private async writeAgentActivityReport( - context: ProjectContext, - trigger: GovernanceTrigger, - tasks: GovernanceSummary["tasks"], - rankedScores: AgentEvaluationScore[], - previousLearnings: LearningRecord[], - proposals: ProposalArtifact[], - repeatedPatterns: string[] - ): Promise { - const reportPath = path.join(context.reportsDir, "agent_activity_report.md"); - const approvedCount = proposals.filter((proposal) => proposal.status === "APPROVED").length; - const reviewCount = proposals.filter((proposal) => proposal.status === "REQUIRES_HUMAN_REVIEW").length; - const rejectedCount = proposals.filter((proposal) => proposal.status === "REJECTED").length; - const strongConsensusCount = proposals.filter((proposal) => proposal.consensusState === "strong").length; - const content = `# Agent Activity Report - -## Runtime overview - -- Repository: ${context.repoName} -- Trigger: ${trigger} -- Agent execution count: ${tasks.length} -- Analysis coverage: ${tasks.filter((task) => task.state !== "NEW").length}/${tasks.length} -- Accepted vs rejected proposals: approved=${approvedCount}, requires_human_review=${reviewCount}, rejected=${rejectedCount}, historical learnings=${previousLearnings.length} -- Strong-consensus proposals: ${strongConsensusCount}/${proposals.length} -- Detected regressions: ${rankedScores.filter((score) => score.overallScore < 0.55).length} - -## Active governance rules - -${renderGovernanceRules()} - -## Task lifecycle snapshot - -${renderList(tasks.map((task) => `${task.taskId} | ${task.agentId} | ${task.state} | ${task.priority}`))} - -## Agent scores - -${renderList( - rankedScores.map( - (score) => `${score.rank}. ${score.agentId} | overall=${score.overallScore} | output=${score.outputQuality}` - ) - )} - -## Repeated learning patterns - -${renderList(repeatedPatterns)} -`; - await writeFileEnsured(reportPath, content); - return reportPath; - } - - private async writeImprovementReport( - context: ProjectContext, - rankedScores: AgentEvaluationScore[], - agentReports: AgentReport[], - proposals: ProposalArtifact[], - conflicts: string[], - repeatedPatterns: string[] - ): Promise { - const reportPath = path.join(context.reportsDir, "improvement_proposals.md"); - const content = `# Improvement Report - -## Ranked improvement proposals - -${renderList( - rankedScores.map((score) => { - const report = agentReports.find((candidate) => candidate.agentId === score.agentId); - return `${score.rank}. ${score.agentId} | score=${score.overallScore} | summary=${report?.summary ?? "N/A"}`; - }) - )} - -## Proposal artifacts - -${renderList( - proposals.map( - (proposal) => - `${proposal.title} | status=${proposal.status} | consensus=${proposal.consensusState} (${proposal.consensusScore.toFixed(2)}) | risk=${proposal.riskLevel} | files=${proposal.affectedFiles.join(", ") || "None"} | path=${proposal.filePath}` - ) - )} - -## Expected benefits - -${renderList(proposals.map((proposal) => `${proposal.title} -> ${proposal.expectedBenefit}`))} - -## Conflict and escalation summary - -${renderList(conflicts)} - -## Repeated patterns from learning memory - -${renderList(repeatedPatterns)} -`; - await writeFileEnsured(reportPath, content); - await writeFileEnsured(path.join(context.reportsDir, "improvement_report.md"), content); - return reportPath; - } - - private planTasks(trigger: GovernanceTrigger, previousLearnings: LearningRecord[]) { - const scheduler = new AutonomousScheduler(this.registry); - const selectedAgents = scheduler.selectAgents(trigger); - const tasks = this.council.planTasks(selectedAgents, trigger, previousLearnings); - - return { - selectedAgents, - tasks - }; - } - - private resolvePlannedTasks( - tasks: GovernanceSummary["tasks"], - selectedAgents: ReturnType - ) { - return tasks.flatMap((task) => { - const selected = selectedAgents.find((entry) => entry.descriptor.agentId === task.agentId); - return selected - ? [ - { - task, - descriptor: selected.descriptor - } - ] - : []; - }); - } -} diff --git a/governance/task-board.ts b/governance/task-board.ts deleted file mode 100644 index eeb1e86..0000000 --- a/governance/task-board.ts +++ /dev/null @@ -1,60 +0,0 @@ -import path from "node:path"; - -import { readJsonSafe, writeJsonEnsured } from "../shared/fs-utils"; -import type { AgentTask } from "../shared/types"; - -interface TaskBoardSnapshot { - backlog: AgentTask[]; - active: AgentTask[]; - completed: AgentTask[]; -} - -function classify(tasks: AgentTask[]): TaskBoardSnapshot { - return { - backlog: tasks.filter((task) => task.state === "NEW"), - active: tasks.filter((task) => task.state === "ANALYZING" || task.state === "PROPOSED"), - completed: tasks.filter((task) => ["APPROVED", "REJECTED", "ARCHIVED"].includes(task.state)) - }; -} - -export class AgentTaskBoard { - constructor(private readonly taskBoardDir: string) {} - - async initialize(): Promise { - const existingBacklog = await readJsonSafe(path.join(this.taskBoardDir, "backlog.json")); - - if (!existingBacklog) { - await this.persist([]); - } - } - - async loadAll(): Promise { - const backlog = (await readJsonSafe(path.join(this.taskBoardDir, "backlog.json"))) ?? []; - const active = (await readJsonSafe(path.join(this.taskBoardDir, "active.json"))) ?? []; - const completed = (await readJsonSafe(path.join(this.taskBoardDir, "completed.json"))) ?? []; - return [...backlog, ...active, ...completed]; - } - - async persist(tasks: AgentTask[]): Promise { - const grouped = classify(tasks); - await writeJsonEnsured(path.join(this.taskBoardDir, "backlog.json"), grouped.backlog); - await writeJsonEnsured(path.join(this.taskBoardDir, "active.json"), grouped.active); - await writeJsonEnsured(path.join(this.taskBoardDir, "completed.json"), grouped.completed); - } - - claim(tasks: AgentTask[], taskId: string): AgentTask[] { - return tasks.map((task) => - task.taskId === taskId - ? { - ...task, - state: "ANALYZING", - claimedAt: new Date().toISOString() - } - : task - ); - } - - update(tasks: AgentTask[], updatedTask: AgentTask): AgentTask[] { - return tasks.map((task) => (task.taskId === updatedTask.taskId ? updatedTask : task)); - } -} diff --git a/integrations/ci/index.ts b/integrations/ci/index.ts deleted file mode 100644 index f92a400..0000000 --- a/integrations/ci/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { uniqueSorted } from "../../shared/fs-utils"; - -import type { CiInfo } from "../../shared/types"; - -export function detectCi(files: string[]): CiInfo { - const providers = new Set(); - const configFiles: string[] = []; - - for (const file of files) { - const lower = file.toLowerCase(); - - if (lower.startsWith(".github/workflows/")) { - providers.add("GitHub Actions"); - configFiles.push(file); - } - - if (lower === ".gitlab-ci.yml") { - providers.add("GitLab CI"); - configFiles.push(file); - } - - if (lower === ".circleci/config.yml" || lower === "circle.yml") { - providers.add("CircleCI"); - configFiles.push(file); - } - - if (lower === "azure-pipelines.yml") { - providers.add("Azure Pipelines"); - configFiles.push(file); - } - } - - return { - providers: uniqueSorted([...providers]), - configFiles: uniqueSorted(configFiles) - }; -} diff --git a/integrations/git/index.ts b/integrations/git/index.ts deleted file mode 100644 index dbe122e..0000000 --- a/integrations/git/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { inspectGit } from "../../tools/git_tools"; - -import type { GitInfo } from "../../shared/types"; - -export function detectGitIntegration(targetPath: string, hasSubmodules: boolean): GitInfo { - return inspectGit(targetPath, hasSubmodules); -} diff --git a/integrations/logs/index.ts b/integrations/logs/index.ts deleted file mode 100644 index 831c307..0000000 --- a/integrations/logs/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { uniqueSorted } from "../../shared/fs-utils"; - -import type { DependencyManifest, LoggingInfo } from "../../shared/types"; - -const LOGGING_DEPENDENCIES = [ - "pino", - "winston", - "bunyan", - "loguru", - "structlog", - "logback", - "serilog" -]; - -const STRUCTURED_LOGGING_DEPENDENCIES = ["pino", "winston", "loguru", "structlog", "serilog"]; - -export function detectLogging(files: string[], manifests: DependencyManifest[]): LoggingInfo { - const dependencySet = new Set( - manifests.flatMap((manifest) => manifest.dependencies.map((dependency) => dependency.toLowerCase())) - ); - const frameworks = LOGGING_DEPENDENCIES.filter((dependency) => dependencySet.has(dependency)); - const configFiles = files.filter((file) => /log(back|ging)|logger/i.test(file)); - - return { - frameworks: uniqueSorted(frameworks), - configFiles: uniqueSorted(configFiles), - structured: frameworks.some((framework) => STRUCTURED_LOGGING_DEPENDENCIES.includes(framework)) - }; -} diff --git a/integrations/metrics/index.ts b/integrations/metrics/index.ts deleted file mode 100644 index c9789d9..0000000 --- a/integrations/metrics/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { uniqueSorted } from "../../shared/fs-utils"; - -import type { DependencyManifest, MetricsInfo } from "../../shared/types"; - -const METRIC_DEPENDENCIES = [ - "@opentelemetry/api", - "opentelemetry", - "prom-client", - "prometheus-client", - "prometheus-fastapi-instrumentator", - "sentry", - "@sentry/node", - "datadog", - "dd-trace", - "newrelic" -]; - -export function detectMetrics(files: string[], manifests: DependencyManifest[]): MetricsInfo { - const dependencySet = new Set( - manifests.flatMap((manifest) => manifest.dependencies.map((dependency) => dependency.toLowerCase())) - ); - const tools = METRIC_DEPENDENCIES.filter((dependency) => dependencySet.has(dependency)); - const configFiles = files.filter((file) => /grafana|prometheus|otel|opentelemetry|sentry|newrelic|datadog/i.test(file)); - const alertsConfigured = files.some((file) => /alert|pagerduty|opsgenie/i.test(file)); - - return { - tools: uniqueSorted(tools), - configFiles: uniqueSorted(configFiles), - alertsConfigured - }; -} diff --git a/integrations/ollama_adapter.ts b/integrations/ollama_adapter.ts deleted file mode 100644 index b3bca9a..0000000 --- a/integrations/ollama_adapter.ts +++ /dev/null @@ -1,142 +0,0 @@ -export interface OllamaListResponse { - models?: Array<{ - name?: string; - }>; -} - -export type OllamaModelResidency = "local" | "remote"; - -export interface OllamaModelDescriptor { - name: string; - residency: OllamaModelResidency; - offlineCapable: boolean; -} - -export interface OllamaGenerateResponse { - response?: string; -} - -interface OllamaGenerateRequest { - model: string; - prompt: string; - stream: false; - think?: boolean; -} - -export interface LocalModelAskOptions { - timeoutMs?: number; -} - -export interface LocalModelAdapter { - listModels(): Promise; - listModelDescriptors?(): Promise; - ask(prompt: string, model: string, options?: LocalModelAskOptions): Promise; -} - -export const DEFAULT_OLLAMA_TIMEOUT_MS = 180_000; - -function withDefaultTag(model: string): string { - return model.includes(":") ? model : `${model}:latest`; -} - -function parseTimeoutMs(value: unknown): number | undefined { - const numeric = typeof value === "number" ? value : Number(value); - if (!Number.isFinite(numeric) || numeric <= 0) { - return undefined; - } - - return numeric; -} - -function classifyModelResidency(model: string): OllamaModelResidency { - const normalized = model.trim().toLowerCase(); - return normalized.endsWith(":cloud") ? "remote" : "local"; -} - -export class OllamaAdapter implements LocalModelAdapter { - constructor( - private readonly baseUrl = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434", - private readonly timeoutMs = DEFAULT_OLLAMA_TIMEOUT_MS - ) {} - - private resolveTimeoutMs(override?: number): number { - return parseTimeoutMs(override) ?? parseTimeoutMs(process.env.OLLAMA_TIMEOUT_MS) ?? parseTimeoutMs(this.timeoutMs) ?? DEFAULT_OLLAMA_TIMEOUT_MS; - } - - async listModels(): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/tags`, { - method: "GET", - signal: AbortSignal.timeout(this.resolveTimeoutMs()) - }); - - if (!response.ok) { - return []; - } - - const payload = (await response.json()) as OllamaListResponse; - return [...new Set((payload.models ?? []).map((model) => model.name).filter(Boolean) as string[])].sort((left, right) => left.localeCompare(right)); - } catch { - return []; - } - } - - async listModelDescriptors(): Promise { - const models = await this.listModels(); - return models.map((name) => { - const residency = classifyModelResidency(name); - return { - name, - residency, - offlineCapable: residency === "local" - }; - }); - } - - async ask(prompt: string, model: string, options: LocalModelAskOptions = {}): Promise { - const request: OllamaGenerateRequest = { - model: withDefaultTag(model), - prompt, - stream: false, - think: false - }; - - const withThinkDisabled = await fetch(`${this.baseUrl}/api/generate`, { - method: "POST", - headers: { - "content-type": "application/json" - }, - body: JSON.stringify(request), - signal: AbortSignal.timeout(this.resolveTimeoutMs(options.timeoutMs)) - }); - - let response = withThinkDisabled; - if (!response.ok && response.status >= 400 && response.status < 500) { - const retryRequest: OllamaGenerateRequest = { - model: request.model, - prompt: request.prompt, - stream: false - }; - - response = await fetch(`${this.baseUrl}/api/generate`, { - method: "POST", - headers: { - "content-type": "application/json" - }, - body: JSON.stringify(retryRequest), - signal: AbortSignal.timeout(this.resolveTimeoutMs(options.timeoutMs)) - }); - } - - if (!response.ok) { - throw new Error(`Ollama request failed with status ${response.status}`); - } - - const payload = (await response.json()) as OllamaGenerateResponse; - if (!payload.response) { - throw new Error("Ollama response did not include generated content."); - } - - return payload.response.trim(); - } -} diff --git a/memory/annotations/index.ts b/memory/annotations/index.ts deleted file mode 100644 index b8f137c..0000000 --- a/memory/annotations/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import path from "node:path"; - -import { ensureDir, readJsonSafe, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ContextAnnotation } from "../../shared/types"; - -function annotationsDir(outputPath: string): string { - return path.join(outputPath, "memory", "annotations"); -} - -function annotationsIndexPath(outputPath: string): string { - return path.join(annotationsDir(outputPath), "index.json"); -} - -function annotationsArtifactPath(outputPath: string): string { - return path.join(outputPath, "AI_CONTEXT", "ANNOTATIONS.md"); -} - -function sortAnnotations(annotations: ContextAnnotation[]): ContextAnnotation[] { - return [...annotations].sort((left, right) => left.scope.localeCompare(right.scope)); -} - -function renderAnnotations(annotations: ContextAnnotation[]): string { - if (annotations.length === 0) { - return "# ANNOTATIONS\n\n- None recorded.\n"; - } - - return `# ANNOTATIONS - -${annotations - .map( - (annotation) => `## ${annotation.scope} - -- Updated: ${annotation.updatedAt} -- Created: ${annotation.createdAt} - -${annotation.note} -` - ) - .join("\n")}`.trimEnd() + "\n"; -} - -export async function listContextAnnotations(outputPath: string): Promise { - const annotations = (await readJsonSafe(annotationsIndexPath(outputPath))) ?? []; - return sortAnnotations(annotations); -} - -export async function readContextAnnotation( - outputPath: string, - scope: string -): Promise { - const annotations = await listContextAnnotations(outputPath); - return annotations.find((annotation) => annotation.scope === scope); -} - -export async function writeAnnotationsArtifact( - outputPath: string, - annotations?: ContextAnnotation[] -): Promise { - const resolvedAnnotations = annotations ?? (await listContextAnnotations(outputPath)); - const artifactPath = annotationsArtifactPath(outputPath); - await writeFileEnsured(artifactPath, renderAnnotations(resolvedAnnotations)); - return artifactPath; -} - -export async function writeContextAnnotation( - outputPath: string, - scope: string, - note: string -): Promise { - const existing = await listContextAnnotations(outputPath); - const previous = existing.find((annotation) => annotation.scope === scope); - const timestamp = new Date().toISOString(); - const nextAnnotation: ContextAnnotation = { - scope, - note, - createdAt: previous?.createdAt ?? timestamp, - updatedAt: timestamp - }; - const remaining = existing.filter((annotation) => annotation.scope !== scope); - const nextAnnotations = sortAnnotations([...remaining, nextAnnotation]); - - await ensureDir(annotationsDir(outputPath)); - await writeJsonEnsured(annotationsIndexPath(outputPath), nextAnnotations); - await writeAnnotationsArtifact(outputPath, nextAnnotations); - - return nextAnnotation; -} - -export async function clearContextAnnotation(outputPath: string, scope: string): Promise { - const existing = await listContextAnnotations(outputPath); - const nextAnnotations = existing.filter((annotation) => annotation.scope !== scope); - - if (nextAnnotations.length === existing.length) { - await writeAnnotationsArtifact(outputPath, existing); - return false; - } - - await ensureDir(annotationsDir(outputPath)); - await writeJsonEnsured(annotationsIndexPath(outputPath), nextAnnotations); - await writeAnnotationsArtifact(outputPath, nextAnnotations); - return true; -} diff --git a/memory/context_registry/ecosystem_radar.ts b/memory/context_registry/ecosystem_radar.ts deleted file mode 100644 index dce247e..0000000 --- a/memory/context_registry/ecosystem_radar.ts +++ /dev/null @@ -1,771 +0,0 @@ -import path from "node:path"; - -import { ensureDir, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - ContextRegistryEntry, - ContextTrustLevel, - EcosystemRadarCandidate, - EcosystemRadarResult, - ProjectContext -} from "../../shared/types"; -import { contextRegistryPaths, writeDynamicContextRegistryEntries } from "./index"; - -interface RadarSeedRepo { - id: string; - fullName: string; - title: string; - category: string; - summary: string; - tags: string[]; - guidance: string[]; - relatedIds: string[]; - keywords: string[]; -} - -interface RadarBucket { - id: string; - title: string; - query: string; - category: string; - keywords: string[]; - guidance: string[]; - relatedIds: string[]; -} - -interface GitHubRepoRecord { - name: string; - full_name: string; - html_url: string; - description: string | null; - stargazers_count: number; - forks_count: number; - language: string | null; - topics?: string[]; - pushed_at?: string; - default_branch?: string; - archived?: boolean; - disabled?: boolean; - homepage?: string | null; - license?: { - spdx_id?: string | null; - name?: string | null; - } | null; - owner: { - login: string; - }; -} - -interface GitHubSearchResponse { - items: GitHubRepoRecord[]; -} - -interface GitHubReadmeResponse { - content?: string; - encoding?: string; -} - -const RADAR_SEEDS: RadarSeedRepo[] = [ - { - id: "repomix-context-ingestion", - fullName: "yamadashy/repomix", - title: "Repomix Context Ingestion", - category: "context-ingestion", - summary: "Reference for packing repositories into AI-friendly context bundles with filters, token-aware output, and repo-to-prompt workflows.", - tags: ["repomix", "context", "ingestion", "repo-packing", "tokens", "prompt"], - guidance: [ - "Use it as a reference for repo-to-context export, ignore rules, and token-budget aware packaging.", - "Extract packaging patterns into project-brain reports or prompts instead of embedding another runtime wholesale.", - "Compare its include/exclude surface with context-lite and external repository workflows." - ], - relatedIds: ["nextjs-application", "react-frontend-foundations"], - keywords: ["context", "repo", "pack", "prompt", "tokens", "ingest"] - }, - { - id: "gitingest-repo-ingestion", - fullName: "coderamp-labs/gitingest", - title: "Gitingest Repository Ingestion", - category: "context-ingestion", - summary: "Reference for ingesting a Git repository into compact, prompt-friendly context that downstream agents can consume quickly.", - tags: ["gitingest", "context", "repo", "ingestion", "prompt", "analysis"], - guidance: [ - "Use it to benchmark how fast project-brain can derive compact onboarding context from a cold repo.", - "Borrow ideas around prompt-friendly shaping, not around replacing the local memory model.", - "Contrast its output with AI_CONTEXT and context-lite summaries." - ], - relatedIds: ["repomix-context-ingestion"], - keywords: ["context", "ingest", "repo", "prompt", "summary"] - }, - { - id: "ast-grep-structural-search", - fullName: "ast-grep/ast-grep", - title: "ast-grep Structural Search", - category: "code-analysis", - summary: "Reference for structural code search, linting, and AST-aware pattern matching that can strengthen evidence collection beyond plain text grep.", - tags: ["ast-grep", "ast", "search", "lint", "rewrite", "static-analysis"], - guidance: [ - "Use it to improve code-surface discovery where ripgrep is too shallow or too noisy.", - "Prefer structural search for navigation, API handlers, and contract patterns that depend on syntax shape.", - "Keep it optional and read-first before considering any rewrite workflow." - ], - relatedIds: ["review-delta-minimal-context"], - keywords: ["ast", "structural", "search", "rewrite", "lint", "pattern"] - }, - { - id: "langgraph-agent-graphs", - fullName: "langchain-ai/langgraph", - title: "LangGraph Agent Graphs", - category: "agent-runtime", - summary: "Reference for resilient agent runtimes modeled as graphs with explicit state transitions, retries, and durable execution patterns.", - tags: ["langgraph", "agents", "graph", "runtime", "orchestration", "state"], - guidance: [ - "Use it to benchmark stateful orchestration patterns for planner, synthesis, and retry flows.", - "Borrow graph and checkpoint ideas where project-brain needs stronger long-running workflow boundaries.", - "Avoid coupling project-brain to a framework unless the boundary stays optional." - ], - relatedIds: ["review-delta-minimal-context"], - keywords: ["agent", "graph", "runtime", "orchestration", "state", "workflow"] - }, - { - id: "deepagentsjs-subagents", - fullName: "langchain-ai/deepagentsjs", - title: "DeepAgentsJS Subagent Runtime", - category: "agent-runtime", - summary: "Reference for deep planning, filesystem-aware agents, and subagent delegation in JavaScript runtimes.", - tags: ["deepagents", "subagents", "planning", "filesystem", "langchain", "runtime"], - guidance: [ - "Use it as the benchmark for the optional deep agent path, not as a replacement for project-brain governance.", - "Borrow planning and subagent decomposition ideas where swarm needs more autonomy.", - "Keep repo access read-only and bounded when importing these patterns." - ], - relatedIds: ["langgraph-agent-graphs"], - keywords: ["deepagents", "subagent", "planning", "filesystem", "agent"] - }, - { - id: "langmem-agent-memory", - fullName: "langchain-ai/langmem", - title: "LangMem Agent Memory", - category: "memory", - summary: "Reference for explicit memory extraction, persistence, and background memory workflows for agents.", - tags: ["langmem", "memory", "agents", "learning", "persistence", "background"], - guidance: [ - "Use it to benchmark how project-brain stores learnings, durable context, and post-run memory extraction.", - "Focus on memory lifecycle and recall quality, not on adopting a framework wholesale.", - "Contrast it with DECISIONS, LEARNINGS, TASKS, and persistent context store behavior." - ], - relatedIds: ["deepagentsjs-subagents", "langgraph-agent-graphs"], - keywords: ["memory", "agents", "learning", "recall", "persistence"] - }, - { - id: "aider-terminal-coding-loop", - fullName: "Aider-AI/aider", - title: "Aider Terminal Coding Loop", - category: "developer-loop", - summary: "Reference for terminal-first AI coding loops, diff discipline, and developer-controlled editing workflows.", - tags: ["aider", "terminal", "coding", "diff", "workflow", "developer-loop"], - guidance: [ - "Use it to benchmark how project-brain should hand off context and tasks into a coding loop without taking over edits.", - "Borrow interaction patterns around diff review, user control, and small-surface changes.", - "Keep project-brain focused on intelligence and review, not direct code ownership." - ], - relatedIds: ["review-delta-minimal-context"], - keywords: ["terminal", "coding", "diff", "workflow", "review"] - }, - { - id: "continue-source-controlled-ai", - fullName: "continuedev/continue", - title: "Continue Source-Controlled AI", - category: "developer-loop", - summary: "Reference for source-controlled AI checks, prompts, and review workflows that live close to the codebase.", - tags: ["continue", "checks", "prompts", "review", "source-controlled", "developer-loop"], - guidance: [ - "Use it to benchmark project-brain prompt registries, review checks, and repo-local AI configuration patterns.", - "Borrow ideas for controlled checks and repo-owned prompt surfaces.", - "Keep project-brain outputs auditable and review-first." - ], - relatedIds: ["github-actions-ci", "review-delta-minimal-context"], - keywords: ["checks", "review", "prompt", "source-controlled", "config"] - }, - { - id: "openhands-agent-runtime", - fullName: "OpenHands/OpenHands", - title: "OpenHands Agent Runtime", - category: "agent-runtime", - summary: "Reference for AI-driven development runtimes with task execution, tooling boundaries, and developer-facing orchestration.", - tags: ["openhands", "agents", "runtime", "automation", "tooling", "developer-loop"], - guidance: [ - "Use it to benchmark runtime UX, task orchestration, and execution boundaries for local agent workflows.", - "Borrow patterns for bounded tool execution and operator visibility.", - "Do not collapse project-brain into a general-purpose coding runtime." - ], - relatedIds: ["deepagentsjs-subagents", "aider-terminal-coding-loop"], - keywords: ["agent", "runtime", "execution", "tooling", "automation"] - } -]; - -const RADAR_BUCKETS: RadarBucket[] = [ - { - id: "agent-runtime", - title: "Agent runtimes and orchestration", - query: "\"agent runtime\" OR \"agent framework\" in:description,readme stars:>200", - category: "agent-runtime", - keywords: ["agent", "runtime", "framework", "orchestration", "workflow", "planning"], - guidance: [ - "Prioritize repos with explicit task/state boundaries over hype-only wrappers.", - "Look for read-only analysis patterns, approval gates, and durable execution models." - ], - relatedIds: ["deepagentsjs-subagents", "langgraph-agent-graphs", "openhands-agent-runtime"] - }, - { - id: "context-ingestion", - title: "Repository context ingestion", - query: "\"repo context\" OR \"repository context\" OR ingest in:description,readme stars:>50", - category: "context-ingestion", - keywords: ["context", "repo", "repository", "ingest", "prompt", "pack"], - guidance: [ - "Favor repos that convert codebases into compact, prompt-friendly, reproducible context bundles.", - "Extract packaging and filtering patterns that improve context-lite." - ], - relatedIds: ["repomix-context-ingestion", "gitingest-repo-ingestion"] - }, - { - id: "developer-loop", - title: "Developer-controlled AI loops", - query: "\"AI pair programming\" OR \"coding assistant\" in:description,readme stars:>200", - category: "developer-loop", - keywords: ["coding", "assistant", "review", "diff", "terminal", "developer"], - guidance: [ - "Favor repos that preserve user control, show diffs clearly, and avoid opaque automation.", - "Use these as references for handoff UX, not for replacing project-brain." - ], - relatedIds: ["aider-terminal-coding-loop", "continue-source-controlled-ai"] - }, - { - id: "memory", - title: "Agent memory systems", - query: "\"agent memory\" OR langmem in:description,readme stars:>50", - category: "memory", - keywords: ["memory", "agent", "recall", "learning", "persistence", "context"], - guidance: [ - "Favor repos with explicit memory extraction, storage, and recall semantics.", - "Use them to harden LEARNINGS and persistent context instead of adding vague chat history." - ], - relatedIds: ["langmem-agent-memory"] - }, - { - id: "code-analysis", - title: "Structural code analysis", - query: "\"structural search\" OR \"AST search\" in:description,readme stars:>100", - category: "code-analysis", - keywords: ["ast", "search", "structural", "analysis", "lint", "rewrite"], - guidance: [ - "Favor repos that provide syntax-aware search and rule systems over raw text matching.", - "Use them to improve evidence quality in reports and domain extraction." - ], - relatedIds: ["ast-grep-structural-search"] - } -]; - -function normalizeTokens(input: string): string[] { - return input - .toLowerCase() - .split(/[^a-z0-9]+/i) - .map((token) => token.trim()) - .filter((token) => token.length >= 2); -} - -function unique(values: string[]): string[] { - return [...new Set(values.filter(Boolean))]; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function slugify(value: string): string { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); -} - -function githubApiBaseUrl(): string { - return (process.env.PROJECT_BRAIN_GITHUB_API_BASE_URL ?? "https://api.github.com").replace(/\/+$/, ""); -} - -function githubHeaders(): Record { - const headers: Record = { - Accept: "application/vnd.github+json", - "User-Agent": "project-brain-ecosystem-radar" - }; - if (process.env.GITHUB_TOKEN) { - headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; - } - return headers; -} - -async function fetchGitHubJson(resourcePath: string): Promise { - const response = await fetch(`${githubApiBaseUrl()}${resourcePath}`, { - headers: githubHeaders() - }); - - if (!response.ok) { - throw new Error(`GitHub API ${response.status} for ${resourcePath}`); - } - - return response.json() as Promise; -} - -async function searchRepositories(query: string, perPage: number): Promise { - const payload = await fetchGitHubJson( - `/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=${perPage}` - ); - return payload.items ?? []; -} - -async function fetchRepository(fullName: string): Promise { - return fetchGitHubJson(`/repos/${fullName}`); -} - -async function fetchRepositoryReadme(fullName: string): Promise { - const payload = await fetchGitHubJson(`/repos/${fullName}/readme`); - if (!payload.content || payload.encoding !== "base64") { - return undefined; - } - - return Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8"); -} - -function extractReadmeSummary(markdown: string): string | undefined { - if (!markdown.trim()) { - return undefined; - } - - const paragraphs = markdown - .split(/\n\s*\n/g) - .map((block) => - block - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#") && !line.startsWith("![") && !line.startsWith("[!")) - .join(" ") - .replace(/`/g, "") - .replace(/\[(.*?)\]\([^)]*\)/g, "$1") - .replace(/\s+/g, " ") - .trim() - ) - .filter((paragraph) => paragraph.length >= 40); - - return paragraphs[0]?.slice(0, 280); -} - -function daysSince(dateValue?: string): number | undefined { - if (!dateValue) { - return undefined; - } - - const parsed = Date.parse(dateValue); - if (Number.isNaN(parsed)) { - return undefined; - } - - return Math.max(0, Math.floor((Date.now() - parsed) / 86_400_000)); -} - -function permissiveLicenseScore(license?: GitHubRepoRecord["license"]): number { - const value = (license?.spdx_id ?? license?.name ?? "").toUpperCase(); - const permissiveLicenses = new Set(["MIT", "APACHE-2.0", "BSD-3-CLAUSE", "BSD-2-CLAUSE", "ISC", "MPL-2.0"]); - return permissiveLicenses.has(value) ? 0.6 : 0; -} - -function starsScore(stars: number): number { - if (stars >= 50_000) { - return 3.2; - } - if (stars >= 10_000) { - return 2.6; - } - if (stars >= 1_000) { - return 1.8; - } - if (stars >= 200) { - return 1.2; - } - return stars > 0 ? 0.6 : 0; -} - -function activityScore(pushedAt?: string): number { - const age = daysSince(pushedAt); - if (age === undefined) { - return 0; - } - if (age <= 30) { - return 2; - } - if (age <= 90) { - return 1.5; - } - if (age <= 180) { - return 1; - } - if (age <= 365) { - return 0.5; - } - return 0; -} - -function keywordScore(text: string, keywords: string[]): { score: number; matches: string[] } { - const tokens = new Set(normalizeTokens(text)); - const matches = unique(keywords.filter((keyword) => tokens.has(keyword.toLowerCase()))); - return { - score: Math.min(2, matches.length * 0.35), - matches - }; -} - -function buildFallbackRepository(seed: RadarSeedRepo): GitHubRepoRecord { - const [owner, name] = seed.fullName.split("/"); - return { - name, - full_name: seed.fullName, - html_url: `https://github.com/${seed.fullName}`, - description: seed.summary, - stargazers_count: 0, - forks_count: 0, - language: null, - topics: seed.tags, - owner: { - login: owner - } - }; -} - -function buildContextEntry( - repo: GitHubRepoRecord, - summary: string, - trustLevel: ContextTrustLevel, - source: string, - category: string, - tags: string[], - guidance: string[], - relatedIds: string[], - entryId?: string, - title?: string -): ContextRegistryEntry { - return { - id: entryId ?? `github-${slugify(repo.full_name)}`, - title: title ?? `${repo.name} (${repo.owner.login})`, - category, - trustLevel, - source, - sourceUrl: repo.html_url, - summary, - tags: unique(tags), - guidance: unique(guidance), - relatedIds: unique(relatedIds) - }; -} - -function buildCandidate( - repo: GitHubRepoRecord, - readmeSummary: string | undefined, - seed: RadarSeedRepo | undefined, - bucket: RadarBucket | undefined -): EcosystemRadarCandidate { - const category = seed?.category ?? bucket?.category ?? "ecosystem"; - const trustLevel: ContextTrustLevel = "maintainer"; - const combinedText = [repo.description ?? "", readmeSummary ?? "", ...(repo.topics ?? [])].join(" "); - const keywords = unique([...(seed?.keywords ?? []), ...(bucket?.keywords ?? [])]); - const keywordSignals = keywordScore(combinedText, keywords); - const scoreParts: Array<{ value: number; reason: string }> = []; - - if (seed) { - scoreParts.push({ value: 2.5, reason: "repo curado para Project Brain" }); - } - if (repo.archived || repo.disabled) { - scoreParts.push({ value: -4, reason: "repo archivado o deshabilitado" }); - } - - const starValue = starsScore(repo.stargazers_count); - if (starValue > 0) { - scoreParts.push({ value: starValue, reason: `${repo.stargazers_count} stars` }); - } - - const activityValue = activityScore(repo.pushed_at); - if (activityValue > 0) { - scoreParts.push({ value: activityValue, reason: `actividad reciente (${daysSince(repo.pushed_at)} dias)` }); - } - - const licenseValue = permissiveLicenseScore(repo.license); - if (licenseValue > 0) { - scoreParts.push({ value: licenseValue, reason: `licencia permisiva (${repo.license?.spdx_id ?? repo.license?.name})` }); - } - - if (readmeSummary) { - scoreParts.push({ value: 0.4, reason: "README util para contexto operativo" }); - } - - if (repo.language && ["typescript", "javascript", "python", "rust", "go"].includes(repo.language.toLowerCase())) { - scoreParts.push({ value: 0.4, reason: `stack afín (${repo.language})` }); - } - - if (keywordSignals.score > 0) { - scoreParts.push({ value: keywordSignals.score, reason: `fit por keywords (${keywordSignals.matches.join(", ")})` }); - } - - const score = Number(scoreParts.reduce((total, part) => total + part.value, 0).toFixed(2)); - const summary = - seed?.summary ?? - repo.description ?? - readmeSummary ?? - `${repo.full_name} puede servir como referencia para ${category} dentro de project-brain.`; - const guidance = seed?.guidance ?? bucket?.guidance ?? [ - "Inspecciona README, superficie CLI y límites operativos antes de copiar patrones.", - "Extrae ideas puntuales; no reemplaces project-brain por el runtime externo." - ]; - const tags = [ - ...(seed?.tags ?? []), - ...(repo.topics ?? []), - ...(repo.language ? [repo.language.toLowerCase()] : []), - category, - ...(bucket ? [bucket.id] : []) - ]; - const relatedIds = [...(seed?.relatedIds ?? []), ...(bucket?.relatedIds ?? [])]; - const entry = buildContextEntry( - repo, - summary, - trustLevel, - seed ? "github-radar curated" : "github-radar search", - category, - tags, - guidance, - relatedIds, - seed?.id, - seed?.title - ); - - return { - entry, - repoFullName: repo.full_name, - bucketId: bucket?.id ?? category, - score, - stars: repo.stargazers_count, - forks: repo.forks_count, - primaryLanguage: repo.language ?? undefined, - pushedAt: repo.pushed_at, - reasons: scoreParts.filter((part) => part.value !== 0).map((part) => part.reason) - }; -} - -async function hydrateSeed(seed: RadarSeedRepo, notes: string[]): Promise { - try { - const [repo, readme] = await Promise.allSettled([ - fetchRepository(seed.fullName), - fetchRepositoryReadme(seed.fullName) - ]); - - const hydratedRepo = repo.status === "fulfilled" ? repo.value : buildFallbackRepository(seed); - if (repo.status !== "fulfilled") { - notes.push(`No se pudo leer metadata GitHub para ${seed.fullName}; se usó el seed local.`); - } - - if (readme.status !== "fulfilled") { - notes.push(`No se pudo leer README para ${seed.fullName}; se usó resumen local.`); - } - - return buildCandidate( - hydratedRepo, - readme.status === "fulfilled" ? extractReadmeSummary(readme.value ?? "") : undefined, - seed, - undefined - ); - } catch (error) { - notes.push(`Fallo inesperado al hidratar seed ${seed.fullName}: ${String(error)}`); - return buildCandidate(buildFallbackRepository(seed), undefined, seed, undefined); - } -} - -async function hydrateDiscoveredRepo( - repo: GitHubRepoRecord, - bucket: RadarBucket, - notes: string[] -): Promise { - let hydratedRepo = repo; - let readmeSummary: string | undefined; - - try { - hydratedRepo = await fetchRepository(repo.full_name); - } catch (error) { - notes.push(`No se pudo ampliar metadata de ${repo.full_name}: ${String(error)}`); - } - - try { - const readme = await fetchRepositoryReadme(repo.full_name); - readmeSummary = extractReadmeSummary(readme ?? ""); - } catch (error) { - notes.push(`No se pudo leer README de ${repo.full_name}: ${String(error)}`); - } - - return buildCandidate(hydratedRepo, readmeSummary, undefined, bucket); -} - -function pickBuckets(bucketId?: string): RadarBucket[] { - return bucketId ? RADAR_BUCKETS.filter((bucket) => bucket.id === bucketId) : RADAR_BUCKETS; -} - -function pickSeeds(bucketId?: string): RadarSeedRepo[] { - if (!bucketId) { - return RADAR_SEEDS; - } - - const bucket = RADAR_BUCKETS.find((candidate) => candidate.id === bucketId); - if (!bucket) { - return []; - } - - const relatedIds = new Set(bucket.relatedIds); - return RADAR_SEEDS.filter( - (seed) => - seed.category === bucket.category || - relatedIds.has(seed.id) || - seed.relatedIds.some((relatedId) => relatedIds.has(relatedId)) - ); -} - -function renderCandidate(candidate: EcosystemRadarCandidate): string { - return `## ${candidate.entry.title} - -- Repo: ${candidate.repoFullName} -- Source: ${candidate.entry.sourceUrl} -- Category: ${candidate.entry.category} -- Bucket: ${candidate.bucketId} -- Score: ${candidate.score} -- Stars/Forks: ${candidate.stars}/${candidate.forks} -- Primary language: ${candidate.primaryLanguage ?? "unknown"} -- Last push: ${candidate.pushedAt ?? "unknown"} -- Why it matters: ${candidate.reasons.join(" | ") || "seed curado"} - -${candidate.entry.summary} -`; -} - -export async function runEcosystemRadar( - context: ProjectContext, - options: { - limit?: number; - bucketId?: string; - seedOnly?: boolean; - } = {} -): Promise { - const limit = Math.max(1, options.limit ?? 6); - const seedOnly = options.seedOnly ?? false; - const paths = contextRegistryPaths(context.outputPath); - const reportPath = path.join(context.outputPath, "reports", "ecosystem_radar.md"); - const cachePath = path.join(paths.cacheDir, "ecosystem_radar.json"); - const notes: string[] = []; - const buckets = pickBuckets(options.bucketId); - - await ensureDir(paths.baseDir); - await ensureDir(paths.entriesDir); - await ensureDir(paths.cacheDir); - await ensureDir(paths.externalContextDir); - - if (!process.env.GITHUB_TOKEN) { - notes.push("GITHUB_TOKEN no está configurado; GitHub API corre en modo público con rate limits más bajos."); - } - - if (options.bucketId && buckets.length === 0) { - notes.push(`No se encontró el bucket \`${options.bucketId}\`; el radar solo materializó seeds compatibles.`); - } - - const selectedSeeds = pickSeeds(options.bucketId); - const seedCandidates = await Promise.all(selectedSeeds.map((seed) => hydrateSeed(seed, notes))); - const discoveredCandidates: EcosystemRadarCandidate[] = []; - - if (!seedOnly) { - const seenRepositories = new Set(seedCandidates.map((candidate) => candidate.repoFullName.toLowerCase())); - const rawDiscoveries = new Map(); - - for (const bucket of buckets) { - try { - const searchHits = await searchRepositories(bucket.query, Math.max(limit, 4)); - for (const repo of searchHits) { - const key = repo.full_name.toLowerCase(); - if (seenRepositories.has(key) || rawDiscoveries.has(key)) { - continue; - } - rawDiscoveries.set(key, { repo, bucket }); - } - } catch (error) { - notes.push(`No se pudo consultar bucket ${bucket.id}: ${String(error)}`); - } - } - - const hydrated = await Promise.all( - [...rawDiscoveries.values()] - .sort((left, right) => right.repo.stargazers_count - left.repo.stargazers_count || left.repo.full_name.localeCompare(right.repo.full_name)) - .slice(0, Math.max(limit * 2, 8)) - .map(({ repo, bucket }) => hydrateDiscoveredRepo(repo, bucket, notes)) - ); - - discoveredCandidates.push( - ...hydrated - .sort((left, right) => right.score - left.score || left.repoFullName.localeCompare(right.repoFullName)) - .slice(0, limit) - ); - } - - const candidates = [...seedCandidates, ...discoveredCandidates].sort( - (left, right) => right.score - left.score || left.entry.title.localeCompare(right.entry.title) - ); - - await writeDynamicContextRegistryEntries( - context.outputPath, - candidates.map((candidate) => candidate.entry) - ); - await writeJsonEnsured(cachePath, { - generatedAt: new Date().toISOString(), - limit, - bucketId: options.bucketId ?? "all", - seedOnly, - curatedSeedCount: seedCandidates.length, - discoveredCount: discoveredCandidates.length, - candidates, - notes - }); - await writeFileEnsured( - reportPath, - `# Ecosystem Radar - -## Run - -- Repository: ${context.repoName} -- Output: ${context.outputPath} -- Seed only: ${seedOnly ? "yes" : "no"} -- Bucket filter: ${options.bucketId ?? "all"} -- Additional discovery limit: ${limit} -- Materialized entries: ${candidates.length} -- Curated seeds: ${seedCandidates.length} -- Discovered candidates: ${discoveredCandidates.length} - -## Notes - -${renderList(notes)} - -## Curated Seeds - -${seedCandidates.length > 0 ? seedCandidates.map((candidate) => renderCandidate(candidate)).join("\n") : "- None"} - -## Discovered Candidates - -${discoveredCandidates.length > 0 ? discoveredCandidates.map((candidate) => renderCandidate(candidate)).join("\n") : "- None"} -` - ); - - return { - context, - reportPath, - cachePath, - candidates, - notes - }; -} diff --git a/memory/context_registry/index.ts b/memory/context_registry/index.ts deleted file mode 100644 index 7bae905..0000000 --- a/memory/context_registry/index.ts +++ /dev/null @@ -1,550 +0,0 @@ -import path from "node:path"; - -import { ensureDir, readJsonSafe, walkDirectory, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - ContextGetResult, - ContextRegistryEntry, - ContextSearchHit, - ContextSearchResult, - ContextSourcesResult, - ContextTrustLevel, - ProjectContext -} from "../../shared/types"; - -const BUILTIN_REGISTRY: ContextRegistryEntry[] = [ - { - id: "node-express-api", - title: "Node + Express API Baseline", - category: "backend", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://expressjs.com/", - summary: "Baseline guidance for small-to-medium Node services using Express, explicit route boundaries, and operational middleware.", - tags: ["node", "express", "api", "backend", "middleware", "service"], - guidance: [ - "Keep route, service, and infrastructure boundaries explicit.", - "Add structured logging, metrics, and error middleware early.", - "Prefer contract-aware APIs and smoke tests for critical routes." - ], - relatedIds: ["vitest-testing-baseline", "structured-logging-node", "metrics-prometheus-node", "openapi-contracts"] - }, - { - id: "vitest-testing-baseline", - title: "Vitest Testing Baseline", - category: "testing", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://vitest.dev/", - summary: "Minimal testing baseline for TypeScript and JavaScript repositories using Vitest for fast feedback and regression safety.", - tags: ["vitest", "testing", "qa", "coverage", "regression", "typescript"], - guidance: [ - "Start with smoke tests around core runtime paths and high-risk modules.", - "Keep fast unit tests near business logic and integration tests near boundaries.", - "Use coverage trends as a signal, not as the only quality gate." - ], - relatedIds: ["node-express-api", "review-delta-minimal-context"] - }, - { - id: "structured-logging-node", - title: "Structured Logging for Node Services", - category: "observability", - trustLevel: "maintainer", - source: "project-brain curated", - sourceUrl: "https://getpino.io/", - summary: "Guidance for JSON logs, request correlation, and log hygiene in Node backends.", - tags: ["logging", "node", "pino", "json", "observability", "backend"], - guidance: [ - "Emit machine-readable logs with stable field names.", - "Attach request or job correlation ids at ingress boundaries.", - "Avoid mixing user-facing errors with internal diagnostic detail." - ], - relatedIds: ["metrics-prometheus-node", "node-express-api"] - }, - { - id: "metrics-prometheus-node", - title: "Prometheus Metrics for Node Services", - category: "observability", - trustLevel: "maintainer", - source: "project-brain curated", - sourceUrl: "https://prometheus.io/", - summary: "Operational metrics baseline for latency, error rate, and resource usage in Node services.", - tags: ["metrics", "prometheus", "prom-client", "latency", "slo", "observability"], - guidance: [ - "Measure request rate, latency, and failure ratio for every critical surface.", - "Track queue depth and background job health if asynchronous work exists.", - "Pair metrics with alerting and runtime dashboards." - ], - relatedIds: ["structured-logging-node", "node-express-api"] - }, - { - id: "openapi-contracts", - title: "OpenAPI Contract-First APIs", - category: "api-design", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://www.openapis.org/", - summary: "Treat OpenAPI as a durable contract for backend and consumer coordination.", - tags: ["openapi", "swagger", "api", "contract", "schema", "backend"], - guidance: [ - "Keep the spec close to the service and review it like code.", - "Generate examples and contract tests for critical endpoints.", - "Avoid undocumented drift between handlers and the published API." - ], - relatedIds: ["node-express-api", "github-actions-ci"] - }, - { - id: "github-actions-ci", - title: "GitHub Actions CI Baseline", - category: "delivery", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://docs.github.com/en/actions", - summary: "Baseline CI guidance for validation, test partitioning, and artifact clarity in GitHub Actions.", - tags: ["github-actions", "ci", "pipeline", "workflow", "build", "test"], - guidance: [ - "Run typecheck, build, and the smallest useful test set on every change.", - "Keep workflow steps explicit and observable instead of hiding too much inside scripts.", - "Promote reproducible artifacts and clear failure output." - ], - relatedIds: ["vitest-testing-baseline", "docker-container-baseline"] - }, - { - id: "docker-container-baseline", - title: "Docker Service Container Baseline", - category: "infrastructure", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://docs.docker.com/", - summary: "Baseline guidance for containerized apps with small images, explicit runtime contracts, and safer defaults.", - tags: ["docker", "container", "image", "runtime", "deployment", "infra"], - guidance: [ - "Use multi-stage builds and keep runtime images minimal.", - "Make health endpoints and environment contracts explicit.", - "Run as non-root where possible and keep dependency surfaces tight." - ], - relatedIds: ["github-actions-ci", "terraform-infra-modules"] - }, - { - id: "react-frontend-foundations", - title: "React Frontend Foundations", - category: "frontend", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://react.dev/", - summary: "Operational baseline for React apps with clear state boundaries, route ownership, and UI safety checks.", - tags: ["react", "frontend", "ui", "state", "routing", "experience"], - guidance: [ - "Separate presentation from data access and state orchestration.", - "Keep navigation, loading states, and empty states intentional.", - "Document component ownership for complex shells and dashboards." - ], - relatedIds: ["nextjs-application", "vitest-testing-baseline"] - }, - { - id: "nextjs-application", - title: "Next.js Application Baseline", - category: "frontend", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://nextjs.org/docs", - summary: "Baseline guidance for Next.js apps with route structure, server/client boundaries, and deployment clarity.", - tags: ["nextjs", "react", "frontend", "ssr", "app-router", "deployment"], - guidance: [ - "Be explicit about server and client boundaries.", - "Keep data fetching close to route ownership and cache rules.", - "Document env vars and hosting assumptions early." - ], - relatedIds: ["react-frontend-foundations", "github-actions-ci"] - }, - { - id: "nestjs-service", - title: "NestJS Service Baseline", - category: "backend", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://docs.nestjs.com/", - summary: "Baseline guidance for NestJS services with modular boundaries, DTO validation, and operational clarity.", - tags: ["nestjs", "backend", "typescript", "service", "module", "api"], - guidance: [ - "Use modules to express real boundaries, not just folders.", - "Validate DTOs and keep transport contracts explicit.", - "Make background workers and side effects observable." - ], - relatedIds: ["openapi-contracts", "structured-logging-node"] - }, - { - id: "terraform-infra-modules", - title: "Terraform Infrastructure Modules", - category: "infrastructure", - trustLevel: "maintainer", - source: "project-brain curated", - sourceUrl: "https://developer.hashicorp.com/terraform/docs", - summary: "Baseline guidance for modular Terraform stacks with clear ownership and plan safety.", - tags: ["terraform", "infra", "iac", "module", "plan", "cloud"], - guidance: [ - "Keep reusable modules small and explicit about inputs/outputs.", - "Separate shared foundations from service-specific stacks.", - "Review plan diffs and destructive changes carefully." - ], - relatedIds: ["docker-container-baseline", "security-baseline"] - }, - { - id: "python-fastapi-service", - title: "Python FastAPI Service Baseline", - category: "backend", - trustLevel: "official", - source: "project-brain curated", - sourceUrl: "https://fastapi.tiangolo.com/", - summary: "Baseline guidance for FastAPI services with typed contracts, clear dependency injection, and route-level observability.", - tags: ["python", "fastapi", "backend", "api", "typing", "service"], - guidance: [ - "Keep request and response models explicit and versioned where needed.", - "Separate domain logic from transport and persistence concerns.", - "Add health checks, logging, and metrics around ingress points." - ], - relatedIds: ["openapi-contracts", "security-baseline"] - }, - { - id: "security-baseline", - title: "Application Security Baseline", - category: "security", - trustLevel: "community", - source: "project-brain curated", - sourceUrl: "https://owasp.org/", - summary: "A cross-stack baseline for secrets handling, dependency hygiene, auth boundaries, and approval discipline.", - tags: ["security", "auth", "secret", "dependency", "compliance", "approval"], - guidance: [ - "Treat secrets, auth, and permission changes as high-risk surfaces.", - "Audit dependency updates and external integrations before promotion.", - "Make approval boundaries explicit for destructive or production-adjacent actions." - ], - relatedIds: ["structured-logging-node", "terraform-infra-modules", "github-actions-ci"] - }, - { - id: "review-delta-minimal-context", - title: "Minimal Review Context Pattern", - category: "workflow", - trustLevel: "community", - source: "project-brain curated", - sourceUrl: "https://github.com/tirth8205/code-review-graph", - summary: "Review only the files, dependents, and tests that materially reduce uncertainty for a given delta.", - tags: ["review", "delta", "impact", "graph", "tests", "workflow"], - guidance: [ - "Start from changed files, then expand only to direct and transitive dependents.", - "Pull in relevant tests and contracts before adding more context.", - "Keep the review surface intentionally small to improve signal." - ], - relatedIds: ["vitest-testing-baseline", "node-express-api"] - } -]; - -export interface RegistryPaths { - baseDir: string; - entriesDir: string; - cacheDir: string; - externalContextDir: string; - searchReportPath: string; - sourcesReportPath: string; -} - -export function contextRegistryPaths(outputPath: string): RegistryPaths { - const baseDir = path.join(outputPath, "memory", "context_registry"); - return { - baseDir, - entriesDir: path.join(baseDir, "entries"), - cacheDir: path.join(baseDir, "cache"), - externalContextDir: path.join(outputPath, "AI_CONTEXT", "EXTERNAL_CONTEXT"), - searchReportPath: path.join(outputPath, "reports", "context_search.md"), - sourcesReportPath: path.join(outputPath, "reports", "context_sources.md") - }; -} - -function isContextRegistryEntry(value: unknown): value is ContextRegistryEntry { - if (!value || typeof value !== "object") { - return false; - } - - const candidate = value as Partial; - return typeof candidate.id === "string" && - typeof candidate.title === "string" && - typeof candidate.category === "string" && - typeof candidate.trustLevel === "string" && - typeof candidate.source === "string" && - typeof candidate.sourceUrl === "string" && - typeof candidate.summary === "string" && - Array.isArray(candidate.tags) && - Array.isArray(candidate.guidance) && - Array.isArray(candidate.relatedIds); -} - -async function loadDynamicRegistryEntries(outputPath: string): Promise { - const paths = contextRegistryPaths(outputPath); - await ensureDir(paths.entriesDir); - const files = (await walkDirectory(paths.entriesDir, 500)).filter((filePath) => filePath.endsWith(".json")); - const entries: ContextRegistryEntry[] = []; - - for (const relativeFile of files) { - const entry = await readJsonSafe(path.join(paths.entriesDir, relativeFile)); - if (isContextRegistryEntry(entry)) { - entries.push(entry); - } - } - - return entries; -} - -export async function loadContextRegistryEntries(outputPath: string): Promise { - const dynamicEntries = await loadDynamicRegistryEntries(outputPath); - const merged = new Map(); - - for (const entry of BUILTIN_REGISTRY) { - merged.set(entry.id, entry); - } - - for (const entry of dynamicEntries) { - merged.set(entry.id, entry); - } - - return [...merged.values()].sort((left, right) => left.title.localeCompare(right.title)); -} - -export async function writeDynamicContextRegistryEntries( - outputPath: string, - entries: ContextRegistryEntry[] -): Promise { - const paths = contextRegistryPaths(outputPath); - await ensureDir(paths.entriesDir); - const writtenPaths: string[] = []; - - for (const entry of entries) { - const filePath = path.join(paths.entriesDir, `${entry.id}.json`); - await writeJsonEnsured(filePath, entry); - writtenPaths.push(filePath); - } - - return writtenPaths; -} - -function normalizeTokens(input: string): string[] { - return input - .toLowerCase() - .split(/[^a-z0-9]+/i) - .map((token) => token.trim()) - .filter((token) => token.length >= 2); -} - -function unique(values: T[]): T[] { - return [...new Set(values)]; -} - -function scoreEntry(entry: ContextRegistryEntry, queryTokens: string[]): ContextSearchHit | undefined { - const titleTokens = normalizeTokens(entry.title); - const summaryTokens = normalizeTokens(entry.summary); - const categoryTokens = normalizeTokens(entry.category); - const idTokens = normalizeTokens(entry.id); - const tagTokens = entry.tags.flatMap((tag) => normalizeTokens(tag)); - const allTagTokens = new Set(tagTokens); - const matchedTags = queryTokens.filter((token) => allTagTokens.has(token)); - - let score = 0; - - for (const token of queryTokens) { - if (entry.id === token || entry.id.includes(token)) { - score += 6; - } - if (titleTokens.includes(token)) { - score += 4; - } - if (tagTokens.includes(token)) { - score += 3; - } - if (summaryTokens.includes(token) || categoryTokens.includes(token) || idTokens.includes(token)) { - score += 2; - } - } - - if (entry.trustLevel === "official") { - score += 0.5; - } - - if (score <= 0) { - return undefined; - } - - return { - entry, - score, - matchedTags: unique(matchedTags).sort((left, right) => left.localeCompare(right)) - }; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function entryMarkdown(entry: ContextRegistryEntry): string { - return `# ${entry.title} - -## Metadata - -- ID: ${entry.id} -- Category: ${entry.category} -- Trust: ${entry.trustLevel} -- Source: ${entry.source} -- Source URL: ${entry.sourceUrl} - -## Summary - -${entry.summary} - -## Guidance - -${renderList(entry.guidance)} - -## Tags - -${renderList(entry.tags)} - -## Related entries - -${renderList(entry.relatedIds)} -`; -} - -export async function searchContextRegistry( - context: ProjectContext, - query: string, - trust?: ContextTrustLevel -): Promise { - const paths = contextRegistryPaths(context.outputPath); - await ensureDir(paths.baseDir); - await ensureDir(paths.entriesDir); - await ensureDir(paths.cacheDir); - await ensureDir(paths.externalContextDir); - - const queryTokens = normalizeTokens(query); - const entries = await loadContextRegistryEntries(context.outputPath); - const hits = entries - .filter((entry) => !trust || entry.trustLevel === trust) - .map((entry) => scoreEntry(entry, queryTokens)) - .filter(Boolean) - .sort((left, right) => right!.score - left!.score || left!.entry.id.localeCompare(right!.entry.id)) - .slice(0, 8) as ContextSearchHit[]; - - const cachePath = path.join(paths.baseDir, "last_search.json"); - await writeJsonEnsured(cachePath, { - query, - trust: trust ?? "any", - hits - }); - await writeFileEnsured( - paths.searchReportPath, - `# Context Search - -## Query - -- Query: ${query} -- Trust filter: ${trust ?? "any"} -- Hits: ${hits.length} - -## Matches - -${hits.length > 0 - ? hits - .map( - (hit) => `## ${hit.entry.title} - -- ID: ${hit.entry.id} -- Trust: ${hit.entry.trustLevel} -- Category: ${hit.entry.category} -- Score: ${hit.score} -- Matched tags: ${hit.matchedTags.join(", ") || "None"} -- Source: ${hit.entry.sourceUrl} - -${hit.entry.summary} -` - ) - .join("\n") - : "- No matches found."} -` - ); - - return { - context, - query, - reportPath: paths.searchReportPath, - cachePath, - hits - }; -} - -export async function getContextRegistryEntry(context: ProjectContext, id: string): Promise { - const entries = await loadContextRegistryEntries(context.outputPath); - const entry = entries.find((candidate) => candidate.id === id); - if (!entry) { - throw new Error(`Unknown context entry: ${id}`); - } - - const paths = contextRegistryPaths(context.outputPath); - await ensureDir(paths.baseDir); - await ensureDir(paths.entriesDir); - await ensureDir(paths.cacheDir); - await ensureDir(paths.externalContextDir); - - const artifactPath = path.join(paths.externalContextDir, `${entry.id}.md`); - const cachePath = path.join(paths.cacheDir, `${entry.id}.json`); - - await writeFileEnsured(artifactPath, entryMarkdown(entry)); - await writeJsonEnsured(cachePath, entry); - - return { - context, - entry, - artifactPath, - cachePath - }; -} - -export async function listContextSources(context: ProjectContext): Promise { - const paths = contextRegistryPaths(context.outputPath); - await ensureDir(paths.baseDir); - await ensureDir(paths.entriesDir); - await ensureDir(paths.cacheDir); - await ensureDir(paths.externalContextDir); - - const entries = await loadContextRegistryEntries(context.outputPath); - const grouped = new Map(); - for (const entry of entries) { - const key = `${entry.source}:${entry.trustLevel}`; - const current = grouped.get(key); - if (current) { - current.entries += 1; - } else { - grouped.set(key, { - source: entry.source, - trustLevel: entry.trustLevel, - entries: 1 - }); - } - } - - const sources = [...grouped.values()].sort( - (left, right) => right.entries - left.entries || left.source.localeCompare(right.source) - ); - - await writeFileEnsured( - paths.sourcesReportPath, - `# Context Sources - -## Sources - -${sources.map((source) => `- ${source.source} | trust=${source.trustLevel} | entries=${source.entries}`).join("\n")} -` - ); - - return { - context, - reportPath: paths.sourcesReportPath, - sources - }; -} diff --git a/memory/context_store/index.ts b/memory/context_store/index.ts deleted file mode 100644 index 3c20eca..0000000 --- a/memory/context_store/index.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { promises as fs } from "node:fs"; -import path from "node:path"; - -import { appendFileEnsured, ensureDir, fileExists, readTextSafe, uniqueSorted, writeFileEnsured } from "../../shared/fs-utils"; -import { StructuredLogger } from "../../shared/logger"; -import type { AgentReport, DiscoveryResult, ProjectContext } from "../../shared/types"; - -const logger = new StructuredLogger("memory-store"); - -const DEFAULT_RULES = `# RULES - -1. Never modify target code automatically without human approval. -2. Understand the repository before proposing changes. -3. Preserve project context and decisions across runs. -4. Record errors, corrections, and learnings in durable memory. -5. Generate documentation as a first-class artifact. -6. Keep recommendations stack-aware and portable. -`; - -const DEFAULT_AGENT_ROSTER = `# AGENTS - -## ChiefAgent - -Coordinates the specialist agents and consolidates their reports. - -## Specialist agents - -- ProductAgent: UX, workflow friction, backlog opportunities -- QAAgent: testing depth, untested surfaces, likely defects -- SecurityAgent: secrets exposure, dependency hygiene, container hardening -- ObservabilityAgent: logs, metrics, alerting, operational readiness -- LegalAgent: license posture and compliance documentation gaps -- OptimizationAgent: performance, dependency bloat, runtime efficiency -- DocumentationAgent: architecture, API and runbook generation -- DevAgent: refactor and maintainability recommendations -`; - -function listOrNone(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None detected"; -} - -function inferProjectType(discovery: DiscoveryResult): string { - const frameworks = new Set(discovery.frameworks); - const infra = new Set(discovery.infrastructure); - - if (frameworks.has("NestJS") && frameworks.has("NextJS")) { - return "Full-stack platform"; - } - - if (frameworks.has("FastAPI") || frameworks.has("Express") || frameworks.has("Spring")) { - return "Backend API service"; - } - - if (frameworks.has("NextJS") || frameworks.has("React")) { - return "Frontend application"; - } - - if (infra.has("Terraform") || infra.has("Kubernetes")) { - return "Infrastructure-oriented project"; - } - - if (discovery.languages.length > 2) { - return "Polyglot software platform"; - } - - return "Software project"; -} - -function buildProjectModel(discovery: DiscoveryResult): string { - return `# PROJECT_MODEL - -Project: ${discovery.repoName} - -Type: -${inferProjectType(discovery)} - -Languages: -${discovery.languages.join(", ") || "Unknown"} - -Frameworks: -${discovery.frameworks.join(", ") || "Unknown"} - -APIs: -${discovery.apis.join(", ") || "Not detected"} - -Testing: -${discovery.testing.join(", ") || "Not detected"} - -Infrastructure: -${discovery.infrastructure.join(", ") || "Not detected"} - -Git: -${discovery.git.isGitRepo ? `${discovery.git.branch ?? "detached"} (${discovery.git.latestCommit ?? "no commit summary"})` : "Not a git repository"} -`; -} - -function buildArchitectureMap(discovery: DiscoveryResult): string { - return `# ARCHITECTURE_MAP - -## Top-level directories - -${listOrNone(discovery.structure.topLevelDirectories)} - -## Structure signals - -- Source files: ${discovery.structure.sourceFileCount} -- Test files: ${discovery.structure.testFileCount} -- Nested subrepos: ${discovery.structure.subrepos.length} -- Git submodules: ${discovery.structure.submodules.length} - -## Runtime hints - -- Frameworks: ${discovery.frameworks.join(", ") || "Unknown"} -- Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"} -- CI providers: ${discovery.ci.providers.join(", ") || "Not detected"} -- Logging: ${discovery.logging.frameworks.join(", ") || "Not detected"} -- Metrics: ${discovery.metrics.tools.join(", ") || "Not detected"} -`; -} - -function buildContextSnapshot(discovery: DiscoveryResult): string { - return `# CONTEXT - -## Repository Snapshot - -- Repository: ${discovery.repoName} -- Target: ${discovery.targetPath} -- Scanned: ${discovery.scannedAt} -- Languages: ${discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${discovery.frameworks.join(", ") || "Unknown"} -- APIs: ${discovery.apis.join(", ") || "Not detected"} -- Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"} -- Testing: ${discovery.testing.join(", ") || "Not detected"} -- Source files: ${discovery.structure.sourceFileCount} -- Test files: ${discovery.structure.testFileCount} - -## Top-level Directories - -${listOrNone(discovery.structure.topLevelDirectories.slice(0, 24))} - -## Recommendations - -${listOrNone(discovery.recommendations)} -`; -} - -async function writeIfSkeletal(filePath: string, content: string): Promise { - const existing = await readTextSafe(filePath); - const meaningfulLines = existing - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0 && !/^#/.test(line)); - - if (meaningfulLines.length === 0) { - await writeFileEnsured(filePath, content); - } -} - -function buildApiMap( - discovery: DiscoveryResult, - openApiSummaries: Array<{ path: string; title?: string; version?: string }> -): string { - const openApiSection = - openApiSummaries.length > 0 - ? openApiSummaries - .map( - (summary) => - `- ${summary.path}${summary.title ? ` | title: ${summary.title}` : ""}${summary.version ? ` | version: ${summary.version}` : ""}` - ) - .join("\n") - : "- No OpenAPI summaries available"; - - return `# API_MAP - -## API styles - -${listOrNone(discovery.apis)} - -## API-related files - -${listOrNone(discovery.apiFiles)} - -## OpenAPI summaries - -${openApiSection} -`; -} - -function buildDependencyGraph(discovery: DiscoveryResult): string { - const sections = - discovery.dependencies.length > 0 - ? discovery.dependencies - .map( - (manifest) => `## ${manifest.path} - -- Ecosystem: ${manifest.ecosystem} -- Dependencies tracked: ${manifest.dependencies.length} -${listOrNone(manifest.dependencies.slice(0, 20))}` - ) - .join("\n\n") - : "No dependency manifests were parsed."; - - return `# DEPENDENCY_GRAPH - -${sections} -`; -} - -function buildStackProfile(discovery: DiscoveryResult): string { - return `# STACK_PROFILE - -## Languages - -${listOrNone(discovery.languages)} - -## Frameworks - -${listOrNone(discovery.frameworks)} - -## APIs - -${listOrNone(discovery.apis)} - -## Infrastructure - -${listOrNone(discovery.infrastructure)} - -## Testing - -${listOrNone(discovery.testing)} - -## Cross-cutting integrations - -- CI/CD: ${discovery.ci.providers.join(", ") || "Not detected"} -- Structured logging: ${discovery.logging.structured ? "Yes" : "No"} -- Metrics: ${discovery.metrics.tools.join(", ") || "Not detected"} -- Alerts: ${discovery.metrics.alertsConfigured ? "Detected" : "Not detected"} -`; -} - -function buildArchitectureSnapshot(discovery: DiscoveryResult): string { - return `# ARCHITECTURE - -## Current snapshot - -- Repository: ${discovery.repoName} -- Project type: ${inferProjectType(discovery)} -- Languages: ${discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${discovery.frameworks.join(", ") || "Unknown"} -- API styles: ${discovery.apis.join(", ") || "Not detected"} -- Infrastructure: ${discovery.infrastructure.join(", ") || "Not detected"} -- CI/CD: ${discovery.ci.providers.join(", ") || "Not detected"} -- Observability: logging=${discovery.logging.frameworks.join(", ") || "none"}, metrics=${discovery.metrics.tools.join(", ") || "none"} -`; -} - -function buildStyleGuide(discovery: DiscoveryResult): string { - const guidance: string[] = []; - - if (discovery.languages.includes("TypeScript")) { - guidance.push("Prefer strict typing, small modules, and explicit boundary contracts."); - } - if (discovery.languages.includes("Python")) { - guidance.push("Keep modules import-safe, typed where practical, and formatter-friendly."); - } - if (discovery.languages.includes("Go")) { - guidance.push("Favor small interfaces, package ownership, and context-aware I/O."); - } - if (discovery.languages.includes("Java")) { - guidance.push("Keep service boundaries explicit and configuration discoverable."); - } - - if (guidance.length === 0) { - guidance.push("Preserve a modular structure, readable naming, and explicit operational contracts."); - } - - return `# STYLE_GUIDE - -${listOrNone(guidance)} -`; -} - -async function createIfMissing(filePath: string, content: string): Promise { - if (!(await fileExists(filePath))) { - await writeFileEnsured(filePath, content); - } -} - -export async function initializeProjectMemory( - outputPath: string, - discovery: DiscoveryResult -): Promise<{ - memoryDir: string; - reportsDir: string; - docsDir: string; - runtimeMemoryDir: string; - learningDir: string; - taskBoardDir: string; - proposalDir: string; - patchProposalDir: string; -}> { - const memoryDir = path.join(outputPath, "AI_CONTEXT"); - const runtimeMemoryDir = path.join(outputPath, "memory"); - const learningDir = path.join(runtimeMemoryDir, "learnings"); - const reportsDir = path.join(outputPath, "reports"); - const docsDir = path.join(outputPath, "docs"); - const taskBoardDir = path.join(outputPath, "tasks"); - const proposalDir = path.join(docsDir, "proposals"); - const patchProposalDir = path.join(outputPath, "patch_proposals"); - - await ensureDir(memoryDir); - await ensureDir(runtimeMemoryDir); - await ensureDir(learningDir); - await ensureDir(reportsDir); - await ensureDir(docsDir); - await fs.rm(proposalDir, { recursive: true, force: true }); - await fs.rm(patchProposalDir, { recursive: true, force: true }); - await ensureDir(taskBoardDir); - await ensureDir(proposalDir); - await ensureDir(patchProposalDir); - - logger.info("Initialized filesystem memory directories", { - component: "memory", - action: "memory_write", - repoName: discovery.repoName, - outputPath, - reportsDir, - proposalDir, - patchProposalDir - }); - - await createIfMissing(path.join(memoryDir, "AGENTS.md"), DEFAULT_AGENT_ROSTER); - await createIfMissing(path.join(memoryDir, "RULES.md"), DEFAULT_RULES); - await createIfMissing(path.join(memoryDir, "ERRORS.md"), "# ERRORS\n"); - await createIfMissing(path.join(memoryDir, "DECISIONS.md"), "# DECISIONS\n\n- Adopt non-destructive analysis as the operating mode.\n"); - await createIfMissing(path.join(memoryDir, "TASKS.md"), "# TASKS\n"); - await createIfMissing(path.join(memoryDir, "LEARNINGS.md"), "# LEARNINGS\n"); - await createIfMissing(path.join(memoryDir, "ANNOTATIONS.md"), "# ANNOTATIONS\n\n- None recorded.\n"); - await createIfMissing(path.join(memoryDir, "CONTEXT.md"), "# CONTEXT\n"); - await createIfMissing(path.join(memoryDir, "ARCHITECTURE.md"), buildArchitectureSnapshot(discovery)); - await createIfMissing(path.join(memoryDir, "STYLE_GUIDE.md"), buildStyleGuide(discovery)); - - return { - memoryDir, - reportsDir, - docsDir, - runtimeMemoryDir, - learningDir, - taskBoardDir, - proposalDir, - patchProposalDir - }; -} - -export async function writeDiscoveryArtifacts( - memoryDir: string, - discovery: DiscoveryResult, - openApiSummaries: Array<{ path: string; title?: string; version?: string }> -): Promise { - await writeFileEnsured(path.join(memoryDir, "PROJECT_MODEL.md"), buildProjectModel(discovery)); - await writeFileEnsured(path.join(memoryDir, "CONTEXT.md"), buildContextSnapshot(discovery)); - await writeFileEnsured(path.join(memoryDir, "ARCHITECTURE_MAP.md"), buildArchitectureMap(discovery)); - await writeFileEnsured(path.join(memoryDir, "API_MAP.md"), buildApiMap(discovery, openApiSummaries)); - await writeFileEnsured(path.join(memoryDir, "DEPENDENCY_GRAPH.md"), buildDependencyGraph(discovery)); - await writeFileEnsured(path.join(memoryDir, "STACK_PROFILE.md"), buildStackProfile(discovery)); - await writeIfSkeletal( - path.join(memoryDir, "LEARNINGS.md"), - `# LEARNINGS - -- Discovery completed for ${discovery.repoName}; use MEMORY_BRIEF before broad source reading. -` - ); - await writeIfSkeletal( - path.join(memoryDir, "ERRORS.md"), - `# ERRORS - -- No corrections recorded yet. -` - ); - logger.info("Wrote discovery artifacts", { - component: "memory", - action: "memory_write", - repoName: discovery.repoName, - memoryDir - }); -} - -export async function updatePersistentMemory( - context: ProjectContext, - agentReports: AgentReport[] -): Promise { - const architecturePath = path.join(context.memoryDir, "ARCHITECTURE.md"); - const contextPath = path.join(context.memoryDir, "CONTEXT.md"); - const tasksPath = path.join(context.memoryDir, "TASKS.md"); - - await writeFileEnsured(architecturePath, buildArchitectureSnapshot(context.discovery)); - - const contextEntry = ` -## ${context.scannedAt} - -- Repo: ${context.repoName} -- Languages: ${context.discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${context.discovery.frameworks.join(", ") || "Unknown"} -- APIs: ${context.discovery.apis.join(", ") || "Not detected"} -- Highest risk: ${ - agentReports.find((report) => report.riskLevel === "high") - ? "high" - : agentReports.find((report) => report.riskLevel === "medium") - ? "medium" - : "low" - } -`; - await appendFileEnsured(contextPath, `${contextEntry}\n`); - - const generatedTasks = uniqueSorted( - agentReports.flatMap((report) => report.recommendations).filter(Boolean) - ); - - if (generatedTasks.length > 0) { - await appendFileEnsured( - tasksPath, - `\n## Generated backlog ${context.scannedAt}\n\n${generatedTasks.map((task) => `- ${task}`).join("\n")}\n` - ); - } - - logger.info("Updated persistent AI context", { - component: "memory", - action: "memory_write", - repoName: context.repoName, - generatedTasks: generatedTasks.length - }); -} - -export async function readExistingTasks(memoryDir: string): Promise { - return readTextSafe(path.join(memoryDir, "TASKS.md")); -} diff --git a/memory/executive_summary/index.ts b/memory/executive_summary/index.ts deleted file mode 100644 index 2c85aa4..0000000 --- a/memory/executive_summary/index.ts +++ /dev/null @@ -1,212 +0,0 @@ -import path from "node:path"; - -import { listScopeMemoryRecords } from "../scope_store"; -import { readJsonSafe, readTextSafe, uniqueSorted, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ExecutiveSummaryResult, ProjectContext, ScopeMemoryRecord } from "../../shared/types"; - -interface SwarmMemoryShape { - intent?: string; - synthesis?: { - headline?: string; - summary?: string; - verifiedFacts?: string[]; - verified_facts?: string[]; - unknowns?: string[]; - evidenceRefs?: string[]; - evidence_refs?: string[]; - priorities?: string[]; - nextSteps?: string[]; - next_steps?: string[]; - }; -} - -function inferProjectType(context: ProjectContext): string { - const frameworks = new Set(context.discovery.frameworks); - if (frameworks.has("NestJS") && frameworks.has("NextJS")) { - return "Full-stack platform"; - } - if (frameworks.has("FastAPI") || frameworks.has("Express") || frameworks.has("Spring")) { - return "Backend API service"; - } - if (frameworks.has("NextJS") || frameworks.has("React")) { - return "Frontend application"; - } - if (context.discovery.infrastructure.length > 0 && context.discovery.structure.sourceFileCount < 10) { - return "Infrastructure-oriented project"; - } - return "Software project"; -} - -function compactBullets(input: string, limit: number): string[] { - return input - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => /^[-*]\s+/.test(line) && !/none recorded|none detected/i.test(line)) - .map((line) => line.replace(/^[-*]\s+/, "").trim()) - .filter(Boolean) - .slice(-limit); -} - -function normalizeList(items: Array, limit: number): string[] { - return uniqueSorted(items.filter((item): item is string => Boolean(item?.trim())).map((item) => item.trim())).slice(0, limit); -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function scopeStatuses(records: ScopeMemoryRecord[]): ExecutiveSummaryResult["scopeStatuses"] { - return records - .map((record) => ({ - scope: record.scope, - freshness: record.freshness.status, - coverage: record.coverage.status, - facts: record.verifiedFacts.length, - unknowns: record.unknowns.length, - evidenceRefs: record.evidenceRefs.length, - hashTruncated: Boolean(record.files.hashTruncated) - })) - .sort((left, right) => left.scope.localeCompare(right.scope)); -} - -function renderScopeTable(scopes: ExecutiveSummaryResult["scopeStatuses"]): string { - if (scopes.length === 0) { - return "- None"; - } - - return [ - "| Scope | Freshness | Coverage | Facts | Unknowns | Evidence | Hash truncated |", - "|---|---|---|---:|---:|---:|---|", - ...scopes.map( - (scope) => - `| ${scope.scope} | ${scope.freshness} | ${scope.coverage} | ${scope.facts} | ${scope.unknowns} | ${scope.evidenceRefs} | ${scope.hashTruncated ? "yes" : "no"} |` - ) - ].join("\n"); -} - -function renderExecutiveSummary(summary: ExecutiveSummaryResult): string { - return `# EXECUTIVE_SUMMARY - -## Identity - -- Repository: ${summary.identity.repoName} -- Type: ${summary.identity.projectType} -- Target: ${summary.identity.targetPath} -- Output: ${summary.identity.outputPath} -- Generated: ${summary.generatedAt} - -## What this project is - -- Languages: ${summary.stack.languages.join(", ") || "Unknown"} -- Frameworks: ${summary.stack.frameworks.join(", ") || "Unknown"} -- APIs: ${summary.stack.apis.join(", ") || "Not detected"} -- Infrastructure: ${summary.stack.infrastructure.join(", ") || "Not detected"} -- Testing: ${summary.stack.testing.join(", ") || "Not detected"} - -## Architecture snapshot - -- Source files: ${summary.architecture.sourceFileCount} -- Test files: ${summary.architecture.testFileCount} -- Top-level directories: ${summary.architecture.topLevelDirectories.join(", ") || "None detected"} - -## Current analysis state - -- Scope records: ${summary.status.scopeCount} -- Fresh complete scopes: ${summary.status.completeFreshScopes} -- Stale scopes: ${summary.status.staleScopes} -- Partial scopes: ${summary.status.partialScopes} -- Latest swarm intent: ${summary.status.latestSwarmIntent ?? "None"} -- Latest swarm headline: ${summary.status.latestSwarmHeadline ?? "None"} - -## Decisions - -${renderList(summary.decisions)} - -## Learnings - -${renderList(summary.learnings)} - -## Risks and unknowns - -${renderList(summary.risksAndUnknowns)} - -## Scope status - -${renderScopeTable(summary.scopeStatuses)} - -## Next actions - -${renderList(summary.nextActions)} - -## Evidence refs - -${renderList(summary.evidenceRefs)} -`; -} - -export async function writeExecutiveSummaryArtifacts(context: ProjectContext): Promise { - const scopeRecords = await listScopeMemoryRecords(context); - const scopes = scopeStatuses(scopeRecords); - const swarm = await readJsonSafe(path.join(context.memoryDir, "swarm", "swarm_run.json")); - const decisions = compactBullets(await readTextSafe(path.join(context.memoryDir, "DECISIONS.md")), 10); - const learnings = compactBullets(await readTextSafe(path.join(context.memoryDir, "LEARNINGS.md")), 10); - const errors = compactBullets(await readTextSafe(path.join(context.memoryDir, "ERRORS.md")), 10); - const swarmUnknowns = normalizeList([...(swarm?.synthesis?.unknowns ?? [])], 8); - const swarmNextSteps = normalizeList([...(swarm?.synthesis?.nextSteps ?? []), ...(swarm?.synthesis?.next_steps ?? []), ...(swarm?.synthesis?.priorities ?? [])], 10); - const scopeUnknowns = normalizeList(scopeRecords.flatMap((record) => record.unknowns), 10); - const evidenceRefs = normalizeList( - [ - path.join(context.memoryDir, "MEMORY_BRIEF.md"), - path.join(context.memoryDir, "PROJECT_MODEL.md"), - path.join(context.memoryDir, "STACK_PROFILE.md"), - path.join(context.runtimeMemoryDir, "scopes"), - path.join(context.memoryDir, "swarm", "swarm_run.json"), - ...(swarm?.synthesis?.evidenceRefs ?? []), - ...(swarm?.synthesis?.evidence_refs ?? []), - ...scopeRecords.flatMap((record) => record.evidenceRefs) - ], - 20 - ); - const result: ExecutiveSummaryResult = { - context, - generatedAt: new Date().toISOString(), - reportPath: path.join(context.memoryDir, "EXECUTIVE_SUMMARY.md"), - memoryPath: path.join(context.runtimeMemoryDir, "executive_summary", "executive_summary.json"), - identity: { - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - projectType: inferProjectType(context) - }, - stack: { - languages: context.discovery.languages, - frameworks: context.discovery.frameworks, - apis: context.discovery.apis, - infrastructure: context.discovery.infrastructure, - testing: context.discovery.testing - }, - architecture: { - topLevelDirectories: context.discovery.structure.topLevelDirectories.slice(0, 24), - sourceFileCount: context.discovery.structure.sourceFileCount, - testFileCount: context.discovery.structure.testFileCount - }, - status: { - scopeCount: scopes.length, - completeFreshScopes: scopes.filter((scope) => scope.freshness === "fresh" && scope.coverage === "complete").length, - staleScopes: scopes.filter((scope) => scope.freshness === "stale").length, - partialScopes: scopes.filter((scope) => scope.coverage === "partial").length, - latestSwarmIntent: swarm?.intent, - latestSwarmHeadline: swarm?.synthesis?.headline - }, - decisions, - learnings, - risksAndUnknowns: normalizeList([...errors, ...swarmUnknowns, ...scopeUnknowns], 16), - scopeStatuses: scopes, - nextActions: normalizeList([...swarmNextSteps, ...context.discovery.recommendations], 14), - evidenceRefs - }; - - await writeFileEnsured(result.reportPath, renderExecutiveSummary(result)); - await writeJsonEnsured(result.memoryPath, result); - return result; -} diff --git a/memory/fact_query/index.ts b/memory/fact_query/index.ts deleted file mode 100644 index 962737a..0000000 --- a/memory/fact_query/index.ts +++ /dev/null @@ -1,283 +0,0 @@ -import path from "node:path"; - -import { readJsonSafe, uniqueSorted, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - FactQueryResult, - ProjectContext, - RepositoryFactGraphDocument, - RepositoryFactGraphEdge, - RepositoryFactGraphNode, - ScopeMemoryRecord -} from "../../shared/types"; -import type { MemoryBriefDocument } from "../memory_brief"; -import { listScopeMemoryRecords } from "../scope_store"; - -interface ScoredMatch { - item: T; - score: number; - text: string; -} - -const STOP_WORDS = new Set([ - "the", - "and", - "for", - "with", - "from", - "this", - "that", - "como", - "para", - "con", - "los", - "las", - "una", - "uno", - "que", - "del", - "por", - "sin" -]); - -function tokenize(input: string): string[] { - return uniqueSorted( - input - .toLowerCase() - .split(/[^a-z0-9_.:/-]+/i) - .map((token) => token.trim()) - .filter((token) => token.length >= 2 && !STOP_WORDS.has(token)) - ); -} - -function scoreText(text: string, tokens: string[]): number { - const normalized = text.toLowerCase(); - return tokens.reduce((score, token) => { - if (normalized === token) { - return score + 8; - } - if (normalized.includes(`/${token}`) || normalized.includes(`${token}/`)) { - return score + 5; - } - if (normalized.includes(token)) { - return score + 3; - } - return score; - }, 0); -} - -function topMatches(items: T[], tokens: string[], textFor: (item: T) => string, limit: number): ScoredMatch[] { - return items - .map((item) => { - const text = textFor(item); - return { - item, - text, - score: scoreText(text, tokens) - }; - }) - .filter((match) => match.score > 0) - .sort((left, right) => right.score - left.score || left.text.localeCompare(right.text)) - .slice(0, limit); -} - -function memoryLines(brief: MemoryBriefDocument | undefined): string[] { - if (!brief) { - return []; - } - - return [ - ...brief.decisions.map((entry) => `decision: ${entry}`), - ...brief.learnings.map((entry) => `learning: ${entry}`), - ...brief.corrections.map((entry) => `correction: ${entry}`), - ...brief.annotations.map((entry) => `annotation: ${entry}`), - ...brief.repeatedPatterns.map((entry) => `repeated-pattern: ${entry}`), - ...brief.recentVerifiedFacts.map((entry) => `verified-fact: ${entry}`), - ...brief.recentUnknowns.map((entry) => `unknown: ${entry}`), - ...brief.evidenceRefs.map((entry) => `evidence: ${entry}`), - ...brief.nextBestActions.map((entry) => `next-action: ${entry}`) - ]; -} - -function scopeMemoryLines(record: ScopeMemoryRecord): string[] { - return [ - ...record.decisions.map((entry) => `decision: ${entry}`), - ...record.verifiedFacts.map((entry) => `verified-fact: ${entry}`), - ...record.unknowns.map((entry) => `unknown: ${entry}`), - ...record.evidenceRefs.map((entry) => `evidence: ${entry}`), - ...record.nextActions.map((entry) => `next-action: ${entry}`) - ]; -} - -function nodeText(node: RepositoryFactGraphNode): string { - return `${node.kind} ${node.id} ${node.label} ${JSON.stringify(node.attributes ?? {})}`; -} - -function edgeText(edge: RepositoryFactGraphEdge): string { - return `${edge.kind} ${edge.from} ${edge.to} ${edge.evidencePath ?? ""} ${edge.line ?? ""}`; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderFactQueryReport(result: FactQueryResult): string { - return `# Fact Query - -## Query - -- Text: ${result.query} -- Answer: ${result.answer} -- Tokens: ${result.tokens.join(", ") || "None"} -- Memory brief: ${result.sources.memoryBriefPath} -- Repository fact graph: ${result.sources.repositoryFactGraphPath} -- Scope memory: ${result.sources.scopeMemoryDir ?? "None"} - -## Summary - -- Memory matches: ${result.memoryMatches.length} -- Node matches: ${result.nodeMatches.length} -- Edge matches: ${result.edgeMatches.length} -- Scope memory matches: ${result.scopeMemoryMatches?.length ?? 0} -- Evidence refs: ${result.evidenceRefs.length} - -## Memory Matches - -${renderList(result.memoryMatches.map((match) => `${match.kind}: ${match.text}`))} - -## Node Matches - -${renderList(result.nodeMatches.map((match) => `${match.kind}: ${match.label} (${match.id})`))} - -## Edge Matches - -${renderList(result.edgeMatches.map((match) => `${match.kind}: ${match.from} -> ${match.to}${match.evidencePath ? ` | ${match.evidencePath}` : ""}`))} - -## Scope Memory Matches - -${renderList((result.scopeMemoryMatches ?? []).map((match) => `${match.scope} | ${match.kind}: ${match.text}`))} - -## Evidence Refs - -${renderList(result.evidenceRefs)} - -## Unknowns - -${renderList(result.unknowns)} -`; -} - -function buildAnswer( - memoryMatches: FactQueryResult["memoryMatches"], - scopeMemoryMatches: NonNullable, - nodeMatches: FactQueryResult["nodeMatches"], - edgeMatches: FactQueryResult["edgeMatches"] -): string { - if (memoryMatches.length === 0 && scopeMemoryMatches.length === 0 && nodeMatches.length === 0 && edgeMatches.length === 0) { - return "UNKNOWN"; - } - - const parts = [ - memoryMatches[0] ? `memory=${memoryMatches[0].kind}: ${memoryMatches[0].text}` : undefined, - scopeMemoryMatches[0] - ? `scope-memory=${scopeMemoryMatches[0].scope}/${scopeMemoryMatches[0].kind}: ${scopeMemoryMatches[0].text}` - : undefined, - nodeMatches[0] ? `node=${nodeMatches[0].kind}: ${nodeMatches[0].label}` : undefined, - edgeMatches[0] ? `edge=${edgeMatches[0].kind}: ${edgeMatches[0].from} -> ${edgeMatches[0].to}` : undefined - ].filter((part): part is string => Boolean(part)); - - return `FOUND: ${parts.join(" | ")}`; -} - -export async function collectFactQuery(context: ProjectContext, query: string): Promise { - const tokens = tokenize(query); - const memoryBriefPath = path.join(context.memoryDir, "MEMORY_BRIEF.md"); - const memoryBriefJsonPath = path.join(context.runtimeMemoryDir, "memory_brief", "memory_brief.json"); - const repositoryFactGraphPath = path.join(context.runtimeMemoryDir, "knowledge_graph", "repository_fact_graph.json"); - const scopeMemoryDir = path.join(context.runtimeMemoryDir, "scopes"); - const reportPath = path.join(context.reportsDir, "fact_query.md"); - const memoryPath = path.join(context.memoryDir, "fact_query", "fact_query.json"); - const brief = await readJsonSafe(memoryBriefJsonPath); - const graph = await readJsonSafe(repositoryFactGraphPath); - const scopeRecords = await listScopeMemoryRecords(context); - const freshScopeRecords = scopeRecords.filter((record) => record.freshness.status === "fresh"); - const memoryMatches = topMatches(memoryLines(brief), tokens, (line) => line, 12).map((match) => { - const [kind, ...rest] = match.item.split(": "); - return { - kind: kind || "memory", - text: rest.join(": ") || match.item, - score: match.score - }; - }); - const nodeMatches = topMatches(graph?.nodes ?? [], tokens, nodeText, 16).map((match) => ({ - id: match.item.id, - kind: match.item.kind, - label: match.item.label, - score: match.score, - attributes: match.item.attributes - })); - const edgeMatches = topMatches(graph?.edges ?? [], tokens, edgeText, 16).map((match) => ({ - kind: match.item.kind, - from: match.item.from, - to: match.item.to, - evidencePath: match.item.evidencePath, - line: match.item.line, - score: match.score - })); - const scopeMemoryMatches = freshScopeRecords - .flatMap((record) => - topMatches(scopeMemoryLines(record), tokens, (line) => line, 8).map((match) => { - const [kind, ...rest] = match.item.split(": "); - return { - scope: record.scope, - kind: kind || "scope-memory", - text: rest.join(": ") || match.item, - score: match.score, - evidenceRefs: record.evidenceRefs - }; - }) - ) - .sort((left, right) => right.score - left.score || left.scope.localeCompare(right.scope)) - .slice(0, 12); - const evidenceRefs = uniqueSorted([ - ...memoryMatches.filter((match) => match.kind === "evidence").map((match) => match.text), - ...scopeMemoryMatches.flatMap((match) => match.evidenceRefs), - ...nodeMatches.map((match) => String(match.attributes?.filePath ?? "")).filter(Boolean), - ...edgeMatches.map((match) => match.evidencePath ?? "").filter(Boolean) - ]).slice(0, 20); - const unknowns = [ - ...(brief?.recentUnknowns ?? []), - ...scopeRecords - .filter((record) => record.freshness.status === "stale") - .map((record) => `Scope memory for ${record.scope} is stale; rerun swarm for that scope before treating it as factual.`), - ...(graph ? [] : ["repository_fact_graph.json is missing; run project-brain code-graph first."]), - ...(brief ? [] : ["memory_brief.json is missing; run project-brain status first."]) - ]; - const result: FactQueryResult = { - query, - answer: buildAnswer(memoryMatches, scopeMemoryMatches, nodeMatches, edgeMatches), - tokens, - reportPath, - memoryPath, - sources: { - memoryBriefPath, - memoryBriefJsonPath, - repositoryFactGraphPath, - scopeMemoryDir - }, - scopeMemoryMatches, - memoryMatches, - nodeMatches, - edgeMatches, - evidenceRefs, - unknowns: uniqueSorted(unknowns).slice(0, 12) - }; - - return result; -} - -export async function runFactQuery(context: ProjectContext, query: string): Promise { - const result = await collectFactQuery(context, query); - await writeJsonEnsured(result.memoryPath, result); - await writeFileEnsured(result.reportPath, renderFactQueryReport(result)); - return result; -} diff --git a/memory/knowledge_graph/index.ts b/memory/knowledge_graph/index.ts deleted file mode 100644 index fcc8654..0000000 --- a/memory/knowledge_graph/index.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { promises as fs } from "node:fs"; -import path from "node:path"; - -import { ensureDir, uniqueSorted, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { EcosystemRepositoryResult, RiskLevel } from "../../shared/types"; - -interface PatternRecord { - pattern: string; - repositories: string[]; - count: number; - sources?: string[]; -} - -interface ReusableImprovementProposal { - title: string; - description: string; - repositories: string[]; - riskLevel: RiskLevel; - expectedBenefit: string; - implementationSketch: string; - confidence: number; -} - -export interface KnowledgeGraphDocument { - generatedAt: string; - repositories: Array<{ - repo: string; - relativePath: string; - languages: string[]; - frameworks: string[]; - highestRisk: RiskLevel; - proposalsGenerated: number; - }>; - architecturePatterns: PatternRecord[]; - repeatedBugs: PatternRecord[]; - reusableModules: PatternRecord[]; - performancePatterns: PatternRecord[]; - reusableImprovements: ReusableImprovementProposal[]; -} - -function listOrNone(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function highestRiskFor(result: EcosystemRepositoryResult): RiskLevel { - if (result.result.agentReports.some((report) => report.riskLevel === "high")) { - return "high"; - } - if (result.result.agentReports.some((report) => report.riskLevel === "medium")) { - return "medium"; - } - return "low"; -} - -function normalizePattern(value: string): string { - const lower = value.toLowerCase(); - - if (/no automated test framework|untested|test files|test-to-source ratio|coverage/i.test(value)) { - return "missing automated test baseline"; - } - if (/ci\/cd|ci pipeline|ci baseline|quality gates/i.test(value)) { - return "missing ci baseline"; - } - if (/structured logging|lack of logging|logging/i.test(value)) { - return "limited structured logging"; - } - if (/metrics|tracing|alerts/i.test(value)) { - return "limited runtime telemetry"; - } - if (/openapi|api contract/i.test(value)) { - return "missing api contract"; - } - if (/circular dependenc/i.test(value)) { - return "circular dependency risk"; - } - if (/unused export/i.test(value)) { - return "unused exported symbols"; - } - if (/large files|over 500 lines|oversized/i.test(value)) { - return "oversized modules"; - } - if (/coupling|architectural drift|complexity/i.test(value)) { - return "architectural coupling drift"; - } - if (/performance|latency|cpu|memory|query|dependency bloat|hotspot/i.test(value)) { - return "performance hotspot pattern"; - } - - return lower.replace(/[^a-z0-9\s]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 120); -} - -function architecturePatternFor(result: EcosystemRepositoryResult): string { - const languages = result.result.context.discovery.languages.join("+") || "Unknown"; - const frameworks = result.result.context.discovery.frameworks.join("+") || "NoFramework"; - return `${languages} :: ${frameworks}`; -} - -function collectPatternRecords( - entries: Array<{ pattern: string; repo: string; source?: string }>, - minimumCount = 2 -): PatternRecord[] { - const grouped = new Map; sources: Set }>(); - - for (const entry of entries) { - const normalized = normalizePattern(entry.pattern); - - if (!normalized) { - continue; - } - - const current = grouped.get(normalized) ?? { - repositories: new Set(), - sources: new Set() - }; - current.repositories.add(entry.repo); - if (entry.source) { - current.sources.add(entry.source); - } - grouped.set(normalized, current); - } - - return [...grouped.entries()] - .map(([pattern, value]) => ({ - pattern, - repositories: uniqueSorted([...value.repositories]), - count: value.repositories.size, - sources: value.sources.size > 0 ? uniqueSorted([...value.sources]) : undefined - })) - .filter((record) => record.count >= minimumCount) - .sort((left, right) => right.count - left.count || left.pattern.localeCompare(right.pattern)); -} - -function collectReusableModules(results: EcosystemRepositoryResult[]): PatternRecord[] { - const entries = results.flatMap((result) => - result.result.context.discovery.structure.topLevelDirectories.map((moduleName) => ({ - pattern: moduleName, - repo: result.repoName - })) - ); - - return collectPatternRecords(entries) - .filter((record) => !["src", "tests", "test", "docs", "dist", "build"].includes(record.pattern)) - .slice(0, 10); -} - -function collectPerformancePatterns(results: EcosystemRepositoryResult[]): PatternRecord[] { - return collectPatternRecords( - results.flatMap((result) => - result.result.agentReports - .filter((report) => ["optimization-agent", "dev-agent", "observability-agent"].includes(report.agentId)) - .flatMap((report) => - [...report.findings, ...report.recommendations].map((entry) => ({ - pattern: entry, - repo: result.repoName, - source: report.agentId - })) - ) - ) - ); -} - -function reusableImprovementDescription(pattern: PatternRecord): { - title: string; - description: string; - riskLevel: RiskLevel; - expectedBenefit: string; - implementationSketch: string; - confidence: number; -} { - if (pattern.pattern === "missing automated test baseline") { - return { - title: "Standardize a shared test baseline across repositories", - description: "Multiple repositories are evolving without an automated test foundation. Standardizing a common smoke and unit-test baseline reduces regression risk across the ecosystem.", - riskLevel: "high", - expectedBenefit: "Raises delivery safety before cross-project autonomous proposals scale further.", - implementationSketch: "Create a shared test template, add smoke coverage for CLI and runtime flows, and wire the baseline into CI for each repository.", - confidence: 0.93 - }; - } - if (pattern.pattern === "missing ci baseline") { - return { - title: "Roll out a reusable CI quality gate", - description: "The ecosystem shows repeated CI gaps. A shared workflow template would enforce build, typecheck, tests, and smoke checks consistently.", - riskLevel: "high", - expectedBenefit: "Makes proposal review safer and reduces repository-specific drift in quality gates.", - implementationSketch: "Publish a standard GitHub Actions workflow and adapt only repository-specific install/build/test commands where needed.", - confidence: 0.9 - }; - } - if (pattern.pattern === "limited structured logging" || pattern.pattern === "limited runtime telemetry") { - return { - title: "Standardize observability primitives across repositories", - description: "Runtime diagnostics are inconsistent across multiple repositories. Shared logging and telemetry conventions would improve incident response and cross-project learning.", - riskLevel: "medium", - expectedBenefit: "Improves traceability, debugging, and reusable operational knowledge.", - implementationSketch: "Adopt a common structured logger and lightweight cycle telemetry schema, then add repository-specific adapters only where needed.", - confidence: 0.84 - }; - } - - return { - title: `Reusable improvement for ${pattern.pattern}`, - description: `The pattern "${pattern.pattern}" appears across multiple repositories and should be treated as a reusable improvement opportunity rather than a one-off fix.`, - riskLevel: pattern.pattern.includes("security") ? "high" : "medium", - expectedBenefit: "Reduces duplicated remediation effort across the ecosystem.", - implementationSketch: "Create a shared guideline or reusable package, then roll it out repository by repository with human approval.", - confidence: 0.78 - }; -} - -function buildReusableImprovements( - repeatedBugs: PatternRecord[], - performancePatterns: PatternRecord[] -): ReusableImprovementProposal[] { - const deduped = new Map(); - - for (const pattern of [...repeatedBugs, ...performancePatterns]) { - const template = reusableImprovementDescription(pattern); - const existing = deduped.get(template.title); - - if (!existing) { - deduped.set(template.title, { - ...template, - repositories: pattern.repositories - }); - continue; - } - - deduped.set(template.title, { - ...existing, - repositories: uniqueSorted([...existing.repositories, ...pattern.repositories]), - confidence: Math.max(existing.confidence, template.confidence) - }); - } - - return [...deduped.values()].slice(0, 6); -} - -function proposalFileName(index: number, title: string): string { - const slug = title - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") - .slice(0, 48); - return `proposal_ecosystem_${String(index).padStart(2, "0")}_${slug}.md`; -} - -async function clearPreviousEcosystemProposals(proposalDir: string): Promise { - try { - const files = await fs.readdir(proposalDir); - await Promise.all( - files - .filter((fileName) => fileName.startsWith("proposal_ecosystem_") && fileName.endsWith(".md")) - .map((fileName) => fs.rm(path.join(proposalDir, fileName), { force: true })) - ); - } catch { - // ignore missing directory - } -} - -export async function buildKnowledgeGraphArtifacts( - outputPath: string, - results: EcosystemRepositoryResult[] -): Promise<{ - knowledgeGraphPath: string; - proposalPaths: string[]; - ecosystemReportPath: string; -}> { - const knowledgeGraphDir = path.join(outputPath, "memory", "knowledge_graph"); - const proposalDir = path.join(outputPath, "docs", "proposals"); - const reportPath = path.join(outputPath, "reports", "ecosystem_health.md"); - - await ensureDir(knowledgeGraphDir); - await ensureDir(proposalDir); - await ensureDir(path.dirname(reportPath)); - await clearPreviousEcosystemProposals(proposalDir); - - const architecturePatterns = collectPatternRecords( - results.map((result) => ({ - pattern: architecturePatternFor(result), - repo: result.repoName - })), - 1 - ); - const repeatedBugs = collectPatternRecords( - results.flatMap((result) => - result.result.agentReports.flatMap((report) => - [...report.findings, ...result.result.context.discovery.recommendations].map((entry) => ({ - pattern: entry, - repo: result.repoName, - source: report.agentId - })) - ) - ) - ); - const reusableModules = collectReusableModules(results); - const performancePatterns = collectPerformancePatterns(results); - const reusableImprovements = buildReusableImprovements(repeatedBugs, performancePatterns); - - const knowledgeGraph: KnowledgeGraphDocument = { - generatedAt: new Date().toISOString(), - repositories: results.map((result) => ({ - repo: result.repoName, - relativePath: result.relativePath, - languages: result.result.context.discovery.languages, - frameworks: result.result.context.discovery.frameworks, - highestRisk: highestRiskFor(result), - proposalsGenerated: result.result.governanceSummary?.proposals.length ?? 0 - })), - architecturePatterns, - repeatedBugs, - reusableModules, - performancePatterns, - reusableImprovements - }; - - const knowledgeGraphPath = path.join(knowledgeGraphDir, "knowledge_graph.json"); - await writeJsonEnsured(knowledgeGraphPath, knowledgeGraph); - - const proposalPaths: string[] = []; - for (const [index, proposal] of reusableImprovements.entries()) { - const filePath = path.join(proposalDir, proposalFileName(index + 1, proposal.title)); - const content = `# ${proposal.title} - -## Description - -${proposal.description} - -## Repositories - -${listOrNone(proposal.repositories)} - -## Risk Level - -- ${proposal.riskLevel} - -## Expected Benefit - -${proposal.expectedBenefit} - -## Implementation Sketch - -${proposal.implementationSketch} - -## Confidence - -- ${proposal.confidence} -`; - await writeFileEnsured(filePath, content); - proposalPaths.push(filePath); - } - - const dominantArchitecture = - architecturePatterns.length > 0 && architecturePatterns[0]?.count !== 1 - ? architecturePatterns[0].pattern - : "No dominant pattern detected"; - const architectureDrift = architecturePatterns - .filter((pattern) => dominantArchitecture === "No dominant pattern detected" || pattern.pattern !== dominantArchitecture) - .slice(0, 5) - .map((pattern) => `${pattern.pattern} -> ${pattern.repositories.join(", ")}`); - - const reportContent = `# Ecosystem Health - -## Repository Health - -${results - .map((result) => { - const discovery = result.result.context.discovery; - return `### ${result.repoName} - -- Path: ${result.relativePath} -- Highest risk: ${highestRiskFor(result)} -- Languages: ${discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${discovery.frameworks.join(", ") || "Unknown"} -- Proposals generated: ${result.result.governanceSummary?.proposals.length ?? 0}`; - }) - .join("\n\n")} - -## Repeated Risks - -${listOrNone( - repeatedBugs.slice(0, 10).map((pattern) => `${pattern.pattern} -> ${pattern.repositories.join(", ")}`) -)} - -## Architecture Drift - -- Dominant architecture pattern: ${dominantArchitecture} -${architectureDrift.length > 0 ? architectureDrift.map((entry) => `- Divergent stack: ${entry}`).join("\n") : "- No material architecture drift detected across the analyzed repositories."} - -## Improvement Opportunities - -${listOrNone( - reusableImprovements.map( - (proposal) => `${proposal.title} -> ${proposal.repositories.join(", ")} | confidence=${proposal.confidence}` - ) -)} -`; - - await writeFileEnsured(reportPath, reportContent); - - return { - knowledgeGraphPath, - proposalPaths, - ecosystemReportPath: reportPath - }; -} diff --git a/memory/learning_store/index.ts b/memory/learning_store/index.ts deleted file mode 100644 index 596c4f3..0000000 --- a/memory/learning_store/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -import path from "node:path"; - -import { appendFileEnsured, readTextSafe } from "../../shared/fs-utils"; -import { StructuredLogger } from "../../shared/logger"; -import type { AgentReport, SwarmRunResult } from "../../shared/types"; - -const logger = new StructuredLogger("learning-store"); - -function formatFindings(agentReports: AgentReport[]): string[] { - return agentReports.flatMap((report) => - report.findings.map((finding) => `[${report.title}] ${finding}`) - ); -} - -function formatLearnings(agentReports: AgentReport[]): string[] { - return agentReports.flatMap((report) => - report.recommendations.map((recommendation) => `[${report.title}] ${recommendation}`) - ); -} - -function normalizeMemoryItem(item: string): string { - return item - .replace(/^[-*]\s+/, "") - .replace(/^UNKNOWN:\s*/i, "") - .replace(/\s+/g, " ") - .trim() - .toLowerCase(); -} - -async function appendUniqueMemorySection(filePath: string, timestamp: string, items: string[]): Promise { - const existing = await readTextSafe(filePath); - const existingItems = new Set( - existing - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => /^[-*]\s+/.test(line)) - .map(normalizeMemoryItem) - .filter(Boolean) - ); - const uniqueItems = items - .map((item) => item.trim()) - .filter(Boolean) - .filter((item) => { - const normalized = normalizeMemoryItem(item); - if (!normalized || existingItems.has(normalized)) { - return false; - } - existingItems.add(normalized); - return true; - }); - - if (uniqueItems.length === 0) { - return 0; - } - - await appendFileEnsured( - filePath, - `\n## ${timestamp}\n\n${uniqueItems.map((item) => `- ${item}`).join("\n")}\n` - ); - return uniqueItems.length; -} - -export async function recordLearningArtifacts(memoryDir: string, agentReports: AgentReport[]): Promise { - const errorsPath = path.join(memoryDir, "ERRORS.md"); - const learningsPath = path.join(memoryDir, "LEARNINGS.md"); - const timestamp = new Date().toISOString(); - const findings = formatFindings(agentReports); - const learnings = formatLearnings(agentReports); - - if (findings.length > 0) { - await appendUniqueMemorySection(errorsPath, timestamp, findings); - } - - if (learnings.length > 0) { - await appendUniqueMemorySection(learningsPath, timestamp, learnings); - } - - logger.info("Recorded learning artifacts", { - component: "memory", - action: "memory_write", - findings: findings.length, - learnings: learnings.length, - memoryDir - }); -} - -export async function recordSwarmLearningArtifacts(memoryDir: string, result: SwarmRunResult): Promise { - const timestamp = new Date().toISOString(); - const decisionsPath = path.join(memoryDir, "DECISIONS.md"); - const errorsPath = path.join(memoryDir, "ERRORS.md"); - const learningsPath = path.join(memoryDir, "LEARNINGS.md"); - const synthesis = result.synthesis; - - if (!synthesis) { - logger.info("Skipped swarm learning artifacts because synthesis is missing", { - component: "memory", - action: "memory_write_skipped", - memoryDir - }); - return; - } - - const verifiedFacts = synthesis.verifiedFacts ?? []; - const unknowns = synthesis.unknowns ?? []; - const nextSteps = synthesis.nextSteps ?? []; - const priorities = synthesis.priorities ?? []; - - await appendUniqueMemorySection(decisionsPath, timestamp, [ - `Swarm analyzed intent: ${result.intent}`, - `Synthesis headline: ${synthesis.headline}` - ]); - - if (verifiedFacts.length > 0 || priorities.length > 0 || nextSteps.length > 0) { - await appendUniqueMemorySection(learningsPath, timestamp, [...verifiedFacts, ...priorities, ...nextSteps]); - } - - if (unknowns.length > 0) { - await appendUniqueMemorySection(errorsPath, timestamp, unknowns.map((item) => `UNKNOWN: ${item}`)); - } - - logger.info("Recorded swarm learning artifacts", { - component: "memory", - action: "memory_write", - verifiedFacts: verifiedFacts.length, - unknowns: unknowns.length, - nextSteps: nextSteps.length, - memoryDir - }); -} diff --git a/memory/learnings/index.ts b/memory/learnings/index.ts deleted file mode 100644 index fa03eae..0000000 --- a/memory/learnings/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import path from "node:path"; - -import { readJsonSafe, writeJsonEnsured } from "../../shared/fs-utils"; -import { StructuredLogger } from "../../shared/logger"; -import type { LearningOutcome, LearningRecord } from "../../shared/types"; - -function createLessonId(agentId: string): string { - return `lesson_${agentId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; -} - -export interface LearningFeedbackInput { - agentId: string; - taskId: string; - context: string; - detectedProblem: string; - actionTaken: string; - outcome: LearningOutcome; - confidenceScore: number; -} - -export interface RepeatedLearningPattern { - detectedProblem: string; - count: number; - agentIds: string[]; -} - -export class AgentLearningStore { - private readonly logger = new StructuredLogger("agent-learning-store"); - - async loadAll(learningDir: string): Promise { - return (await readJsonSafe(path.join(learningDir, "index.json"))) ?? []; - } - - async appendBatch(learningDir: string, records: LearningRecord[]): Promise { - const existing = await this.loadAll(learningDir); - const merged = [...existing, ...records]; - const runFileName = `${new Date().toISOString().replace(/[:.]/g, "-")}.json`; - - await writeJsonEnsured(path.join(learningDir, runFileName), records); - await writeJsonEnsured(path.join(learningDir, "index.json"), merged); - this.logger.info("Persisted learning records", { - component: "memory", - action: "memory_write", - learningDir, - records: records.length - }); - } - - createRecord(input: LearningFeedbackInput): LearningRecord { - return { - lessonId: createLessonId(input.agentId), - agentId: input.agentId, - taskId: input.taskId, - context: input.context, - detectedProblem: input.detectedProblem, - actionTaken: input.actionTaken, - outcome: input.outcome, - confidenceScore: input.confidenceScore, - createdAt: new Date().toISOString() - }; - } - - findRepeatedPatterns(records: LearningRecord[]): RepeatedLearningPattern[] { - const patterns = new Map }>(); - - for (const record of records) { - const key = record.detectedProblem.trim().toLowerCase(); - if (!key) { - continue; - } - - if (!patterns.has(key)) { - patterns.set(key, { count: 0, agentIds: new Set() }); - } - - const entry = patterns.get(key); - if (!entry) { - continue; - } - - entry.count += 1; - entry.agentIds.add(record.agentId); - } - - return [...patterns.entries()] - .filter(([, value]) => value.count > 1) - .map(([detectedProblem, value]) => ({ - detectedProblem, - count: value.count, - agentIds: [...value.agentIds].sort((left, right) => left.localeCompare(right)) - })) - .sort((left, right) => right.count - left.count || left.detectedProblem.localeCompare(right.detectedProblem)); - } -} diff --git a/memory/memory_brief/index.ts b/memory/memory_brief/index.ts deleted file mode 100644 index 49d9bfc..0000000 --- a/memory/memory_brief/index.ts +++ /dev/null @@ -1,246 +0,0 @@ -import path from "node:path"; - -import { fileExists, readJsonSafe, readTextSafe, uniqueSorted, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { LearningRecord, ProjectContext } from "../../shared/types"; - -interface SwarmMemoryShape { - intent?: string; - optimization?: { - cacheHits?: number; - cacheMisses?: number; - cacheWrites?: number; - learnedScopeBoosts?: string[]; - }; - synthesis?: { - headline?: string; - summary?: string; - verified_facts?: string[]; - verifiedFacts?: string[]; - unknowns?: string[]; - evidence_refs?: string[]; - evidenceRefs?: string[]; - priorities?: string[]; - next_steps?: string[]; - nextSteps?: string[]; - }; -} - -export interface MemoryBriefDocument { - version: 1; - generatedAt: string; - repoName: string; - targetPath: string; - outputPath: string; - canonicalInputs: string[]; - decisions: string[]; - learnings: string[]; - corrections: string[]; - annotations: string[]; - repeatedPatterns: string[]; - recentVerifiedFacts: string[]; - recentUnknowns: string[]; - evidenceRefs: string[]; - nextBestActions: string[]; - tokenGuidance: string[]; -} - -function compactLines(input: string, limit: number): string[] { - return input - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => /^[-*]\s+/.test(line) && !/none recorded|none detected/i.test(line)) - .map((line) => line.replace(/^[-*]\s+/, "").trim()) - .filter(Boolean) - .slice(-limit); -} - -function normalizeList(items: Array, limit: number): string[] { - return uniqueSorted( - items - .filter((item): item is string => Boolean(item && item.trim())) - .map((item) => item.trim()) - ).slice(0, limit); -} - -function learningPatterns(records: LearningRecord[], limit: number): string[] { - const counts = new Map }>(); - - for (const record of records) { - const key = record.detectedProblem.trim(); - if (!key) { - continue; - } - - const current = counts.get(key) ?? { count: 0, agents: new Set() }; - current.count += 1; - current.agents.add(record.agentId); - counts.set(key, current); - } - - return [...counts.entries()] - .filter(([, value]) => value.count > 1) - .sort((left, right) => right[1].count - left[1].count || left[0].localeCompare(right[0])) - .slice(0, limit) - .map(([problem, value]) => `${problem} (${value.count}x; agents=${[...value.agents].sort().join(", ")})`); -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function discoveryFacts(context: ProjectContext): string[] { - const { discovery } = context; - return normalizeList( - [ - `Repository ${context.repoName} has ${discovery.structure.sourceFileCount} source files and ${discovery.structure.testFileCount} test files.`, - discovery.languages.length > 0 ? `Languages: ${discovery.languages.join(", ")}.` : undefined, - discovery.frameworks.length > 0 ? `Frameworks: ${discovery.frameworks.join(", ")}.` : undefined, - discovery.apis.length > 0 ? `APIs: ${discovery.apis.join(", ")}.` : undefined, - discovery.infrastructure.length > 0 ? `Infrastructure: ${discovery.infrastructure.join(", ")}.` : undefined, - discovery.testing.length > 0 ? `Testing: ${discovery.testing.join(", ")}.` : undefined, - discovery.ci.providers.length > 0 ? `CI/CD: ${discovery.ci.providers.join(", ")}.` : undefined, - discovery.structure.topLevelDirectories.length > 0 - ? `Top-level directories: ${discovery.structure.topLevelDirectories.slice(0, 12).join(", ")}.` - : undefined - ], - 10 - ); -} - -function discoveryActions(context: ProjectContext): string[] { - return normalizeList(context.discovery.recommendations, 6); -} - -async function existingArtifacts(paths: string[]): Promise { - const pairs = await Promise.all(paths.map(async (artifactPath) => ({ artifactPath, exists: await fileExists(artifactPath) }))); - return pairs.filter((pair) => pair.exists).map((pair) => pair.artifactPath); -} - -function renderMemoryBrief(brief: MemoryBriefDocument): string { - return `# MEMORY_BRIEF - -## Identity - -- Repository: ${brief.repoName} -- Target: ${brief.targetPath} -- Output: ${brief.outputPath} -- Generated: ${brief.generatedAt} - -## Canonical Inputs - -${renderList(brief.canonicalInputs)} - -## Decisions - -${renderList(brief.decisions)} - -## Learnings - -${renderList(brief.learnings)} - -## Corrections - -${renderList(brief.corrections)} - -## Annotations - -${renderList(brief.annotations)} - -## Repeated Patterns - -${renderList(brief.repeatedPatterns)} - -## Recent Verified Facts - -${renderList(brief.recentVerifiedFacts)} - -## Recent Unknowns - -${renderList(brief.recentUnknowns)} - -## Evidence Refs - -${renderList(brief.evidenceRefs)} - -## Next Best Actions - -${renderList(brief.nextBestActions)} - -## Token Guidance - -${renderList(brief.tokenGuidance)} -`; -} - -export async function writeMemoryBriefArtifacts(context: ProjectContext): Promise { - const decisions = compactLines(await readTextSafe(path.join(context.memoryDir, "DECISIONS.md")), 8); - const learnings = compactLines(await readTextSafe(path.join(context.memoryDir, "LEARNINGS.md")), 8); - const corrections = compactLines(await readTextSafe(path.join(context.memoryDir, "ERRORS.md")), 8); - const annotations = compactLines(await readTextSafe(path.join(context.memoryDir, "ANNOTATIONS.md")), 8); - const learningRecords = (await readJsonSafe(path.join(context.learningDir, "index.json"))) ?? []; - const swarmMemory = await readJsonSafe(path.join(context.memoryDir, "swarm", "swarm_run.json")); - const synthesis = swarmMemory?.synthesis; - const verifiedFacts = normalizeList([...(synthesis?.verified_facts ?? []), ...(synthesis?.verifiedFacts ?? [])], 10); - const unknowns = normalizeList(synthesis?.unknowns ?? [], 8); - const evidenceRefs = normalizeList([...(synthesis?.evidence_refs ?? []), ...(synthesis?.evidenceRefs ?? [])], 10); - const nextSteps = normalizeList([...(synthesis?.next_steps ?? []), ...(synthesis?.nextSteps ?? []), ...(synthesis?.priorities ?? [])], 8); - const runbookPath = path.join(context.reportsDir, "runbook.md"); - const harnessAuditPath = path.join(context.reportsDir, "harness_audit.md"); - const factQueryPath = path.join(context.reportsDir, "fact_query.md"); - const startPath = path.join(context.reportsDir, "start.md"); - const scopeMemoryPath = path.join(context.runtimeMemoryDir, "scopes"); - const executiveSummaryPath = path.join(context.memoryDir, "EXECUTIVE_SUMMARY.md"); - const executiveSummaryJsonPath = path.join(context.runtimeMemoryDir, "executive_summary", "executive_summary.json"); - const canonicalInputs = [ - path.join(context.memoryDir, "MEMORY_BRIEF.md"), - executiveSummaryPath, - executiveSummaryJsonPath, - path.join(context.memoryDir, "CONTEXT.md"), - path.join(context.memoryDir, "PROJECT_MODEL.md"), - path.join(context.memoryDir, "STACK_PROFILE.md"), - path.join(context.runtimeMemoryDir, "knowledge_graph", "repository_fact_graph.json"), - scopeMemoryPath, - path.join(context.memoryDir, "swarm", "swarm_run.json"), - factQueryPath, - runbookPath, - harnessAuditPath, - startPath, - path.join(context.memoryDir, "DECISIONS.md"), - path.join(context.memoryDir, "LEARNINGS.md"), - path.join(context.memoryDir, "ERRORS.md"), - path.join(context.memoryDir, "ANNOTATIONS.md") - ]; - const existingInputs = await existingArtifacts(canonicalInputs); - - const brief: MemoryBriefDocument = { - version: 1, - generatedAt: new Date().toISOString(), - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - canonicalInputs: existingInputs, - decisions: decisions.length > 0 ? decisions : ["Adopt non-destructive analysis as the operating mode."], - learnings, - corrections, - annotations, - repeatedPatterns: learningPatterns(learningRecords, 8), - recentVerifiedFacts: normalizeList([...discoveryFacts(context), ...verifiedFacts], 14), - recentUnknowns: unknowns, - evidenceRefs: normalizeList([...evidenceRefs, ...existingInputs], 14), - nextBestActions: normalizeList([...nextSteps, ...discoveryActions(context)], 10), - tokenGuidance: [ - "Read MEMORY_BRIEF before broad reports.", - "Use repository_fact_graph.json for structural facts before asking a model.", - "Use memory/scopes/*.json to reuse fresh and complete scoped verified facts before repeating a swarm analysis.", - "Treat stale or partial scope memory as a delta target, not as confirmed current state.", - "Use UNKNOWN instead of guessing missing relationships.", - "Append new corrections, learnings, and decisions instead of duplicating whole reports.", - "Prefer targeted scopes over full-repo swarm runs." - ] - }; - - await writeJsonEnsured(path.join(context.runtimeMemoryDir, "memory_brief", "memory_brief.json"), brief); - await writeFileEnsured(path.join(context.memoryDir, "MEMORY_BRIEF.md"), renderMemoryBrief(brief)); - - return brief; -} diff --git a/memory/preflight_facts/index.ts b/memory/preflight_facts/index.ts deleted file mode 100644 index e9da7cc..0000000 --- a/memory/preflight_facts/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -import path from "node:path"; - -import { collectFactQuery } from "../fact_query"; -import { listScopeMemoryRecords } from "../scope_store"; -import { fileExists, readJsonSafe, uniqueSorted } from "../../shared/fs-utils"; -import type { ExecutiveSummaryResult, PreflightFactsResult, ProjectContext } from "../../shared/types"; - -interface PreflightFactsOptions { - scope?: string; - scopePaths?: string[]; - maxFacts?: number; -} - -function confidenceFor(result: { - memoryMatches: unknown[]; - scopeMemoryMatches?: unknown[]; - nodeMatches: unknown[]; - edgeMatches: unknown[]; - executiveFacts: string[]; -}): PreflightFactsResult["confidence"] { - if ((result.scopeMemoryMatches?.length ?? 0) > 0 || result.memoryMatches.length > 0) { - return "high"; - } - if (result.nodeMatches.length > 0 || result.edgeMatches.length > 0) { - return "medium"; - } - if (result.executiveFacts.length > 0) { - return "low"; - } - return "none"; -} - -function nextActionFor( - confidence: PreflightFactsResult["confidence"], - hasFactGraph: boolean, - staleScopes: string[] -): PreflightFactsResult["recommendedNextAction"] { - if (!hasFactGraph) { - return "run-code-graph"; - } - if (staleScopes.length > 0) { - return "run-swarm-delta"; - } - if (confidence === "high") { - return "answer-from-memory"; - } - if (confidence === "none") { - return "run-fact-query"; - } - return "continue-workflow"; -} - -function executiveFacts(summary: ExecutiveSummaryResult | undefined, query: string): string[] { - if (!summary) { - return []; - } - - const normalized = query.toLowerCase(); - const candidates = [ - `Project type: ${summary.identity.projectType}`, - summary.stack.languages.length > 0 ? `Languages: ${summary.stack.languages.join(", ")}` : undefined, - summary.stack.frameworks.length > 0 ? `Frameworks: ${summary.stack.frameworks.join(", ")}` : undefined, - `Scopes: total=${summary.status.scopeCount}, freshComplete=${summary.status.completeFreshScopes}, stale=${summary.status.staleScopes}`, - summary.status.latestSwarmIntent ? `Latest swarm intent: ${summary.status.latestSwarmIntent}` : undefined, - summary.status.latestSwarmHeadline ? `Latest swarm headline: ${summary.status.latestSwarmHeadline}` : undefined - ].filter((item): item is string => Boolean(item)); - - return candidates.filter((candidate) => - candidate - .toLowerCase() - .split(/[^a-z0-9_.:/-]+/i) - .some((token) => token.length > 2 && normalized.includes(token)) - ); -} - -export async function preflightFacts( - context: ProjectContext, - intent: string, - options: PreflightFactsOptions = {} -): Promise { - const maxFacts = options.maxFacts ?? 12; - const query = intent; - const factQuery = await collectFactQuery(context, query); - const executiveSummaryPath = path.join(context.memoryDir, "EXECUTIVE_SUMMARY.md"); - const executiveSummaryJsonPath = path.join(context.runtimeMemoryDir, "executive_summary", "executive_summary.json"); - const executiveSummary = await readJsonSafe(executiveSummaryJsonPath); - const scopeRecords = await listScopeMemoryRecords(context); - const requestedScopes = options.scopePaths?.length ? new Set(options.scopePaths) : undefined; - const scopedRecords = requestedScopes - ? scopeRecords.filter((record) => requestedScopes.has(record.scope)) - : scopeRecords; - const freshScopes = scopedRecords.filter((record) => record.freshness.status === "fresh").map((record) => record.scope); - const staleScopes = scopedRecords.filter((record) => record.freshness.status === "stale").map((record) => record.scope); - const missingScopes = options.scopePaths?.filter((scope) => !scopeRecords.some((record) => record.scope === scope)) ?? []; - const execFacts = executiveFacts(executiveSummary, query); - const facts = uniqueSorted([ - ...factQuery.memoryMatches.map((match) => `${match.kind}: ${match.text}`), - ...(factQuery.scopeMemoryMatches ?? []).map((match) => `${match.scope}/${match.kind}: ${match.text}`), - ...factQuery.nodeMatches.map((match) => `${match.kind}: ${match.label}`), - ...factQuery.edgeMatches.map((match) => `${match.kind}: ${match.from} -> ${match.to}`), - ...execFacts - ]).slice(0, maxFacts); - const hasFactGraph = await fileExists(factQuery.sources.repositoryFactGraphPath); - const hasMemoryBrief = await fileExists(factQuery.sources.memoryBriefJsonPath); - const hasExecutiveSummary = await fileExists(executiveSummaryJsonPath); - const confidence = confidenceFor({ - memoryMatches: factQuery.memoryMatches, - scopeMemoryMatches: factQuery.scopeMemoryMatches, - nodeMatches: factQuery.nodeMatches, - edgeMatches: factQuery.edgeMatches, - executiveFacts: execFacts - }); - - return { - intent, - scope: options.scope, - query, - factsFound: facts.length > 0, - facts, - evidence: factQuery.evidenceRefs, - freshness: { - freshScopes, - staleScopes, - missingScopes - }, - staleIgnored: factQuery.unknowns.filter((unknown) => /Scope memory .* stale/i.test(unknown)), - confidence, - recommendedNextAction: nextActionFor(confidence, hasFactGraph, staleScopes), - readiness: { - hasMemoryBrief, - hasExecutiveSummary, - hasFactGraph, - hasFreshScopeMemory: freshScopes.length > 0 - }, - sources: { - memoryBriefPath: factQuery.sources.memoryBriefPath, - memoryBriefJsonPath: factQuery.sources.memoryBriefJsonPath, - executiveSummaryPath, - executiveSummaryJsonPath, - repositoryFactGraphPath: factQuery.sources.repositoryFactGraphPath, - scopeMemoryDir: factQuery.sources.scopeMemoryDir ?? path.join(context.runtimeMemoryDir, "scopes") - }, - unknowns: factQuery.unknowns - }; -} diff --git a/memory/readiness/index.ts b/memory/readiness/index.ts deleted file mode 100644 index 96a8daf..0000000 --- a/memory/readiness/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -import path from "node:path"; - -import { fileExists, readJsonSafe } from "../../shared/fs-utils"; -import type { MemoryBriefDocument } from "../memory_brief"; -import type { MemoryReadinessResult, ProjectContext } from "../../shared/types"; - -const DEFAULT_MAX_AGE_HOURS = 72; - -function ageHours(generatedAt: string | undefined): number | undefined { - if (!generatedAt) { - return undefined; - } - const timestamp = Date.parse(generatedAt); - if (!Number.isFinite(timestamp)) { - return undefined; - } - - return Number(((Date.now() - timestamp) / 3_600_000).toFixed(2)); -} - -export async function assessMemoryReadiness( - context: ProjectContext, - options: { maxAgeHours?: number } = {} -): Promise { - const maxAgeHours = options.maxAgeHours ?? DEFAULT_MAX_AGE_HOURS; - const memoryBriefPath = path.join(context.memoryDir, "MEMORY_BRIEF.md"); - const memoryBriefJsonPath = path.join(context.runtimeMemoryDir, "memory_brief", "memory_brief.json"); - const hasMarkdown = await fileExists(memoryBriefPath); - const hasJson = await fileExists(memoryBriefJsonPath); - - if (!hasMarkdown || !hasJson) { - return { - status: "missing", - memoryBriefPath, - memoryBriefJsonPath, - maxAgeHours, - factsCount: 0, - evidenceCount: 0, - tokenGuidanceCount: 0, - reason: "MEMORY_BRIEF markdown or JSON artifact is missing." - }; - } - - const brief = await readJsonSafe(memoryBriefJsonPath); - if (!brief) { - return { - status: "invalid", - memoryBriefPath, - memoryBriefJsonPath, - maxAgeHours, - factsCount: 0, - evidenceCount: 0, - tokenGuidanceCount: 0, - reason: "MEMORY_BRIEF JSON could not be parsed." - }; - } - - const factsCount = brief.recentVerifiedFacts.length; - const evidenceCount = brief.evidenceRefs.length; - const tokenGuidanceCount = brief.tokenGuidance.length; - const currentAgeHours = ageHours(brief.generatedAt); - - if (!brief.repoName || !brief.generatedAt) { - return { - status: "invalid", - memoryBriefPath, - memoryBriefJsonPath, - generatedAt: brief.generatedAt, - ageHours: currentAgeHours, - maxAgeHours, - factsCount, - evidenceCount, - tokenGuidanceCount, - reason: "MEMORY_BRIEF is missing required identity fields." - }; - } - - if (factsCount < 1 || evidenceCount < 1 || tokenGuidanceCount < 1) { - return { - status: "invalid", - memoryBriefPath, - memoryBriefJsonPath, - generatedAt: brief.generatedAt, - ageHours: currentAgeHours, - maxAgeHours, - factsCount, - evidenceCount, - tokenGuidanceCount, - reason: "MEMORY_BRIEF does not meet the minimum factual schema." - }; - } - - if (currentAgeHours !== undefined && currentAgeHours > maxAgeHours) { - return { - status: "stale", - memoryBriefPath, - memoryBriefJsonPath, - generatedAt: brief.generatedAt, - ageHours: currentAgeHours, - maxAgeHours, - factsCount, - evidenceCount, - tokenGuidanceCount, - reason: `MEMORY_BRIEF is older than ${maxAgeHours} hours.` - }; - } - - return { - status: "ready", - memoryBriefPath, - memoryBriefJsonPath, - generatedAt: brief.generatedAt, - ageHours: currentAgeHours, - maxAgeHours, - factsCount, - evidenceCount, - tokenGuidanceCount, - reason: "MEMORY_BRIEF is present, fresh, factual, and evidence-backed." - }; -} diff --git a/memory/scope_store/index.ts b/memory/scope_store/index.ts deleted file mode 100644 index 7563bcc..0000000 --- a/memory/scope_store/index.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { createHash } from "node:crypto"; -import { promises as fs } from "node:fs"; -import path from "node:path"; - -import { ensureDir, readJsonSafe, uniqueSorted, writeJsonEnsured } from "../../shared/fs-utils"; -import type { - ProjectContext, - ScopeMemoryFileHash, - ScopeMemoryLookupResult, - ScopeMemoryRecord, - SwarmRunResult, - SwarmWorkerResult -} from "../../shared/types"; - -const MAX_HASHED_FILES_PER_SCOPE = 80; - -function normalizeScope(scope: string): string { - const normalized = scope.trim().replace(/^\.\/+/, "").replace(/\/+$/, ""); - return normalized || "."; -} - -function scopeKey(scope: string): string { - return Buffer.from(normalizeScope(scope)).toString("base64url"); -} - -function scopeMemoryDir(context: ProjectContext): string { - return path.join(context.runtimeMemoryDir, "scopes"); -} - -export function scopeMemoryPath(context: ProjectContext, scope: string): string { - return path.join(scopeMemoryDir(context), `${scopeKey(scope)}.json`); -} - -function filesForScope(context: ProjectContext, scope: string): string[] { - const normalized = normalizeScope(scope); - return normalized === "." - ? context.discovery.files - : context.discovery.files.filter((file) => file === normalized || file.startsWith(`${normalized}/`)); -} - -function hashedFilesForScope(context: ProjectContext, scope: string): string[] { - return filesForScope(context, scope).slice(0, MAX_HASHED_FILES_PER_SCOPE); -} - -function mergeMemoryItems(previous: string[] | undefined, next: string[], limit: number): string[] { - return uniqueSorted([...(previous ?? []), ...next].map((item) => item.trim()).filter(Boolean)).slice(0, limit); -} - -function scopedWorkerScopes(context: ProjectContext, workerResults: SwarmWorkerResult[]): string[] { - const scopesFromWorkers = workerResults.flatMap((worker) => worker.scopePaths.map(normalizeScope)); - if (scopesFromWorkers.length > 0) { - return scopesFromWorkers; - } - - return context.discovery.structure.topLevelDirectories; -} - -function scopeFileStats(context: ProjectContext, scope: string): { totalCount: number; hashTruncated: boolean } { - const totalCount = filesForScope(context, scope).length; - return { - totalCount, - hashTruncated: totalCount > MAX_HASHED_FILES_PER_SCOPE - }; -} - -function normalizeRecordFreshness(context: ProjectContext, record: ScopeMemoryRecord): Promise { - return hashScopeFiles(context, record.scope).then((currentHashes) => ({ - ...record, - freshness: compareFreshness(record, currentHashes) - })); -} - -async function hashFile(context: ProjectContext, relativePath: string): Promise { - try { - const content = await fs.readFile(path.join(context.targetPath, relativePath)); - return { - path: relativePath, - sha256: createHash("sha256").update(content).digest("hex") - }; - } catch { - return { - path: relativePath, - missing: true - }; - } -} - -async function hashScopeFiles(context: ProjectContext, scope: string): Promise { - return Promise.all(hashedFilesForScope(context, scope).map((file) => hashFile(context, file))); -} - -function compareFreshness( - existing: ScopeMemoryRecord, - currentHashes: ScopeMemoryFileHash[] -): ScopeMemoryRecord["freshness"] { - const previousHashes = existing.files?.hashed ?? []; - const previous = new Map(previousHashes.map((file) => [file.path, file])); - const currentPaths = new Set(currentHashes.map((file) => file.path)); - const changedFiles: string[] = []; - const missingFiles: string[] = []; - let unchangedFiles = 0; - - for (const current of currentHashes) { - const old = previous.get(current.path); - if (current.missing) { - missingFiles.push(current.path); - continue; - } - if (!old?.sha256 || old.sha256 !== current.sha256) { - changedFiles.push(current.path); - continue; - } - unchangedFiles += 1; - } - - for (const previousFile of previousHashes) { - if (!currentPaths.has(previousFile.path)) { - missingFiles.push(previousFile.path); - } - } - - return { - status: changedFiles.length > 0 || missingFiles.length > 0 ? "stale" : "fresh", - changedFiles, - missingFiles, - unchangedFiles - }; -} - -async function readScopeMemory(context: ProjectContext, scope: string): Promise { - return readJsonSafe(scopeMemoryPath(context, scope)); -} - -export async function loadScopeMemoryRecords( - context: ProjectContext, - scopes: string[] -): Promise { - const uniqueScopes = uniqueSorted(scopes.map(normalizeScope)); - const records: ScopeMemoryRecord[] = []; - let hits = 0; - let misses = 0; - let stale = 0; - - for (const scope of uniqueScopes) { - const existing = await readScopeMemory(context, scope); - if (!existing) { - misses += 1; - continue; - } - - const currentHashes = await hashScopeFiles(context, scope); - const freshness = compareFreshness(existing, currentHashes); - const record: ScopeMemoryRecord = { - ...existing, - freshness - }; - - if (freshness.status === "fresh") { - hits += 1; - } else { - stale += 1; - } - - records.push(record); - } - - return { - records, - hits, - misses, - stale - }; -} - -export async function listScopeMemoryRecords(context: ProjectContext): Promise { - try { - const entries = await fs.readdir(scopeMemoryDir(context)); - const records = await Promise.all( - entries - .filter((entry) => entry.endsWith(".json")) - .map((entry) => readJsonSafe(path.join(scopeMemoryDir(context), entry))) - ); - return Promise.all( - records - .filter((record): record is ScopeMemoryRecord => Boolean(record)) - .map((record) => normalizeRecordFreshness(context, record)) - ); - } catch { - return []; - } -} - -function mergeWorkerItems( - workerResults: SwarmWorkerResult[], - scope: string, - selector: (result: SwarmWorkerResult) => string[] | undefined, - limit: number -): string[] { - return uniqueSorted( - workerResults - .filter((result) => result.status === "completed") - .filter((result) => result.scopePaths.map(normalizeScope).includes(normalizeScope(scope))) - .flatMap((result) => selector(result) ?? []) - .map((item) => item.trim()) - .filter(Boolean) - ).slice(0, limit); -} - -function workersForScope(workerResults: SwarmWorkerResult[], scope: string): SwarmWorkerResult[] { - return workerResults.filter((result) => result.scopePaths.map(normalizeScope).includes(normalizeScope(scope))); -} - -function coverageForScope(workerResults: SwarmWorkerResult[], scope: string): ScopeMemoryRecord["coverage"] { - const workers = workersForScope(workerResults, scope); - const completedWorkers = workers.filter((worker) => worker.status === "completed").length; - const failedWorkers = workers.filter((worker) => worker.status === "failed").length; - const timedOutWorkers = workers.filter((worker) => worker.status === "timed_out").length; - const status = - completedWorkers > 0 && failedWorkers === 0 && timedOutWorkers === 0 - ? "complete" - : completedWorkers > 0 - ? "partial" - : timedOutWorkers > 0 - ? "timed_out" - : "failed"; - - return { - status, - workerTaskIds: workers.map((worker) => worker.taskId), - completedWorkers, - failedWorkers, - timedOutWorkers - }; -} - -export async function writeScopeMemoryFromSwarmResult(context: ProjectContext, result: SwarmRunResult): Promise { - const scopes = uniqueSorted(scopedWorkerScopes(context, result.workerResults)); - await ensureDir(scopeMemoryDir(context)); - - let writes = 0; - for (const scope of scopes) { - const previous = await readScopeMemory(context, scope); - const hashes = await hashScopeFiles(context, scope); - const stats = scopeFileStats(context, scope); - const verifiedFacts = mergeWorkerItems(result.workerResults, scope, (worker) => worker.verifiedFacts, 16); - const unknowns = mergeWorkerItems(result.workerResults, scope, (worker) => worker.unknowns, 12); - const evidenceRefs = mergeWorkerItems(result.workerResults, scope, (worker) => worker.evidenceRefs, 16); - const nextActions = mergeWorkerItems(result.workerResults, scope, (worker) => worker.recommendations, 10); - - const record: ScopeMemoryRecord = { - version: 1, - repoName: context.repoName, - targetPath: context.targetPath, - scope, - scopeKey: scopeKey(scope), - updatedAt: new Date().toISOString(), - generatedBy: { - command: "swarm", - intent: result.intent, - provider: result.synthesis.provider, - model: result.synthesis.model - }, - files: { - count: hashes.length, - totalCount: stats.totalCount, - hashTruncated: stats.hashTruncated, - hashed: hashes - }, - coverage: coverageForScope(result.workerResults, scope), - freshness: { - status: "fresh", - changedFiles: [], - missingFiles: hashes.filter((file) => file.missing).map((file) => file.path), - unchangedFiles: hashes.filter((file) => file.sha256).length - }, - decisions: mergeMemoryItems(previous?.decisions, [`Swarm analyzed scope "${scope}" for intent: ${result.intent}`], 12), - verifiedFacts: mergeMemoryItems(previous?.verifiedFacts, verifiedFacts, 24), - unknowns: mergeMemoryItems(previous?.unknowns, unknowns, 18), - evidenceRefs: mergeMemoryItems(previous?.evidenceRefs, evidenceRefs, 24), - nextActions: mergeMemoryItems(previous?.nextActions, nextActions, 16), - sourceArtifacts: mergeMemoryItems(previous?.sourceArtifacts, [result.reportPath, result.memoryPath, ...evidenceRefs], 32) - }; - - await writeJsonEnsured(scopeMemoryPath(context, scope), record); - writes += 1; - } - - return writes; -} - -export function renderScopeMemoryForPrompt(records: ScopeMemoryRecord[]): string { - if (records.length === 0) { - return "Scope memory: None available for this chunk."; - } - - return [ - "Scope memory from previous runs:", - ...records.map((record) => - [ - `- Scope: ${record.scope}`, - ` Freshness: ${record.freshness.status}`, - ` Coverage: ${record.coverage?.status ?? "unknown"}`, - ` Hashed files: ${record.files?.count ?? record.files?.hashed?.length ?? 0}/${record.files?.totalCount ?? record.files?.count ?? "unknown"}${record.files?.hashTruncated ? " (truncated)" : ""}`, - ` Generated by: ${record.generatedBy?.command ?? "unknown"} (${record.generatedBy?.model ?? "unknown model"})`, - ` Verified facts: ${record.verifiedFacts.slice(0, 5).join(" | ") || "None"}`, - ` Unknowns: ${record.unknowns.slice(0, 4).join(" | ") || "None"}`, - ` Evidence: ${record.evidenceRefs.slice(0, 4).join(" | ") || "None"}` - ].join("\n") - ) - ].join("\n"); -} diff --git a/memory/session_log/index.ts b/memory/session_log/index.ts deleted file mode 100644 index bd4eb13..0000000 --- a/memory/session_log/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import path from "node:path"; - -import { readTextSafe, writeFileEnsured } from "../../shared/fs-utils"; -import type { ProjectContext } from "../../shared/types"; - -type ContextFieldValue = string | string[] | undefined; - -function compact(value: string): string { - return value.trim().replace(/\s+/g, " "); -} - -function contextFile(context: ProjectContext, fileName: string): string { - return path.join(context.memoryDir, fileName); -} - -async function appendUniqueLine(filePath: string, text: string): Promise { - const normalized = compact(text); - if (!normalized) { - return; - } - - const current = await readTextSafe(filePath); - const existing = new Set( - current - .split(/\r?\n/) - .map((line) => compact(line.replace(/^[-*]\s+/, ""))) - .filter(Boolean) - ); - if (existing.has(normalized)) { - return; - } - - const prefix = current.trim().length > 0 ? `${current.replace(/\s*$/, "")}\n` : ""; - await writeFileEnsured(filePath, `${prefix}- ${normalized}\n`); -} - -function renderField(value: ContextFieldValue): string { - if (Array.isArray(value)) { - const items = value.map(compact).filter(Boolean); - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- UNKNOWN"; - } - - const text = compact(value ?? ""); - return text.length > 0 ? text : "UNKNOWN"; -} - -function titleFromKey(key: string): string { - return key - .replace(/[_-]+/g, " ") - .replace(/\b\w/g, (match) => match.toUpperCase()); -} - -export async function appendLearning(context: ProjectContext, text: string): Promise { - await appendUniqueLine(contextFile(context, "LEARNINGS.md"), text); -} - -export async function appendError(context: ProjectContext, text: string): Promise { - await appendUniqueLine(contextFile(context, "ERRORS.md"), text); -} - -export async function updateContext(context: ProjectContext, fields: Record): Promise { - const filePath = contextFile(context, "CONTEXT.md"); - const sections = Object.entries(fields) - .filter(([, value]) => value !== undefined) - .map(([key, value]) => `## ${titleFromKey(key)}\n\n${renderField(value)}`) - .join("\n\n"); - - await writeFileEnsured( - filePath, - `# CONTEXT\n\n${sections}\n` - ); -} diff --git a/operations/harness_audit/index.ts b/operations/harness_audit/index.ts deleted file mode 100644 index 7929344..0000000 --- a/operations/harness_audit/index.ts +++ /dev/null @@ -1,263 +0,0 @@ -import path from "node:path"; - -import { assessMemoryReadiness } from "../../memory/readiness"; -import { fileExists, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { HarnessAuditCheck, HarnessAuditMemoryLayer, HarnessAuditResult, ProjectContext } from "../../shared/types"; - -function outputFlag(context: ProjectContext): string { - return `--output "${context.outputPath}"`; -} - -async function existing(paths: string[]): Promise { - const pairs = await Promise.all(paths.map(async (filePath) => ({ filePath, exists: await fileExists(filePath) }))); - return pairs.filter((pair) => pair.exists).map((pair) => pair.filePath); -} - -function layer( - id: string, - label: string, - artifacts: string[], - present: string[], - tokenCost: HarnessAuditMemoryLayer["tokenCost"], - purpose: string -): HarnessAuditMemoryLayer { - const presentCount = artifacts.filter((artifact) => present.includes(artifact)).length; - const status: HarnessAuditMemoryLayer["status"] = - presentCount === artifacts.length ? "ready" : presentCount > 0 ? "partial" : "missing"; - - return { - id, - label, - status, - tokenCost, - artifacts, - purpose - }; -} - -function check( - id: string, - label: string, - status: HarnessAuditCheck["status"], - summary: string, - evidence: string[], - recommendation?: string -): HarnessAuditCheck { - return { - id, - label, - status, - summary, - evidence, - recommendation - }; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderAudit(result: HarnessAuditResult): string { - return `# Harness Audit - -## Summary - -- Repository: ${result.context.repoName} -- Target: ${result.context.targetPath} -- Output: ${result.context.outputPath} -- Generated: ${result.generatedAt} -- Score: ${result.score} -- Token risk: ${result.tokenRisk} -- Memory readiness: ${result.memoryReadiness.status} -- Memory reason: ${result.memoryReadiness.reason} - -## Memory Layers - -${result.memoryLayers - .map( - (layerResult) => `### ${layerResult.label} - -- Status: ${layerResult.status} -- Token cost: ${layerResult.tokenCost} -- Purpose: ${layerResult.purpose} -- Artifacts: -${renderList(layerResult.artifacts)} -` - ) - .join("\n")} - -## Checks - -${result.checks - .map( - (item) => `### ${item.label} - -- Status: ${item.status} -- Summary: ${item.summary} -- Evidence: -${renderList(item.evidence)} -- Recommendation: ${item.recommendation ?? "None"} -` - ) - .join("\n")} - -## Suggested Commands - -${renderList(result.suggestedCommands)} -`; -} - -export async function runHarnessAudit(context: ProjectContext): Promise { - const output = outputFlag(context); - const doctorPath = path.join(context.memoryDir, "doctor", "doctor.json"); - const memoryBriefPath = path.join(context.memoryDir, "MEMORY_BRIEF.md"); - const memoryBriefJsonPath = path.join(context.runtimeMemoryDir, "memory_brief", "memory_brief.json"); - const mapPath = path.join(context.docsDir, "codebase_map", "SUMMARY.md"); - const factGraphPath = path.join(context.runtimeMemoryDir, "knowledge_graph", "repository_fact_graph.json"); - const factQueryPath = path.join(context.reportsDir, "fact_query.md"); - const runbookPath = path.join(context.reportsDir, "runbook.md"); - const firewallPath = path.join(context.reportsDir, "agent_firewall.md"); - const swarmPath = path.join(context.memoryDir, "swarm", "swarm_run.json"); - const planPath = path.join(context.docsDir, "improvement_plan", "SUMMARY.md"); - - const allArtifacts = [ - doctorPath, - memoryBriefPath, - memoryBriefJsonPath, - mapPath, - factGraphPath, - factQueryPath, - runbookPath, - firewallPath, - swarmPath, - planPath - ]; - const present = await existing(allArtifacts); - const has = (filePath: string): boolean => present.includes(filePath); - const memoryReadiness = await assessMemoryReadiness(context); - - const memoryLayers: HarnessAuditMemoryLayer[] = [ - layer( - "compact-index", - "Compact Memory Index", - [memoryBriefPath, memoryBriefJsonPath], - present, - "low", - "Small handoff that should be read before broad analysis." - ), - layer( - "factual-graph", - "Factual Structure Graph", - [mapPath, factGraphPath], - present, - "low", - "Deterministic repository facts and evidence paths." - ), - layer( - "filtered-context", - "Filtered Context Query", - [factQueryPath], - present, - "low", - "Narrow matching facts before loading detailed reports." - ), - layer( - "execution-control", - "Execution Control", - [doctorPath, runbookPath, firewallPath], - present, - "medium", - "Runtime readiness, ordered steps, and policy boundaries." - ), - layer( - "deep-analysis", - "Deep Analysis Memory", - [swarmPath, planPath], - present, - "high", - "Expensive findings and durable direction after cheap context exists." - ) - ]; - - const checks: HarnessAuditCheck[] = [ - check( - "memory-readiness", - "Memory Readiness", - memoryReadiness.status === "ready" ? "pass" : memoryReadiness.status === "stale" ? "warn" : "fail", - memoryReadiness.reason, - [memoryReadiness.memoryBriefPath, memoryReadiness.memoryBriefJsonPath].filter((artifactPath) => present.includes(artifactPath)), - memoryReadiness.status === "ready" ? undefined : `project-brain start "optimize analysis and cost" . ${output}` - ), - check( - "progressive-disclosure", - "Progressive Disclosure", - has(memoryBriefPath) && has(factGraphPath) && has(factQueryPath) ? "pass" : has(memoryBriefPath) || has(factGraphPath) ? "warn" : "fail", - "Memory should be consumed as compact index, factual graph, then detailed artifacts.", - [memoryBriefPath, factGraphPath, factQueryPath].filter(has), - has(factQueryPath) ? undefined : `project-brain fact-query "memory optimization" . ${output}` - ), - check( - "preflight-before-models", - "Preflight Before Models", - has(doctorPath) && has(runbookPath) && has(firewallPath) ? "pass" : has(doctorPath) || has(runbookPath) ? "warn" : "fail", - "Model-heavy runs should have local readiness, an ordered runbook, and governance visibility.", - [doctorPath, runbookPath, firewallPath].filter(has), - !has(doctorPath) - ? `project-brain doctor . ${output}` - : !has(runbookPath) - ? `project-brain runbook "optimize analysis and cost" . ${output}` - : !has(firewallPath) - ? `project-brain firewall . --trigger repository-change ${output}` - : undefined - ), - check( - "cost-gate", - "Cost Gate", - has(swarmPath) ? (has(factGraphPath) && has(factQueryPath) ? "pass" : "warn") : has(factGraphPath) && has(factQueryPath) ? "pass" : "warn", - "Swarm and cloud-capable work should come after deterministic facts and filtered context.", - [factGraphPath, factQueryPath, swarmPath].filter(has), - has(factGraphPath) && has(factQueryPath) ? undefined : `project-brain code-graph . ${output}` - ), - check( - "continuity", - "Continuity", - has(planPath) ? "pass" : has(swarmPath) ? "warn" : "fail", - "Findings should become durable decisions or roadmap items so future runs do not repeat the same analysis.", - [swarmPath, planPath].filter(has), - has(swarmPath) && !has(planPath) ? `project-brain plan-improvements . ${output}` : undefined - ) - ]; - - const score = Number((checks.reduce((sum, item) => sum + (item.status === "pass" ? 1 : item.status === "warn" ? 0.5 : 0), 0) / checks.length).toFixed(2)); - const tokenRisk: HarnessAuditResult["tokenRisk"] = score >= 0.75 ? "low" : score >= 0.5 ? "medium" : "high"; - const suggestedCommands = checks - .map((item) => item.recommendation) - .filter((item): item is string => Boolean(item)); - const result: HarnessAuditResult = { - context, - generatedAt: new Date().toISOString(), - reportPath: path.join(context.reportsDir, "harness_audit.md"), - memoryPath: path.join(context.memoryDir, "harness_audit", "harness_audit.json"), - score, - tokenRisk, - memoryReadiness, - checks, - memoryLayers, - suggestedCommands - }; - - await writeJsonEnsured(result.memoryPath, { - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - generatedAt: result.generatedAt, - score, - tokenRisk, - memoryReadiness, - checks, - memoryLayers, - suggestedCommands - }); - await writeFileEnsured(result.reportPath, renderAudit(result)); - return result; -} diff --git a/orchestrator/chief-agent.ts b/orchestrator/chief-agent.ts deleted file mode 100644 index 835ca13..0000000 --- a/orchestrator/chief-agent.ts +++ /dev/null @@ -1 +0,0 @@ -export { ChiefAgent } from "../core/orchestrator/chief-agent"; diff --git a/orchestrator/main.ts b/orchestrator/main.ts deleted file mode 100644 index 00acf96..0000000 --- a/orchestrator/main.ts +++ /dev/null @@ -1 +0,0 @@ -export { ProjectBrainOrchestrator } from "../core/orchestrator/main"; diff --git a/orchestrator/scheduler.ts b/orchestrator/scheduler.ts deleted file mode 100644 index 9b34e74..0000000 --- a/orchestrator/scheduler.ts +++ /dev/null @@ -1 +0,0 @@ -export { WeeklyScheduler } from "../core/orchestrator/scheduler"; diff --git a/package-lock.json b/package-lock.json index 00743f2..1a7b824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,4194 +1,18 @@ { - "name": "project-brain", - "version": "0.2.4", + "name": "@ruzer/project-brain", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "project-brain", - "version": "0.2.4", - "license": "MIT", - "dependencies": { - "@langchain/ollama": "^1.2.6", - "commander": "^14.0.1", - "deepagents": "^1.8.4", - "langchain": "^1.2.35", - "typescript": "^5.9.3", - "zod": "^4.3.6" - }, - "bin": { - "brain": "dist/cli/project-brain.js", - "project-brain": "dist/cli/project-brain.js" - }, - "devDependencies": { - "@types/node": "^20.19.0", - "@typescript-eslint/parser": "^8.57.0", - "dependency-cruiser": "^17.3.8", - "eslint": "^10.0.3", - "ts-node": "^10.9.2", - "ts-prune": "^0.10.3", - "vitest": "^4.0.18" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cfworker/json-schema": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", - "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "license": "MIT" - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.3", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@langchain/core": { - "version": "1.1.34", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.34.tgz", - "integrity": "sha512-IDlZES5Vexo5meLQRCGkAU7NM0tPGPfPP5wcUzBd7Ot+JoFBmSXutC4gGzvZod5AKRVn3I0Qy5k8vkTraY21jA==", - "license": "MIT", - "dependencies": { - "@cfworker/json-schema": "^4.0.2", - "@standard-schema/spec": "^1.1.0", - "ansi-styles": "^5.0.0", - "camelcase": "6", - "decamelize": "1.2.0", - "js-tiktoken": "^1.0.12", - "langsmith": ">=0.5.0 <1.0.0", - "mustache": "^4.2.0", - "p-queue": "^6.6.2", - "uuid": "^11.1.0", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@langchain/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@langchain/langgraph": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.2.3.tgz", - "integrity": "sha512-wvc7cQ4t6aLmI3PtVvvpN7VTqEmQunrlVnuR6t7z/1l98bj6TnQg8uS+NiJ+gF2TkVC5YXkfqY8Z4EpdD6FlcQ==", - "license": "MIT", - "dependencies": { - "@langchain/langgraph-checkpoint": "^1.0.1", - "@langchain/langgraph-sdk": "~1.7.3", - "@standard-schema/spec": "1.1.0", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": "^1.1.16", - "zod": "^3.25.32 || ^4.2.0", - "zod-to-json-schema": "^3.x" - }, - "peerDependenciesMeta": { - "zod-to-json-schema": { - "optional": true - } - } - }, - "node_modules/@langchain/langgraph-checkpoint": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.1.tgz", - "integrity": "sha512-HM0cJLRpIsSlWBQ/xuDC67l52SqZ62Bh2Y61DX+Xorqwoh5e1KxYvfCD7GnSTbWWhjBOutvnR0vPhu4orFkZfw==", - "license": "MIT", - "dependencies": { - "uuid": "^10.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": "^1.0.1" - } - }, - "node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@langchain/langgraph-sdk": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.7.4.tgz", - "integrity": "sha512-SuQyFvL9Q/eBJdSAHLaM1mmfKoh5JAmRF4PdIokX9pyVYBvJqUpvsOcUYtkC3zniHOh/65y1eqvojt/WgPvN8Q==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.15", - "p-queue": "^9.0.1", - "p-retry": "^7.1.1", - "uuid": "^13.0.0" - }, - "peerDependencies": { - "@angular/core": "^18.0.0 || ^19.0.0 || ^20.0.0", - "@langchain/core": "^1.1.16", - "react": "^18 || ^19", - "react-dom": "^18 || ^19", - "svelte": "^4.0.0 || ^5.0.0", - "vue": "^3.0.0" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - }, - "@langchain/core": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "svelte": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", - "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", - "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/@langchain/langgraph/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@langchain/ollama": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.2.6.tgz", - "integrity": "sha512-wEfjRjyB20SMduqjriIBEalXZf1twbfaNTxxLIjKCVrufHPtKJKGy1a0tQHqa+27HwektNNXlcMre7MTuaS5Rw==", - "license": "MIT", - "dependencies": { - "ollama": "^0.6.3", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.0.0" - } - }, - "node_modules/@langchain/ollama/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@ts-morph/common": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz", - "integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.7", - "minimatch": "^3.0.4", - "mkdirp": "^1.0.4", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@ts-morph/common/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.40.tgz", - "integrity": "sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.0.18", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.18", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-jsx-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", - "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn-loose": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", - "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/code-block-writer": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", - "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepagents": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/deepagents/-/deepagents-1.8.4.tgz", - "integrity": "sha512-qFtburxGIUuPALc0uYlTeDKNZaDR+8rFHYF3TMx7kveTbkNHCzYuTtpeHiWlYXL3dl5xKRJ/ZwLCbXsEkAnttA==", - "license": "MIT", - "dependencies": { - "@langchain/core": "^1.1.33", - "@langchain/langgraph": "^1.1.4", - "fast-glob": "^3.3.3", - "langchain": "1.2.34", - "micromatch": "^4.0.8", - "uuid": "^13.0.0", - "yaml": "^2.8.2", - "zod": "^4.3.6" - } - }, - "node_modules/deepagents/node_modules/langchain": { - "version": "1.2.34", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.34.tgz", - "integrity": "sha512-7Ij3VK3P9phCUgGjV5ckkBqgR7tw+0n54sx4lb1aqZfpbzda9PWgt0hUeTKVEV51VSZ7eBVomYnG6qUQ4IlX2w==", - "license": "MIT", - "dependencies": { - "@langchain/langgraph": "^1.1.2", - "@langchain/langgraph-checkpoint": "^1.0.0", - "langsmith": ">=0.5.0 <1.0.0", - "uuid": "^11.1.0", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.1.33" - } - }, - "node_modules/deepagents/node_modules/langchain/node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/deepagents/node_modules/uuid": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", - "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/dependency-cruiser": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.4.0.tgz", - "integrity": "sha512-+WdFoOb+fT1XNC0iPqOyLpfhLd8xVh7eLXJxPAtiXCS+YmXzGrjqVTte7+L8SZIsnJj0aFhb8LxECIBJY5TTIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "8.16.0", - "acorn-jsx": "5.3.2", - "acorn-jsx-walk": "2.0.0", - "acorn-loose": "8.5.2", - "acorn-walk": "8.3.5", - "commander": "14.0.3", - "enhanced-resolve": "5.21.0", - "ignore": "7.0.5", - "interpret": "3.1.1", - "is-installed-globally": "1.0.0", - "json5": "2.2.3", - "picomatch": "4.0.4", - "prompts": "2.4.2", - "rechoir": "0.8.0", - "safe-regex": "2.1.1", - "semver": "7.7.4", - "tsconfig-paths-webpack-plugin": "4.2.0", - "watskeburt": "5.0.3" - }, - "bin": { - "depcruise": "bin/dependency-cruise.mjs", - "depcruise-baseline": "bin/depcruise-baseline.mjs", - "depcruise-fmt": "bin/depcruise-fmt.mjs", - "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", - "dependency-cruise": "bin/dependency-cruise.mjs", - "dependency-cruiser": "bin/dependency-cruise.mjs" - }, - "engines": { - "node": "^20.12||^22||>=24" - } - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-directory": "^4.0.1", - "is-path-inside": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-network-error": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", - "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tiktoken": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", - "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.5.1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/langchain": { - "version": "1.2.35", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.35.tgz", - "integrity": "sha512-uMK/YZu8i4mCfGarB3EJ/efd7CKidtphw7M5qcgTxWvXMXxHCG8KMLMq70hnJ63GQa1TpgreOzePrK4bvdE2ig==", - "license": "MIT", - "dependencies": { - "@langchain/langgraph": "^1.1.2", - "@langchain/langgraph-checkpoint": "^1.0.0", - "langsmith": ">=0.5.0 <1.0.0", - "uuid": "^11.1.0", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.1.34" - } - }, - "node_modules/langsmith": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.6.3.tgz", - "integrity": "sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA==", - "license": "MIT", - "dependencies": { - "p-queue": "6.6.2" - }, - "peerDependencies": { - "@opentelemetry/api": "*", - "@opentelemetry/exporter-trace-otlp-proto": "*", - "@opentelemetry/sdk-trace-base": "*", - "openai": "*", - "ws": ">=7" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@opentelemetry/exporter-trace-otlp-proto": { - "optional": true - }, - "@opentelemetry/sdk-trace-base": { - "optional": true - }, - "openai": { - "optional": true - }, - "ws": { - "optional": true - } - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "name": "@ruzer/project-brain", + "version": "0.3.0", "license": "MIT", "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/ollama": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", - "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", - "license": "MIT", - "dependencies": { - "whatwg-fetch": "^3.6.20" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", - "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", - "license": "MIT", - "dependencies": { - "is-network-error": "^1.1.0" + "brain": "bin/brain.mjs" }, "engines": { "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "dev": true, - "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "regexp-tree": "~0.1.1" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/true-myth": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz", - "integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "10.* || >= 12.*" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-morph": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz", - "integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ts-morph/common": "~0.12.3", - "code-block-writer": "^11.0.0" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-prune": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/ts-prune/-/ts-prune-0.10.3.tgz", - "integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^6.2.1", - "cosmiconfig": "^7.0.1", - "json5": "^2.1.3", - "lodash": "^4.17.21", - "true-myth": "^4.1.0", - "ts-morph": "^13.0.1" - }, - "bin": { - "ts-prune": "lib/index.js" - } - }, - "node_modules/ts-prune/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", - "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.7.0", - "tapable": "^2.2.1", - "tsconfig-paths": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/watskeburt": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-5.0.3.tgz", - "integrity": "sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA==", - "dev": true, - "license": "MIT", - "bin": { - "watskeburt": "dist/run-cli.js" - }, - "engines": { - "node": "^20.12||^22.13||>=24.0" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" } } } diff --git a/package.json b/package.json index bdc486e..8a864cf 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,43 @@ { - "name": "project-brain", - "version": "0.2.4", - "description": "Autonomous project analysis engine that builds context, memory, and agent-driven reports for any software repository.", - "bin": { - "project-brain": "dist/cli/project-brain.js", - "brain": "dist/cli/project-brain.js" + "name": "@ruzer/project-brain", + "version": "0.3.0", + "description": "Contexto verificable y ligero para repositorios de software.", + "type": "module", + "main": "./src/index.mjs", + "exports": { + ".": "./src/index.mjs", + "./schema": "./schema/context-contract.schema.json" }, - "scripts": { - "lint": "eslint .", - "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit", - "verify:quick": "npm run lint && npm run typecheck && npm run build", - "verify": "npm run verify:quick && npm run test && npm run test:smoke", - "security:repo": "node scripts/check-repo-safety.mjs --all", - "security:audit": "npm audit --omit=dev --audit-level=high", - "review:exports": "node scripts/unused-exports-review.mjs", - "hooks:install": "node scripts/install-hooks.mjs", - "prepare": "node scripts/install-hooks.mjs", - "test": "vitest run", - "test:watch": "vitest", - "test:smoke": "vitest run tests/smoke", - "analyze": "npm run build && node dist/cli/project-brain.js analyze . --output ./sample-output/self-analysis", - "weekly": "npm run build && node dist/cli/project-brain.js weekly . --output ./sample-output/self-analysis", - "report": "npm run build && node dist/cli/project-brain.js report . --output ./sample-output/self-analysis", - "prepack": "npm run build" + "bin": { + "brain": "./bin/brain.mjs" }, - "keywords": [ - "agents", - "autonomous-systems", - "code-analysis", - "context-engineering", - "developer-tools" + "files": [ + "bin", + "schema", + "src", + "templates", + "CHANGELOG.md", + "LICENSE", + "README.md", + "SECURITY.md" ], - "author": "", - "license": "MIT", - "dependencies": { - "@langchain/ollama": "^1.2.6", - "commander": "^14.0.1", - "deepagents": "^1.8.4", - "langchain": "^1.2.35", - "typescript": "^5.9.3", - "zod": "^4.3.6" - }, - "devDependencies": { - "@types/node": "^20.19.0", - "@typescript-eslint/parser": "^8.57.0", - "dependency-cruiser": "^17.3.8", - "eslint": "^10.0.3", - "ts-node": "^10.9.2", - "ts-prune": "^0.10.3", - "vitest": "^4.0.18" + "scripts": { + "test": "node --test", + "check": "npm test && node bin/brain.mjs doctor .", + "prepack": "npm run check" }, "engines": { "node": ">=20" }, - "files": [ - "dist", - "README.md", - "docs", - "prompts", - "reports/templates", - "AI_REVIEW_START_HERE.md", - "schemas", - "CHANGELOG.md" - ] + "repository": { + "type": "git", + "url": "git+https://github.com/ruzer/project-brain.git" + }, + "keywords": [ + "ai-context", + "agents", + "obsidian", + "repository" + ], + "license": "MIT" } diff --git a/planning/architecture_plan/index.ts b/planning/architecture_plan/index.ts deleted file mode 100644 index 7db816c..0000000 --- a/planning/architecture_plan/index.ts +++ /dev/null @@ -1,361 +0,0 @@ -import path from "node:path"; - -import { ensureDir, uniqueSorted, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ProjectContext, ArchitecturePlanResult } from "../../shared/types"; - -interface ArchitectureLayerSignal { - layer: string; - evidence: string[]; -} - -interface ArchitectureState { - generatedAt: string; - repoName: string; - targetPath: string; - outputPath: string; - languages: string[]; - frameworks: string[]; - apis: string[]; - infrastructure: string[]; - testing: string[]; - logging: string[]; - metrics: string[]; - fileCount: number; - sourceFileCount: number; - testFileCount: number; - layers: ArchitectureLayerSignal[]; - risks: string[]; -} - -function cleanLabel(value: string): string { - return value.trim(); -} - -function toListLines(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None found."; -} - -function pickTopEvidence(items: string[], maxItems = 8): string[] { - return items.slice(0, maxItems); -} - -function detectLayers(context: ProjectContext): ArchitectureLayerSignal[] { - const files = context.discovery.files; - const layers: ArchitectureLayerSignal[] = []; - - const frontendEvidence = pickTopEvidence( - files.filter( - (filePath) => - /(^|\/)(app|src)\/(app|pages|components|views|layouts?)\//i.test(filePath) || - /^src\/components\//i.test(filePath) || - /^public\//i.test(filePath) || - /(^|\/)components\//i.test(filePath) || - /(^|\/)views\//i.test(filePath) - ) - ); - if (frontendEvidence.length > 0) { - layers.push({ - layer: "Frontend", - evidence: pickTopEvidence(frontendEvidence) - }); - } - - const backendEvidence = pickTopEvidence( - files.filter( - (filePath) => - /(^|\/)(src|app)\/(api|controllers|routes|middlewares?)\//i.test(filePath) || - /(^|\/)(api|routes|controllers|services)\//i.test(filePath) || - /(^|\/)backend\//i.test(filePath) || - /(^|\/)(default\/)?app\/controllers\//i.test(filePath) - ) - ); - if (backendEvidence.length > 0) { - layers.push({ - layer: "Backend/API", - evidence: pickTopEvidence(backendEvidence) - }); - } - - const dataEvidence = pickTopEvidence( - files.filter( - (filePath) => - /(^|\/)(schemas?|models?|repositories?|entities?|migrations?|scripts?)\//i.test(filePath) || - /\.(sql|prisma|graphql)$/i.test(filePath) || - /(^|\/)db\//i.test(filePath) - ) - ); - if (dataEvidence.length > 0) { - layers.push({ - layer: "Data/Storage", - evidence: pickTopEvidence(dataEvidence) - }); - } - - const infraEvidence = pickTopEvidence( - files.filter( - (filePath) => - /(^|\/)(deploy|infrastructure|ops|ci|.github|docker|kubernetes)\//i.test(filePath) || - /(^|\/)(docker|k8s|helm|terraform|ansible)\./i.test(filePath) - ) - ); - if (infraEvidence.length > 0) { - layers.push({ - layer: "Operations", - evidence: pickTopEvidence(infraEvidence) - }); - } - - const workerEvidence = pickTopEvidence( - files.filter( - (filePath) => - /(^|\/)(jobs?|workers?|cron|tasks?|queues?|scheduler|events?)\//i.test(filePath) || - /(^|\/)(cron|jobs)\//i.test(filePath) - ) - ); - if (workerEvidence.length > 0) { - layers.push({ - layer: "Workers/Jobs", - evidence: pickTopEvidence(workerEvidence) - }); - } - - const cliEvidence = pickTopEvidence( - files.filter((filePath) => /(^|\/)(scripts|bin|cli|tools?)\//i.test(filePath)) - ); - if (cliEvidence.length > 0) { - layers.push({ - layer: "CLI/DevOps Tooling", - evidence: pickTopEvidence(cliEvidence) - }); - } - - const genericEvidence = pickTopEvidence( - files.filter((filePath) => /(^|\/)(config|docs|tests?|spec)\//i.test(filePath)) - ); - if (layers.length === 0 && genericEvidence.length > 0) { - layers.push({ - layer: "Core Application", - evidence: pickTopEvidence(genericEvidence) - }); - } - - return [...layers] - .sort((left, right) => left.layer.localeCompare(right.layer)) - .filter((layer, index, sortedLayers) => - sortedLayers.findIndex((candidate) => candidate.layer === layer.layer) === index - ); -} - -function buildRisks(context: ProjectContext): string[] { - const signals = context.discovery.recommendations; - const infrastructureFlags = signals - .filter((signal) => /op(s|eration|eración)|infra|deploy|ci|cicd|secrets?/i.test(signal)) - .slice(0, 5); - const architectureFlags = signals.filter((signal) => - /\bboundary|module|coupl|drift|architecture|refactor|ownership|governance/i.test(signal) - ); - const risks = uniqueSorted([ - ...infrastructureFlags, - ...architectureFlags, - ...(context.discovery.structure.topLevelDirectories.length > 10 - ? ["Repository has high top-level breadth; module boundaries may be too coupled."] - : []), - ...(context.discovery.structure.subrepos.length > 1 ? ["Nested sub-repos detected; integration boundaries need explicit ownership."] : []), - ...(context.discovery.dependencies.length === 0 ? ["No dependency manifest detected; dependency-based risk visibility is limited."] : []), - ...(!context.discovery.infrastructure.some((value) => /monitor|observ|metric|telemet/i.test(value)) - ? ["No explicit runtime observability system was detected from discovery."] - : []) - ]); - return uniqueSorted(risks).slice(0, 8); -} - -function buildCurrentState(context: ProjectContext, layers: ArchitectureLayerSignal[]): string { - return `# Architecture State (${context.repoName}) - -## Repository Snapshot - -- Repository: ${context.repoName} -- Target: ${context.targetPath} -- Output: ${context.outputPath} -- Languages: ${context.discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${context.discovery.frameworks.join(", ") || "Unknown"} -- API surface: ${context.discovery.apis.join(", ") || "Not explicitly detected"} -- Infrastructure: ${context.discovery.infrastructure.join(", ") || "Not explicitly detected"} -- Testing: ${context.discovery.testing.join(", ") || "Not explicitly detected"} -- Files scanned: ${context.discovery.structure.fileCount} -- Source files: ${context.discovery.structure.sourceFileCount} -- Test files: ${context.discovery.structure.testFileCount} - -## Inferred Layers - -${layers - .map((layer) => `### ${cleanLabel(layer.layer)}\n${toListLines(pickTopEvidence(layer.evidence, 4))}`) - .join("\n\n") || "No strong layer evidence was detected in this repository."} -`; -} - -function buildBlueprint(context: ProjectContext, layers: ArchitectureLayerSignal[]): string { - const risks = buildRisks(context); - const discoverySignals = uniqueSorted([ - ...context.discovery.recommendations, - ...context.discovery.logging.frameworks, - ...context.discovery.metrics.tools, - ...context.discovery.infrastructure - ]); - - return `# ${context.repoName} Architecture Blueprint - -## 1) Real architecture (evidence-backed) - -${buildCurrentState(context, layers)} - -## 2) Boundary and responsibility proposal - -1. Keep existing top-level functional domains as temporary bounded contexts until explicit module boundaries are approved. -2. Route all cross-layer calls through narrow service interfaces and avoid direct DB/framework coupling in UI logic. -3. Define ownership for: API contracts, data models, and operations/tooling before major refactors. - -## 3) Confirmed contracts and assumptions - - ${toListLines(discoverySignals.slice(0, 10))} - -## 4) Key risks detected - -${toListLines(risks)} - -## 5) Evolution plan (30/60/90) - -- **Phase 1 (0-30):** Lock architecture evidence (\`project-brain context-lite\`, \`project-brain architecture-plan\`) and stop adding new high-risk module coupling. -- **Phase 2 (30-60):** Create/validate explicit boundaries for high-touch areas and define API contracts for each boundary. -- **Phase 3 (60-90):** Migrate orchestration to smaller modules and add regression tests before removing legacy seams. - -## 6) Build order (recommended) - -1. Generate evidence artifacts for every critical boundary (\`project-brain context-lite\`, \`project-brain architecture-plan\`). -2. Define module ownership and event/data boundaries in \`docs/architecture_plan/STATE.md\`. -3. Align implementation with contracts in one layer at a time. -4. Add regression validations before moving to next layer. -5. Re-run \`project-brain architecture-plan\` and \`project-brain plan-improvements\` after each major boundary change. - -## 7) Suggested guardrails - -- Do not replace architecture before the evidence snapshot is consistent for three consecutive runs. -- Require review for proposals touching auth, identity, persistence, and deployment files. -- Keep generated temporary state and CLAUDE context synced with this blueprint. -`; -} - -function buildState(context: ProjectContext, layers: ArchitectureLayerSignal[]): string { - const normalizedLayers = layers.map((layer) => layer.layer).join(", ") || "Unknown"; - return `# Architecture Evolution State - -## Baseline - -- Repository: ${context.repoName} -- Last sync: ${new Date().toISOString()} -- Current baseline: ${context.discovery.frameworks.join(", ") || "Unknown"} in ${context.discovery.languages.join(", ") || "Unknown"} -- Top-level layers detected: ${normalizedLayers} -- Source files: ${context.discovery.structure.sourceFileCount} -- CI/infra signals: ${context.discovery.infrastructure.join(", ") || "None"} - -## Snapshot Notes - -- File density: ${context.discovery.structure.fileCount} total files -- Subrepos: ${context.discovery.structure.subrepos.join(", ") || "None"} -- Structure depth: ${context.discovery.structure.topLevelDirectories.length} -- Memory output: ${context.memoryDir} -- Last updated by: project-brain architecture-plan - -## Open architectural questions - -1. Which boundaries are owner-driven (team ownership vs. technical ownership)? -2. Which contracts are stable enough to freeze before refactors? -3. Which subsystems can remain coupled while risk is reduced elsewhere? - -## Next checkpoint - -- Add this file to your change control and require a review when changing cross-layer calls. -`; -} - -function buildClaudeContext(context: ProjectContext, layers: ArchitectureLayerSignal[]): string { - return `# ${context.repoName} - CLAUDE Working Context - -Use this file as execution context for targeted architecture-aligned improvements. - -## Ground truth - -- Do not assume files that are not present in the repository. -- Stack detected: ${context.discovery.languages.join(", ") || "Unknown"} with frameworks ${context.discovery.frameworks.join(", ") || "Unknown"}. -- Top-level responsibilities inferred: ${layers.map((layer) => layer.layer).join(", ") || "Unknown"}. - -## Working rules - -- Keep changes scoped to one boundary at a time. -- Preserve contracts first, then optimize internals. -- Validate each boundary with deterministic artifacts before broad refactors. - -## Fast start - -1. Read \`docs/architecture_plan/BLUEPRINT.md\` -2. Read \`docs/architecture_plan/STATE.md\` -3. Review \`reports\` artifacts before proposing code changes. -4. Update this context only when execution evidence changes materially. - -## Hard constraints - -- Avoid changing persistence, auth, and deployment files in one pass without rollback plan. -- Keep this project brain artifact directory (\`reports\`, \`docs\`, \`memory\`, \`AI_CONTEXT\`) separated from app source. -`; -} - -function summarizeFiles(context: ProjectContext): ArchitectureState { - return { - generatedAt: new Date().toISOString(), - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - languages: context.discovery.languages, - frameworks: context.discovery.frameworks, - apis: context.discovery.apis, - infrastructure: context.discovery.infrastructure, - testing: context.discovery.testing, - logging: context.discovery.logging.frameworks, - metrics: context.discovery.metrics.tools, - fileCount: context.discovery.structure.fileCount, - sourceFileCount: context.discovery.structure.sourceFileCount, - testFileCount: context.discovery.structure.testFileCount, - layers: detectLayers(context), - risks: buildRisks(context) - }; -} - -export async function writeArchitecturePlanArtifacts(context: ProjectContext): Promise { - const planDir = path.join(context.docsDir, "architecture_plan"); - const blueprintPath = path.join(planDir, "BLUEPRINT.md"); - const statePath = path.join(planDir, "STATE.md"); - const claudePath = path.join(planDir, "CLAUDE.md"); - const memoryPath = path.join(context.runtimeMemoryDir, "architecture_plan", "architecture_plan.json"); - const architectureState = summarizeFiles(context); - const layers = architectureState.layers; - - await ensureDir(planDir); - - const blueprint = buildBlueprint(context, layers); - const state = buildState(context, layers); - const claudeContext = buildClaudeContext(context, layers); - - await writeFileEnsured(blueprintPath, blueprint); - await writeFileEnsured(statePath, state); - await writeFileEnsured(claudePath, claudeContext); - await writeJsonEnsured(memoryPath, architectureState); - - return { - context, - planDir, - blueprintPath, - statePath, - claudeContextPath: claudePath, - memoryPath - }; -} diff --git a/planning/improvement_plan/index.ts b/planning/improvement_plan/index.ts deleted file mode 100644 index dcc4323..0000000 --- a/planning/improvement_plan/index.ts +++ /dev/null @@ -1,227 +0,0 @@ -import path from "node:path"; - -import { ensureDir, uniqueSorted, writeFileEnsured } from "../../shared/fs-utils"; -import type { - AgentReport, - ContextAnnotation, - GovernanceSummary, - ImprovementPlanResult, - ProjectContext, - ProposalArtifact -} from "../../shared/types"; - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function rankBucket(proposal: ProposalArtifact): "Now" | "Next" | "Later" { - if ( - proposal.riskLevel === "high" || - proposal.status === "REQUIRES_HUMAN_REVIEW" || - proposal.consensusState === "strong" - ) { - return "Now"; - } - - if (proposal.status === "APPROVED" || proposal.consensusState === "moderate") { - return "Next"; - } - - return "Later"; -} - -function detectTrack(proposal: ProposalArtifact): string { - const text = `${proposal.title} ${proposal.summary} ${proposal.expectedBenefit} ${proposal.consensusThemes.join(" ")}`.toLowerCase(); - - if (/\b(test|coverage|qa|regression|smoke)\b/.test(text)) { - return "Quality"; - } - if (/\b(security|auth|secret|dependency|compliance|permission)\b/.test(text)) { - return "Security"; - } - if (/\b(metric|telemetry|logging|observability|tracing|alert)\b/.test(text)) { - return "Observability"; - } - if (/\b(ux|ui|workflow|usability|operator)\b/.test(text)) { - return "Experience"; - } - if (/\b(architecture|module|boundary|refactor|maintainab|drift)\b/.test(text)) { - return "Architecture"; - } - if (/\b(doc|runbook|readme|guide|onboarding)\b/.test(text)) { - return "Documentation"; - } - - return "Platform"; -} - -function summarizeRisk(report: AgentReport): string[] { - return report.findings.slice(0, 3).map((finding) => `${report.agentId} | ${finding}`); -} - -function summarizeProposal(proposal: ProposalArtifact): string { - return `${proposal.title} | status=${proposal.status} | consensus=${proposal.consensusState} (${proposal.consensusScore.toFixed(2)}) | risk=${proposal.riskLevel}`; -} - -export async function writeImprovementPlanArtifacts( - context: ProjectContext, - agentReports: AgentReport[], - governanceSummary: GovernanceSummary, - annotations: ContextAnnotation[] -): Promise { - const planDir = path.join(context.docsDir, "improvement_plan"); - const summaryPath = path.join(planDir, "SUMMARY.md"); - const statePath = path.join(planDir, "STATE.md"); - const risksPath = path.join(planDir, "KNOWN_RISKS.md"); - const roadmapPath = path.join(planDir, "ROADMAP.md"); - const tracksPath = path.join(planDir, "TRACKS.md"); - - await ensureDir(planDir); - - const proposals = governanceSummary.proposals; - const now = proposals.filter((proposal) => rankBucket(proposal) === "Now"); - const next = proposals.filter((proposal) => rankBucket(proposal) === "Next"); - const later = proposals.filter((proposal) => rankBucket(proposal) === "Later"); - - const groupedTracks = new Map(); - for (const proposal of proposals) { - const track = detectTrack(proposal); - groupedTracks.set(track, [...(groupedTracks.get(track) ?? []), proposal]); - } - - const highRiskReports = agentReports.filter((report) => report.riskLevel === "high"); - const mediumRiskReports = agentReports.filter((report) => report.riskLevel === "medium"); - const annotationLines = annotations.map((annotation) => `[${annotation.scope}] ${annotation.note}`); - const firewall = governanceSummary.firewall; - - await writeFileEnsured( - summaryPath, - `# Improvement Plan Summary - -## Current posture - -- Repository: ${context.repoName} -- Languages: ${context.discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${context.discovery.frameworks.join(", ") || "Unknown"} -- Trigger: ${governanceSummary.trigger} -- Agent reports: ${agentReports.length} -- Proposals: ${proposals.length} -- Firewall review gates: ${firewall?.stats.reviewRequired ?? 0} - -## Top actions now - -${renderList(now.slice(0, 5).map(summarizeProposal))} - -## Suggested next commands - -${renderList([ - 'project-brain ask "dime que le falta criticamente"', - 'project-brain swarm "ayudame a mejorar este repo"', - 'project-brain review-delta .', - 'project-brain firewall . --trigger repository-change' -])} -` - ); - - await writeFileEnsured( - statePath, - `# State - -## Snapshot - -- Repository: ${context.repoName} -- Generated at: ${new Date().toISOString()} -- Languages: ${context.discovery.languages.join(", ") || "Unknown"} -- Frameworks: ${context.discovery.frameworks.join(", ") || "Unknown"} -- APIs: ${context.discovery.apis.join(", ") || "Not detected"} -- Testing: ${context.discovery.testing.join(", ") || "Not detected"} -- Infrastructure: ${context.discovery.infrastructure.join(", ") || "Not detected"} -- CI/CD: ${context.discovery.ci.providers.join(", ") || "Not detected"} -- Structured logging: ${context.discovery.logging.structured ? "Detected" : "Not detected"} -- Metrics: ${context.discovery.metrics.tools.join(", ") || "Not detected"} - -## Governance posture - -- Approved proposals: ${proposals.filter((proposal) => proposal.status === "APPROVED").length} -- Human-review proposals: ${proposals.filter((proposal) => proposal.status === "REQUIRES_HUMAN_REVIEW").length} -- Rejected proposals: ${proposals.filter((proposal) => proposal.status === "REJECTED").length} -- Firewall allowed: ${firewall?.stats.allowed ?? 0} -- Firewall review-required: ${firewall?.stats.reviewRequired ?? 0} -- Firewall blocked: ${firewall?.stats.blocked ?? 0} - -## Local annotations - -${renderList(annotationLines)} -` - ); - - await writeFileEnsured( - risksPath, - `# Known Risks - -## High risk - -${renderList(highRiskReports.flatMap(summarizeRisk))} - -## Medium risk - -${renderList(mediumRiskReports.flatMap(summarizeRisk))} - -## Proposal-level risks - -${renderList( - proposals.map( - (proposal) => - `${proposal.title} | risk=${proposal.riskLevel} | status=${proposal.status} | files=${proposal.affectedFiles.join(", ") || "None"}` - ) - )} -` - ); - - await writeFileEnsured( - roadmapPath, - `# Roadmap - -## Now - -${renderList(now.map((proposal) => `${summarizeProposal(proposal)} | files=${proposal.affectedFiles.join(", ") || "None"}`))} - -## Next - -${renderList(next.map((proposal) => `${summarizeProposal(proposal)} | files=${proposal.affectedFiles.join(", ") || "None"}`))} - -## Later - -${renderList(later.map((proposal) => `${summarizeProposal(proposal)} | files=${proposal.affectedFiles.join(", ") || "None"}`))} -` - ); - - await writeFileEnsured( - tracksPath, - `# Tracks - -${[...groupedTracks.entries()] - .sort(([left], [right]) => left.localeCompare(right)) - .map( - ([track, trackProposals]) => `## ${track} - -- Proposal count: ${trackProposals.length} -- Files: ${uniqueSorted(trackProposals.flatMap((proposal) => proposal.affectedFiles)).join(", ") || "None"} - -${renderList(trackProposals.map((proposal) => `${proposal.title} | agent=${proposal.agentId} | status=${proposal.status}`))} -` - ) - .join("\n")} -` - ); - - return { - context, - planDir, - summaryPath, - statePath, - risksPath, - roadmapPath, - tracksPath - }; -} diff --git a/planning/project_seed/index.ts b/planning/project_seed/index.ts deleted file mode 100644 index 61bb8e2..0000000 --- a/planning/project_seed/index.ts +++ /dev/null @@ -1,550 +0,0 @@ -import path from "node:path"; - -import { ensureDir, fileExists, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ProjectSeedArchetype, ProjectSeedInput, ProjectSeedPriority, ProjectSeedResult } from "../../shared/types"; - -interface ArchetypeDefinition { - label: string; - defaultStack: string; - defaultFeatures: string[]; - defaultEntities: string[]; - defaultIntegrations: string[]; - buildOrder: string[]; -} - -const ARCHETYPES: Record = { - "saas-webapp": { - label: "SaaS / Web App", - defaultStack: "Next.js + PostgreSQL + Prisma + email + object storage", - defaultFeatures: ["workspace onboarding", "dashboard", "billing-ready account model", "admin settings"], - defaultEntities: ["User", "Workspace", "Membership", "Project", "ActivityLog"], - defaultIntegrations: ["email", "object storage", "analytics"], - buildOrder: ["domain model", "auth and roles", "core CRUD flows", "dashboard", "billing boundary", "deployment"] - }, - "marketing-site": { - label: "Marketing Site", - defaultStack: "Astro or Next.js static site + CMS-ready content model", - defaultFeatures: ["landing page", "content sections", "lead capture", "SEO metadata"], - defaultEntities: ["Page", "Lead", "Campaign", "Article"], - defaultIntegrations: ["analytics", "email capture", "CMS"], - buildOrder: ["content model", "visual system", "landing page", "SEO", "forms", "deployment"] - }, - "mobile-app": { - label: "Mobile App", - defaultStack: "Expo React Native + API backend + local persistence", - defaultFeatures: ["onboarding", "authenticated app shell", "offline-ready screens", "push-ready settings"], - defaultEntities: ["User", "Device", "Session", "Notification"], - defaultIntegrations: ["push notifications", "analytics", "auth provider"], - buildOrder: ["navigation", "auth", "core screens", "offline state", "notifications", "store/deployment"] - }, - "api-backend": { - label: "API / Backend", - defaultStack: "Node.js API + PostgreSQL + OpenAPI + Docker", - defaultFeatures: ["REST API", "input validation", "database migrations", "health checks"], - defaultEntities: ["User", "ApiKey", "AuditEvent", "Resource"], - defaultIntegrations: ["database", "observability", "CI"], - buildOrder: ["data model", "API contracts", "validation", "auth", "observability", "deployment"] - }, - "internal-tool": { - label: "Internal Tool", - defaultStack: "Next.js admin dashboard + PostgreSQL + role-based access", - defaultFeatures: ["operator dashboard", "tables and filters", "role-gated actions", "audit trail"], - defaultEntities: ["User", "Role", "Record", "AuditEvent"], - defaultIntegrations: ["SSO", "database", "report export"], - buildOrder: ["roles", "data model", "tables", "forms", "audit logging", "deployment"] - }, - "content-platform": { - label: "Content Platform", - defaultStack: "Next.js + headless CMS + search + static delivery", - defaultFeatures: ["publishing workflow", "content taxonomy", "search", "SEO"], - defaultEntities: ["Author", "Article", "Category", "Tag", "Asset"], - defaultIntegrations: ["CMS", "search", "image storage", "analytics"], - buildOrder: ["content schema", "editor workflow", "public pages", "search", "SEO", "deployment"] - }, - custom: { - label: "Custom", - defaultStack: "To be confirmed from requirements", - defaultFeatures: ["core user journey", "admin or operator workflow", "observability baseline"], - defaultEntities: ["User", "PrimaryRecord", "AuditEvent"], - defaultIntegrations: ["database", "auth", "observability"], - buildOrder: ["requirements", "domain model", "architecture", "core workflow", "testing", "deployment"] - } -}; - -const PRIORITY_LABELS: Record = { - "mvp-fast": "MVP rapido", - "solid-architecture": "Arquitectura solida", - "low-cost": "Costo bajo", - "security-first": "Seguridad alta" -}; - -function cleanList(values: string[]): string[] { - return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; -} - -function listOrDefault(values: string[], defaults: string[]): string[] { - const cleaned = cleanList(values); - return cleaned.length > 0 ? cleaned : defaults; -} - -function renderList(values: string[]): string { - return values.length > 0 ? values.map((value) => `- ${value}`).join("\n") : "- Pending confirmation"; -} - -function renderNumbered(values: string[]): string { - return values.length > 0 ? values.map((value, index) => `${index + 1}. ${value}`).join("\n") : "1. Pending confirmation"; -} - -function buildCodingDiscipline(): string { - return `- State assumptions before changing code. -- Prefer the smallest implementation that satisfies the confirmed goal. -- Do not refactor adjacent code unless the task requires it. -- Every changed line should trace to the current task or accepted decision. -- Define verification before implementation. -- If context is missing or contradictory, stop and ask instead of guessing.`; -} - -function normalizeInput(input: ProjectSeedInput): ProjectSeedInput { - const archetype = ARCHETYPES[input.archetype] ? input.archetype : "custom"; - const definition = ARCHETYPES[archetype]; - return { - ...input, - projectName: input.projectName.trim() || "New Project", - problem: input.problem.trim() || "Pending problem statement.", - audience: input.audience.trim() || "Pending audience definition.", - archetype, - stackPreference: input.stackPreference.trim() || definition.defaultStack, - features: listOrDefault(input.features, definition.defaultFeatures), - roles: input.authRequired ? listOrDefault(input.roles, ["owner", "admin", "member"]) : [], - dataEntities: listOrDefault(input.dataEntities, definition.defaultEntities), - integrations: listOrDefault(input.integrations, definition.defaultIntegrations), - language: input.language.trim() || "es", - notes: cleanList(input.notes), - contextOnly: true - }; -} - -async function writeSeedFile(filePath: string, content: string, overwrite: boolean): Promise { - if (!overwrite && (await fileExists(filePath))) { - throw new Error(`Refusing to overwrite existing project seed artifact: ${filePath}. Re-run with --force to replace it.`); - } - - await writeFileEnsured(filePath, content); -} - -async function writeSeedJson(filePath: string, data: unknown, overwrite: boolean): Promise { - if (!overwrite && (await fileExists(filePath))) { - throw new Error(`Refusing to overwrite existing project seed artifact: ${filePath}. Re-run with --force to replace it.`); - } - - await writeJsonEnsured(filePath, data); -} - -function buildCharter(input: ProjectSeedInput): string { - const definition = ARCHETYPES[input.archetype]; - return `# Project Charter - -## Project - -- Name: ${input.projectName} -- Archetype: ${definition.label} -- Priority: ${PRIORITY_LABELS[input.priority]} -- Language: ${input.language} - -## Problem - -${input.problem} - -## Audience - -${input.audience} - -## Intended Outcome - -Create a project that can be built from explicit context instead of implicit assumptions. This seed is intentionally context-first: implementation should follow the requirements, architecture, decisions, and runbook in \`AI_CONTEXT/\`. - -## Constraints - -- Start with reviewable architecture and backlog before writing application code. -- Keep generated context synchronized when scope changes. -- Treat auth, data storage, payments, and deployment as explicit decisions. - -## Open Notes - -${renderList(input.notes)} -`; -} - -function buildRequirements(input: ProjectSeedInput): string { - return `# Requirements - -## Core Features - -${renderList(input.features)} - -## Users and Roles - -${input.authRequired ? renderList(input.roles) : "- No authentication required for the first version."} - -## Data Entities - -${renderList(input.dataEntities)} - -## Integrations - -${renderList(input.integrations)} - -## Non-Functional Requirements - -- Priority mode: ${PRIORITY_LABELS[input.priority]} -- Testing must cover the first critical user journey. -- Logging must be sufficient to debug onboarding and core workflow failures. -- Secrets must live outside source control. -`; -} - -function buildProjectBlueprint(input: ProjectSeedInput): string { - const definition = ARCHETYPES[input.archetype]; - return `# Project Blueprint - -## 1. Project Overview - -${input.projectName} is a ${definition.label} for ${input.audience}. - -Primary problem: - -${input.problem} - -## 2. Tech Stack - -- Preferred stack: ${input.stackPreference} -- Default archetype stack: ${definition.defaultStack} -- Rationale: choose the smallest stack that supports the core workflow, data model, auth needs, and deployment path. - -## 3. Directory Structure - -\`\`\`text -${input.projectName}/ - AI_CONTEXT/ - docs/ - memory/ - tasks/ - src/ - tests/ -\`\`\` - -## 4. Data Model - -${renderList(input.dataEntities)} - -## 5. API Design - -- Define API contracts after confirming the data model. -- Validate all backend inputs. -- Keep admin/operator actions separate from public user actions. - -## 6. Frontend Architecture - -- Start from the core user journey. -- Keep navigation and role-gated screens explicit. -- Add design system decisions before component expansion. - -## 7. Design System - -- Choose typography, spacing, color, empty states, and error states before building screens. -- For operational tools, prioritize density, scanning, and repeated use. - -## 8. Auth and Authorization - -${input.authRequired ? renderList(input.roles.map((role) => `${role}: permissions pending confirmation`)) : "- Auth is not required for the initial version."} - -## 9. Build Order - -${renderNumbered(definition.buildOrder)} - -## 10. Environment Setup - -- Runtime stack: ${input.stackPreference} -- Required env vars: define after integrations are confirmed. -- Keep \`.env.example\` current when implementation begins. - -## 11. Dependencies - -- Add dependencies only when a requirement needs them. -- Prefer mature libraries for auth, database access, validation, testing, and payments. - -## 12. Deployment - -- Choose deployment after stack confirmation. -- Define preview, staging, and production expectations before launch. - -## 13. Testing - -- Unit tests for core domain rules. -- Integration tests for API/data boundaries. -- Smoke test for the main user journey. - -## 14. Skills to Use - -- Use frontend and UI skills for screen-heavy projects. -- Use security scan workflows before production exposure. -- Use architecture-plan after any major boundary change. - -## 15. Builder Context - -Read \`CLAUDE.md\` and \`AI_CONTEXT/MEMORY_BRIEF.md\` before implementation. - -## 16. Rules - -- Do not invent requirements not captured in this seed. -- Update \`AI_CONTEXT/DECISIONS.md\` when making stack or architecture choices. -- Keep work incremental and verifiable. - -## Coding Discipline - -${buildCodingDiscipline()} -`; -} - -function buildDecisions(input: ProjectSeedInput): string { - return `# Decisions - -## Accepted - -- Project archetype: ${ARCHETYPES[input.archetype].label} -- Priority mode: ${PRIORITY_LABELS[input.priority]} -- Initial stack preference: ${input.stackPreference} -- Context-only generation: yes - -## Pending - -- Final deployment platform. -- Final auth provider and session strategy. -- Database migration strategy. -- Observability and alerting baseline. -- Payment or billing provider, if applicable. -`; -} - -function buildMemoryBrief(input: ProjectSeedInput): string { - return `# Memory Brief - -## Project - -- Name: ${input.projectName} -- Archetype: ${ARCHETYPES[input.archetype].label} -- Audience: ${input.audience} -- Priority: ${PRIORITY_LABELS[input.priority]} - -## Problem - -${input.problem} - -## Stack - -${input.stackPreference} - -## Features - -${renderList(input.features)} - -## Data - -${renderList(input.dataEntities)} - -## Integrations - -${renderList(input.integrations)} - -## Next Best Step - -Review \`AI_CONTEXT/PROJECT_BLUEPRINT.md\`, confirm pending decisions in \`AI_CONTEXT/DECISIONS.md\`, then implement the first build-order item. -`; -} - -function buildRunbook(input: ProjectSeedInput): string { - const definition = ARCHETYPES[input.archetype]; - return `# Project Runbook - -## Immediate Path - -${renderNumbered([ - "Confirm the project charter with the product owner.", - "Validate data entities and role model.", - "Choose final stack and deployment target.", - "Create application scaffold only after decisions are accepted.", - "Implement one core workflow end-to-end.", - "Add smoke tests and security checks before expanding scope." -])} - -## Build Order - -${renderNumbered(definition.buildOrder)} - -## Commands After Code Exists - -\`\`\`bash -project-brain doctor . -project-brain architecture-plan . -project-brain security-audit . -\`\`\` - -## Coding Discipline - -${buildCodingDiscipline()} -`; -} - -function buildArchitectureBlueprint(input: ProjectSeedInput): string { - return `# Architecture Blueprint - -## Baseline - -- Project: ${input.projectName} -- Archetype: ${ARCHETYPES[input.archetype].label} -- Stack preference: ${input.stackPreference} -- Context source: \`memory/project_seed/project_seed.json\` - -## Proposed Boundaries - -- UI / presentation -- Application services -- Domain model -- Persistence -- Integrations -- Operations and deployment - -## Guardrails - -- Keep authorization decisions server-side when auth is enabled. -- Keep integrations behind adapters. -- Do not couple UI components directly to persistence. -- Add observability before exposing production traffic. -`; -} - -function buildArchitectureState(input: ProjectSeedInput): string { - return `# Architecture State - -## Current State - -- Project seed created. -- Application code not generated by project-brain. -- Architecture is pending user confirmation. - -## Open Questions - -- Which stack choice is final? -- Which roles and permissions are required for v1? -- Which entities are required for the first workflow? -- Which integrations are mandatory for MVP? - -## Source - -- \`AI_CONTEXT/PROJECT_CHARTER.md\` -- \`AI_CONTEXT/REQUIREMENTS.md\` -- \`AI_CONTEXT/PROJECT_BLUEPRINT.md\` -`; -} - -function buildBacklog(input: ProjectSeedInput): string { - return `# Initial Backlog - -## Now - -- Confirm charter and requirements. -- Freeze initial stack and deployment target. -- Model entities: ${input.dataEntities.join(", ")} -- Define first user journey. - -## Next - -${renderList(input.features.map((feature) => `Implement ${feature}`))} - -## Later - -- Expand observability. -- Add production security audit. -- Re-run \`project-brain architecture-plan\` after first implementation pass. -`; -} - -function buildClaudeContext(input: ProjectSeedInput): string { - return `# ${input.projectName} - -This project was seeded by project-brain. - -## Required Reading - -1. \`AI_CONTEXT/MEMORY_BRIEF.md\` -2. \`AI_CONTEXT/PROJECT_CHARTER.md\` -3. \`AI_CONTEXT/REQUIREMENTS.md\` -4. \`AI_CONTEXT/PROJECT_BLUEPRINT.md\` -5. \`AI_CONTEXT/DECISIONS.md\` - -## Build Rules - -- Treat \`AI_CONTEXT/\` as the source of truth until code exists. -- Ask before changing project scope, stack, auth, payments, persistence, or deployment assumptions. -- Keep implementation aligned with \`tasks/initial_backlog.md\`. - -## Coding Discipline - -${buildCodingDiscipline()} -`; -} - -export async function writeProjectSeedArtifacts(targetPath: string, input: ProjectSeedInput): Promise { - const normalized = normalizeInput(input); - const overwrite = Boolean(normalized.overwrite); - const aiContextDir = path.join(targetPath, "AI_CONTEXT"); - const docsArchitectureDir = path.join(targetPath, "docs", "architecture_plan"); - const memoryDir = path.join(targetPath, "memory", "project_seed"); - const tasksDir = path.join(targetPath, "tasks"); - - await Promise.all([ensureDir(aiContextDir), ensureDir(docsArchitectureDir), ensureDir(memoryDir), ensureDir(tasksDir)]); - - const artifactPaths = { - projectCharterPath: path.join(aiContextDir, "PROJECT_CHARTER.md"), - requirementsPath: path.join(aiContextDir, "REQUIREMENTS.md"), - blueprintPath: path.join(aiContextDir, "PROJECT_BLUEPRINT.md"), - decisionsPath: path.join(aiContextDir, "DECISIONS.md"), - memoryBriefPath: path.join(aiContextDir, "MEMORY_BRIEF.md"), - runbookPath: path.join(aiContextDir, "RUNBOOK.md"), - architectureBlueprintPath: path.join(docsArchitectureDir, "BLUEPRINT.md"), - architectureStatePath: path.join(docsArchitectureDir, "STATE.md"), - projectSeedMemoryPath: path.join(memoryDir, "project_seed.json"), - backlogPath: path.join(tasksDir, "initial_backlog.md"), - claudePath: path.join(targetPath, "CLAUDE.md") - }; - - await writeSeedFile(artifactPaths.projectCharterPath, buildCharter(normalized), overwrite); - await writeSeedFile(artifactPaths.requirementsPath, buildRequirements(normalized), overwrite); - await writeSeedFile(artifactPaths.blueprintPath, buildProjectBlueprint(normalized), overwrite); - await writeSeedFile(artifactPaths.decisionsPath, buildDecisions(normalized), overwrite); - await writeSeedFile(artifactPaths.memoryBriefPath, buildMemoryBrief(normalized), overwrite); - await writeSeedFile(artifactPaths.runbookPath, buildRunbook(normalized), overwrite); - await writeSeedFile(artifactPaths.architectureBlueprintPath, buildArchitectureBlueprint(normalized), overwrite); - await writeSeedFile(artifactPaths.architectureStatePath, buildArchitectureState(normalized), overwrite); - await writeSeedFile(artifactPaths.backlogPath, buildBacklog(normalized), overwrite); - await writeSeedFile(artifactPaths.claudePath, buildClaudeContext(normalized), overwrite); - await writeSeedJson(artifactPaths.projectSeedMemoryPath, { - generatedAt: new Date().toISOString(), - source: "project-brain project_seed", - referencePattern: "the-architect-style guided blueprint adapted to AI_CONTEXT", - input: normalized - }, overwrite); - - return { - targetPath, - projectName: normalized.projectName, - archetype: normalized.archetype, - contextOnly: normalized.contextOnly, - artifactPaths, - nextSteps: [ - "Review AI_CONTEXT/PROJECT_CHARTER.md and AI_CONTEXT/REQUIREMENTS.md.", - "Confirm pending decisions in AI_CONTEXT/DECISIONS.md.", - "Use tasks/initial_backlog.md to start implementation in small verified steps." - ] - }; -} diff --git a/planning/runbook/index.ts b/planning/runbook/index.ts deleted file mode 100644 index 7e448db..0000000 --- a/planning/runbook/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -import path from "node:path"; - -import { buildWorkflowRuntimeDefinitions, type WorkflowRuntimeDefinition } from "../../core/workflow_registry"; -import { writeExecutiveSummaryArtifacts } from "../../memory/executive_summary"; -import { fileExists, writeFileEnsured, writeJsonEnsured } from "../../shared/fs-utils"; -import type { ProjectContext, RunbookResult, RunbookStep } from "../../shared/types"; - -async function artifactExists(filePath: string): Promise { - return fileExists(filePath); -} - -function stepFromWorkflow( - id: string, - workflow: WorkflowRuntimeDefinition, - status: RunbookStep["status"], -): RunbookStep { - return { - id, - title: workflow.humanLabel, - status, - command: workflow.command, - rationale: workflow.rationale, - cheap: workflow.cheap, - usesModel: workflow.usesModel, - evidence: workflow.artifactPaths - }; -} - -function renderList(items: string[]): string { - return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None"; -} - -function renderRunbook(result: RunbookResult): string { - return `# Runbook - -## Intent - -- Repository: ${result.context.repoName} -- Intent: ${result.intent} -- Generated: ${result.generatedAt} - -## Principle - -Run deterministic memory and graph steps before model-heavy analysis. - -## Executive Summary - -- Markdown: ${result.executiveSummary.reportPath} -- JSON: ${result.executiveSummary.memoryPath} -- Scopes: ${result.executiveSummary.status.scopeCount} -- Fresh complete scopes: ${result.executiveSummary.status.completeFreshScopes} -- Stale scopes: ${result.executiveSummary.status.staleScopes} - -## Steps - -${result.steps - .map( - (item) => `### ${item.id}. ${item.title} - -- Status: ${item.status} -- Cheap: ${item.cheap ? "yes" : "no"} -- Uses model: ${item.usesModel ? "yes" : "no"} -- Command: \`${item.command}\` -- Rationale: ${item.rationale} -- Evidence: -${renderList(item.evidence)} -` - ) - .join("\n")} - -## Next Commands - -${renderList(result.steps.filter((item) => item.status !== "done").slice(0, 4).map((item) => item.command))} -`; -} - -export async function buildRunbook(context: ProjectContext, intent: string): Promise { - const workflows = buildWorkflowRuntimeDefinitions(context, intent).filter((workflow) => - ["doctor", "map-codebase", "code-graph", "fact-query", "harness-audit", "firewall", "swarm", "plan-improvements", "resume"].includes(workflow.workflowId) - ); - const existsByWorkflow = new Map(); - for (const workflow of workflows) { - const exists = (await Promise.all(workflow.artifactPaths.map((artifactPath) => artifactExists(artifactPath)))).some(Boolean); - existsByWorkflow.set(workflow.workflowId, exists); - } - const memoryBriefExists = await artifactExists(path.join(context.memoryDir, "MEMORY_BRIEF.md")); - const statusFor = (workflow: WorkflowRuntimeDefinition): RunbookStep["status"] => { - if (workflow.workflowId === "resume") { - return "ready"; - } - if (existsByWorkflow.get(workflow.workflowId)) { - return "done"; - } - if (workflow.workflowId === "fact-query") { - return existsByWorkflow.get("code-graph") && memoryBriefExists ? "ready" : "blocked"; - } - if (workflow.workflowId === "harness-audit") { - return memoryBriefExists || existsByWorkflow.get("code-graph") ? "ready" : "pending"; - } - if (workflow.workflowId === "swarm") { - return existsByWorkflow.get("code-graph") && existsByWorkflow.get("fact-query") ? "ready" : "blocked"; - } - if (workflow.workflowId === "plan-improvements") { - return existsByWorkflow.get("swarm") ? "ready" : "blocked"; - } - return "pending"; - }; - const steps = workflows.map((workflow, index) => stepFromWorkflow(String(index + 1).padStart(2, "0"), workflow, statusFor(workflow))); - const executiveSummary = await writeExecutiveSummaryArtifacts(context); - const result: RunbookResult = { - context, - intent, - generatedAt: new Date().toISOString(), - reportPath: path.join(context.reportsDir, "runbook.md"), - memoryPath: path.join(context.memoryDir, "runbook", "runbook.json"), - executiveSummary, - steps - }; - - await writeJsonEnsured(result.memoryPath, { - repoName: context.repoName, - targetPath: context.targetPath, - outputPath: context.outputPath, - intent, - generatedAt: result.generatedAt, - executiveSummary: { - reportPath: executiveSummary.reportPath, - memoryPath: executiveSummary.memoryPath, - status: executiveSummary.status - }, - steps - }); - await writeFileEnsured(result.reportPath, renderRunbook(result)); - return result; -} diff --git a/prompts/agent_prompts/README.md b/prompts/agent_prompts/README.md deleted file mode 100644 index 8c2f50c..0000000 --- a/prompts/agent_prompts/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# agent prompts - -This directory is reserved for exported agent prompt assets. - -Runtime system prompts still live under `agents/prompts/` to preserve compatibility with the current implementation. diff --git a/prompts/context_templates/architecture_review.prompt.md b/prompts/context_templates/architecture_review.prompt.md deleted file mode 100644 index b3f96cc..0000000 --- a/prompts/context_templates/architecture_review.prompt.md +++ /dev/null @@ -1,31 +0,0 @@ -# architecture review prompt - -You are a senior software architect reviewing a production repository. - -Goal: -Assess the implemented architecture as it exists today. - -Focus on: - -- module boundaries -- coupling and cohesion -- ownership clarity -- orchestration flow -- safety and governance constraints -- scalability risks -- evolution constraints - -Ignore: - -- aspirational designs not present in the codebase -- documentation claims that are not implemented - -Output: - -- real architecture summary -- critical weaknesses -- missing boundaries -- safe refactor priorities -- migration path with human approval gates - -Do not hallucinate features. diff --git a/prompts/context_templates/context_bootstrap_master.md b/prompts/context_templates/context_bootstrap_master.md deleted file mode 100644 index 6b4a39a..0000000 --- a/prompts/context_templates/context_bootstrap_master.md +++ /dev/null @@ -1,145 +0,0 @@ -# context bootstrap master prompt - -Actua como analista tecnico del repositorio actual. -Inspecciona el proyecto real y crea o actualiza `AI_CONTEXT/` para que otros agentes AI puedan trabajar aqui sin inventar arquitectura, flujos, reglas ni contratos. - -## Objetivo - -Construir un contexto operativo, breve y confiable del proyecto real. - -## Alcance minimo - -Antes de escribir, inspecciona al menos: - -- `README*` -- `package.json`, lockfiles y scripts -- estructura de `src/`, `app/`, `pages/`, `components/`, `lib/`, `api/` -- configuracion relevante (`env`, auth, db, build, CI) -- migraciones, schemas, seeds, contratos, tests y docs existentes -- `docs/`, `app/docs/` y documentos raiz como `API.md`, `ARCHITECTURE.md`, `BUSINESS_RULES.md`, `FLOWS.md` - -## Reglas - -1. No inventes nada. - Si algo no esta confirmado en codigo o docs, marcalo como `Pendiente de confirmar`. - -2. Si hay contradiccion entre docs y codigo: - Prioriza codigo/runtime/schema vigentes y documenta la contradiccion. - -3. Usa rutas reales del repositorio. - -4. Cada hallazgo importante debe incluir evidencia breve: - `Evidencia: ruta[:linea]` - -5. Distingue claramente: - - estado actual confirmado - - pendiente de confirmar - - docs `Draft`, `Spec`, `Legacy`, `Reference` - -6. Si ya existe `AI_CONTEXT/`: - - actualiza solo lo necesario - - no borres notas manuales fuera de bloques generados - - conserva decisiones y learnings manuales si no fueron invalidados por evidencia nueva - -7. No propongas refactors imaginarios. - `TASKS.md` debe salir de evidencia real del repo. - -## Archivos a crear o actualizar - -- `AI_CONTEXT/system_overview.md` -- `AI_CONTEXT/domain_inventory.md` -- `AI_CONTEXT/modules_map.md` -- `AI_CONTEXT/frontend_architecture.md` -- `AI_CONTEXT/backend_flows_and_contracts.md` -- `AI_CONTEXT/ui_rules.md` -- `AI_CONTEXT/DECISIONS.md` -- `AI_CONTEXT/LEARNINGS.md` -- `AI_CONTEXT/TASKS.md` - -## Contenido esperado - -### `system_overview.md` - -- objetivo del sistema -- actores principales -- arquitectura operativa actual -- stack principal -- persistencia y fuente de verdad -- restricciones permanentes -- fuentes canonicias o fuentes de referencia si estan explicitadas - -### `domain_inventory.md` - -- dominios funcionales activos -- docs asociadas -- superficies reales de codigo por dominio -- dominios documentados sin superficie confirmada - -### `modules_map.md` - -- mapa de modulos funcionales -- rutas clave -- panel admin/backoffice si existe -- relacion entre modulos y datos -- activo vs legacy/fallback - -### `frontend_architecture.md` - -- layouts, paginas y componentes principales -- rendering strategy -- estado y data fetching -- i18n si existe -- dependencias UI -- reglas de navegacion y shells visibles - -### `backend_flows_and_contracts.md` - -- endpoints reales -- auth/authz -- validaciones -- acceso a DB -- jobs, webhooks o integraciones -- contratos vigentes -- fuentes canonicas declaradas si existen - -### `ui_rules.md` - -- direccion visual activa -- componentes base a respetar -- responsive, accesibilidad y copy -- patrones que no deben degradarse - -### `DECISIONS.md` - -- decisiones vigentes confirmadas -- por que existen si esta explicitado -- contradicciones o ambiguedades importantes - -### `LEARNINGS.md` - -- descubrimientos relevantes -- errores ya detectados -- trampas recurrentes -- cosas que futuros agentes no deben romper - -### `TASKS.md` - -- pendientes concretos -- prioridad -- contexto minimo -- criterio de cierre -- bloqueos si existen - -## Estilo de salida - -- usa bullets cortos -- se concreto, tecnico y breve -- evita teoria general -- evita relleno -- si faltan archivos, crealos con contenido inicial util - -## Entrega final - -1. Crea o actualiza los archivos. -2. Resume en 10-15 lineas el estado real del proyecto. -3. Lista supuestos, huecos o zonas que requieren confirmacion manual. diff --git a/prompts/context_templates/frontend_analysis.prompt.md b/prompts/context_templates/frontend_analysis.prompt.md deleted file mode 100644 index 8c4ad5d..0000000 --- a/prompts/context_templates/frontend_analysis.prompt.md +++ /dev/null @@ -1,42 +0,0 @@ -# frontend analysis prompt - -You are a senior frontend architecture reviewer. - -Goal: -Analyze the target frontend repository as an operational interface, not as a developer demo. - -Inputs: - -- repository tree -- `AI_CONTEXT` -- generated reports -- UX and QA findings - -Ignore completely: - -- README files -- onboarding guides -- installation instructions -- developer documentation -- CI/CD setup unless it directly affects user-facing reliability - -Focus on: - -- navigation structure -- dashboard usefulness -- form complexity -- workflow clarity -- terminology clarity -- search and filtering -- table usability -- error clarity - -Output: - -- key frontend surfaces -- usability risks -- component-level priorities -- recommended implementation backlog -- review-only patch opportunities - -Do not generate code automatically. diff --git a/prompts/context_templates/performance_review.prompt.md b/prompts/context_templates/performance_review.prompt.md deleted file mode 100644 index c19c1a7..0000000 --- a/prompts/context_templates/performance_review.prompt.md +++ /dev/null @@ -1,31 +0,0 @@ -# performance review prompt - -You are a performance engineer reviewing an application repository. - -Goal: -Identify the highest-value performance improvements without risking correctness. - -Focus on: - -- heavy frontend rendering paths -- large tables and dashboards -- repeated network-driven UI work -- expensive form workflows -- search and filtering latency -- build and dependency overhead - -Prioritize: - -- user-visible slowness -- operational bottlenecks -- low-risk improvements first - -Output: - -- bottleneck hypotheses -- evidence from the repository -- component-level improvement tasks -- validation steps -- review-only implementation candidates - -Do not auto-apply changes. diff --git a/prompts/context_templates/ux_improvement.prompt.md b/prompts/context_templates/ux_improvement.prompt.md deleted file mode 100644 index 92fa0d3..0000000 --- a/prompts/context_templates/ux_improvement.prompt.md +++ /dev/null @@ -1,39 +0,0 @@ -# ux improvement prompt - -You are a senior ERP UX architect focused on non-technical administrative staff. - -Primary user: - -- works with forms daily -- needs clear labels and minimal steps -- should not be forced to understand technical data structures - -Goal: -Turn a developer-oriented ERP interface into an administrative workflow interface. - -Prioritize: - -- fewer clicks -- clearer workflow order -- simpler forms -- dropdowns or search instead of raw IDs -- plain-language labels -- visible next actions -- operational dashboard clarity - -Ignore: - -- visual polish without functional impact -- onboarding documentation -- setup instructions -- developer ergonomics that do not affect end users - -Required output: - -- friction points -- component-level improvements -- navigation simplification plan -- form simplification tasks -- review-only implementation suggestions - -Do not auto-apply changes. diff --git a/reports/beta-readiness.md b/reports/beta-readiness.md deleted file mode 100644 index 215e66f..0000000 --- a/reports/beta-readiness.md +++ /dev/null @@ -1,36 +0,0 @@ -# Beta readiness - -Status: release with warnings. - -## Ready for internal beta - -Yes. The product is stable enough for internal beta validation with real projects. - -Evidence: - -- Local test/build/lint/audit gates passed in baseline. -- Five representative target types were validated. -- Core deterministic CLI commands passed across all targets. -- `swarm cheap` and `swarm balanced` passed on representative small targets with bounded timeouts. -- Runtime outputs were written to isolated `.tmp/beta-validation` directories. -- Output contracts and installation docs are now documented. - -## Not ready for public beta without warnings - -Remaining blockers/warnings: - -- GitHub Actions must pass remotely after push. -- Dependabot alerts must refresh and confirm no fixable high/moderate vulnerabilities remain. -- A real non-technical user test has not yet been executed. -- `swarm thorough` has not been validated in this release candidate. - -## Not ready for commercial sale - -Commercial readiness still needs: - -- signed releases or provenance -- broader OS validation -- stronger onboarding polish -- support process -- more real-world fixtures -- optional metrics/telemetry diff --git a/reports/ci_status_report.md b/reports/ci_status_report.md deleted file mode 100644 index e6bbd01..0000000 --- a/reports/ci_status_report.md +++ /dev/null @@ -1,46 +0,0 @@ -# CI Status Report - -## Pipeline - -- Workflow: `project-brain-ci` -- Platform: GitHub Actions -- Triggers: - - `push` - - `pull_request` - -## Quality gates - -The pipeline runs the following stages in order: - -1. checkout -2. install dependencies -3. build -4. typecheck -5. run tests -6. run smoke tests -7. generate reports - -## Failure conditions - -The workflow fails if any of the following commands fail: - -- `npm run build` -- `npm run typecheck` -- `npm run test` -- `npm run test:smoke` - -## Merge protection intent - -When this workflow is used as a required status check in GitHub branch protection, failing tests or smoke tests block merge. - -## Generated CI artifacts - -The workflow uploads these reports as build artifacts: - -- `reports/ci_status_report.md` -- `reports/test_baseline_report.md` -- `reports/test_coverage_initial.md` - -## Outcome - -This pipeline establishes a minimal but strict CI gate so proposed changes must pass build, typecheck, automated tests, and smoke workflows before acceptance. diff --git a/reports/dev_architecture_analysis.md b/reports/dev_architecture_analysis.md deleted file mode 100644 index 9369f2f..0000000 --- a/reports/dev_architecture_analysis.md +++ /dev/null @@ -1,106 +0,0 @@ -# Dev Architecture Analysis - -## Summary - -53 modules were analyzed with dependency-cruiser, ts-prune, and ESLint. The dependency graph has 53 local nodes and 87 local edges, with a coupling index of 3.28. - -## Structural Metrics - -- Number of modules: 53 -- Dependency graph: 53 local nodes / 87 local edges -- Coupling index: 3.28 -- Circular dependencies: 0 -- Unused exports: 16 -- Largest modules over 500 lines: 0 - -## Top 10 Architecture Risks - -### 1. [HIGH] Isolate self-governance-system.ts responsibilities - -- Problem: governance/self-governance-system.ts combines 377 lines with a coupling score of 11, making it a high-friction change hotspot. -- Affected files: governance/self-governance-system.ts -- Suggested change: Split planning, execution, persistence, and report rendering responsibilities into narrower services with explicit interfaces. -- Estimated difficulty: high -- Confidence: 0.9 - -### 2. [MEDIUM] Prune unused public exports - -- Problem: ts-prune reported 17 unused export(s), which increases public API surface without delivering value. -- Affected files: governance/autonomous-scheduler.ts, orchestrator/chief-agent.ts, orchestrator/main.ts, orchestrator/scheduler.ts, shared/fs-utils.ts, shared/types.ts -- Suggested change: Remove compatibility re-exports that are no longer consumed, or document them as intentional public API contracts. -- Estimated difficulty: low -- Confidence: 0.86 - -### 3. [MEDIUM] Extract repeated agent analysis scaffolding - -- Problem: The duplication scan found 9 modules repeating the same evaluation skeleton, which will make agent behavior harder to evolve consistently. -- Affected files: agents/architecture_agent/index.ts, agents/dependency_agent/index.ts, agents/legal_agent/index.ts, agents/observability_agent/index.ts, agents/optimization_agent/index.ts, agents/product_agent/index.ts, agents/product_owner_agent/index.ts, agents/qa_agent/index.ts, agents/security_agent/index.ts -- Suggested change: Move repeated findings/recommendations setup into shared helper utilities or richer base-agent primitives before adding more specialist heuristics. -- Estimated difficulty: medium -- Confidence: 0.82 - -### 4. [MEDIUM] Isolate main.ts responsibilities - -- Problem: core/orchestrator/main.ts combines 159 lines with a coupling score of 10, making it a high-friction change hotspot. -- Affected files: core/orchestrator/main.ts -- Suggested change: Split planning, execution, persistence, and report rendering responsibilities into narrower services with explicit interfaces. -- Estimated difficulty: medium -- Confidence: 0.78 - -### 5. [MEDIUM] Add explicit runtime error boundaries - -- Problem: Several high-signal modules perform async or file-system work without visible try/catch or promise error boundaries. -- Affected files: agents/dev_agent/index.ts, analysis/dependency_scanner/index.ts, core/orchestrator/main.ts, memory/context_store/index.ts -- Suggested change: Wrap repository IO, manifest parsing, and orchestration transitions in explicit error boundaries that preserve context and failure cause. -- Estimated difficulty: medium -- Confidence: 0.77 - -### 6. [MEDIUM] Instrument key runtime boundaries with structured logs - -- Problem: Critical runtime modules with high fan-in or fan-out still operate without structured logging, reducing diagnosability during continuous analysis. -- Affected files: analysis/api_scanner/index.ts, analysis/dependency_scanner/index.ts, memory/context_store/index.ts, tools/openapi_tools/index.ts -- Suggested change: Add structured lifecycle logs around discovery, parsing, message coordination, and persistence boundaries so failures can be traced by cycle and module. -- Estimated difficulty: low -- Confidence: 0.74 - -## Refactoring Suggestions - -- Isolate self-governance-system.ts responsibilities -> Split planning, execution, persistence, and report rendering responsibilities into narrower services with explicit interfaces. (files: governance/self-governance-system.ts; difficulty: high; confidence: 0.9) -- Prune unused public exports -> Remove compatibility re-exports that are no longer consumed, or document them as intentional public API contracts. (files: governance/autonomous-scheduler.ts, orchestrator/chief-agent.ts, orchestrator/main.ts, orchestrator/scheduler.ts, shared/fs-utils.ts, shared/types.ts; difficulty: low; confidence: 0.86) -- Extract repeated agent analysis scaffolding -> Move repeated findings/recommendations setup into shared helper utilities or richer base-agent primitives before adding more specialist heuristics. (files: agents/architecture_agent/index.ts, agents/dependency_agent/index.ts, agents/legal_agent/index.ts, agents/observability_agent/index.ts, agents/optimization_agent/index.ts, agents/product_agent/index.ts, agents/product_owner_agent/index.ts, agents/qa_agent/index.ts, agents/security_agent/index.ts; difficulty: medium; confidence: 0.82) -- Isolate main.ts responsibilities -> Split planning, execution, persistence, and report rendering responsibilities into narrower services with explicit interfaces. (files: core/orchestrator/main.ts; difficulty: medium; confidence: 0.78) -- Add explicit runtime error boundaries -> Wrap repository IO, manifest parsing, and orchestration transitions in explicit error boundaries that preserve context and failure cause. (files: agents/dev_agent/index.ts, analysis/dependency_scanner/index.ts, core/orchestrator/main.ts, memory/context_store/index.ts; difficulty: medium; confidence: 0.77) - -## Modules With Highest Complexity - -- shared/fs-utils.ts (118 lines, coupling 20, complexity 33.93, change 83 via coupling-size-proxy) -- agents/product_owner_agent/index.ts (44 lines, coupling 2, complexity 29.47, change 43 via coupling-size-proxy) -- agents/product_agent/index.ts (44 lines, coupling 1, complexity 27.97, change 39 via coupling-size-proxy) -- agents/base-agent.ts (58 lines, coupling 13, complexity 24.93, change 58 via coupling-size-proxy) -- governance/self-governance-system.ts (377 lines, coupling 11, complexity 24.07, change 44 via coupling-size-proxy) - -## Modules Recommended For Isolation - -- shared/fs-utils.ts (118 lines, coupling 20, complexity 33.93, change 83 via coupling-size-proxy) -- agents/catalog.ts (123 lines, coupling 12, complexity 17.1, change 42 via coupling-size-proxy) -- governance/self-governance-system.ts (377 lines, coupling 11, complexity 24.07, change 44 via coupling-size-proxy) -- core/orchestrator/main.ts (159 lines, coupling 10, complexity 16.3, change 36 via coupling-size-proxy) -- tools/dev_analysis_tools/index.ts (353 lines, coupling 4, complexity 20.27, change 22 via coupling-size-proxy) - -## Architectural Observations - -- The local dependency graph is currently acyclic; the main maintainability risk is centralization in a few runtime hubs, not dependency loops. -- governance/self-governance-system.ts (377 lines, coupling 11), core/orchestrator/main.ts (159 lines, coupling 10) currently dominate orchestration and state flow. -- Git history was not available, so change hotspots were approximated from coupling, file size, and duplicate blocks. - -## Static Analysis Snapshot - -- dependency-cruiser: dependency-cruiser completed successfully. -- ts-prune: ts-prune completed successfully. -- ESLint: ESLint completed successfully. -- Circular dependency paths: None -- Largest modules: governance/self-governance-system.ts (377 lines), tools/dev_analysis_tools/index.ts (353 lines), memory/context_store/index.ts (350 lines), shared/types.ts (253 lines), tools/dev_analysis_tools/contracts.ts (205 lines) -- Highest change hotspots: shared/fs-utils.ts (83), agents/base-agent.ts (58), governance/self-governance-system.ts (44), agents/product_owner_agent/index.ts (43), agents/catalog.ts (42) -- Missing logging candidates: memory/context_store/index.ts, analysis/api_scanner/index.ts, tools/openapi_tools/index.ts, analysis/dependency_scanner/index.ts, governance/message-center.ts -- Missing error handling candidates: memory/context_store/index.ts, analysis/dependency_scanner/index.ts, core/orchestrator/main.ts, agents/dev_agent/index.ts, governance/message-center.ts -- Unused exports: governance/autonomous-scheduler.ts -> ScheduledCycle (used in module), orchestrator/chief-agent.ts -> ChiefAgent, orchestrator/main.ts -> ProjectBrainOrchestrator, orchestrator/scheduler.ts -> WeeklyScheduler, shared/fs-utils.ts -> relativeTo, shared/types.ts -> AgentAction (used in module), shared/types.ts -> AgentMessageType (used in module), shared/types.ts -> TaskState (used in module), shared/types.ts -> ProposalStatus (used in module), shared/types.ts -> RepoStructure (used in module), memory/context_store/index.ts -> readExistingTasks, tools/dev_analysis_tools/contracts.ts -> SOURCE_EXTENSIONS (used in module), tools/dev_analysis_tools/contracts.ts -> IGNORED_PREFIXES (used in module), tools/dev_analysis_tools/contracts.ts -> DependencyCruiserDependency (used in module), tools/dev_analysis_tools/contracts.ts -> EslintMessage (used in module), agents/product_agent/index.ts -> ProductAgent diff --git a/reports/final-tag-gate-0.2.0.md b/reports/final-tag-gate-0.2.0.md deleted file mode 100644 index 8eef741..0000000 --- a/reports/final-tag-gate-0.2.0.md +++ /dev/null @@ -1,323 +0,0 @@ -# Final tag gate 0.2.0 - -Date: 2026-05-08. - -Final recommendation: release with warnings. - -## 1. Final state - -`project-brain` 0.2.0 is ready to commit and push as an internal beta release candidate. - -It is not ready to tag until remote GitHub Actions and Dependabot refresh are confirmed after push. - -## 2. Pre-commit review - -### package.json - -Status: pass. - -- `version`: `0.2.0`. -- `bin.project-brain`: `dist/cli/project-brain.js`. -- `engines.node`: `>=20`. -- `prepack`: `npm run build`. -- `files` includes runtime CLI output, docs, prompts, templates, schemas, and changelog. -- Runtime noise is not included in the package files list. - -### package-lock.json - -Status: pass. - -Changes are coherent with `package.json`: - -- root version changed to `0.2.0`. -- root engines added. -- `@types/node` aligned to Node 20 support. -- no suspicious runtime dependency changes were observed in the reviewed diff. - -### Workflows - -Status: pass locally, pending remote execution. - -- `project-brain-ci.yml` runs on Node 20 and Node 22. -- `project-brain-ci.yml` runs `npm ci`, lint, build, typecheck, tests, smoke tests, audit high, and repo safety. -- `security-baseline.yml` runs on Node 20 and Node 22. -- `dependency-review.yml` fails on high severity. -- No workflow secrets are required by the changed jobs. - -### Reports policy - -Status: pass. - -The following reports are deliberately versioned release evidence: - -- `reports/validation-matrix.md` -- `reports/validation-results.json` -- `reports/beta-readiness.md` -- `reports/release-candidate-0.2.0.md` -- `reports/final-tag-gate-0.2.0.md` - -Runtime ad hoc reports remain ignored by policy, for example `reports/doctor.md`. - -### Schemas - -Status: pass. - -Validated as JSON: - -- `schemas/repository_fact_graph.schema.json` -- `schemas/preflight_facts.schema.json` -- `schemas/validation_results.schema.json` -- `reports/validation-results.json` - -### Docs - -Status: pass. - -Docs are consistent with “internal beta stable with warnings”: - -- `docs/installation.md` -- `docs/first-analysis-5-min.md` -- `docs/user-test-script.md` -- `docs/output-contract.md` -- `docs/backlog-commercial-hardening.md` -- `docs/release-checklist.md` -- `docs/releases/0.2.0.md` -- `README.md` -- `docs/usage.md` -- `AI_REVIEW_START_HERE.md` -- `CHANGELOG.md` - -## 3. Local reproducible validation - -Environment: - -- Local Node: `v25.9.0`. -- Local npm: `11.12.1`. -- `nvm`, `volta`, `asdf`, and `fnm`: not available locally. - -Result table: - -| Command | Result | -|---|---| -| `npm ci` | pass | -| `npm test` | pass | -| `npm run build` | pass | -| `npm run lint` | pass | -| `npm audit --audit-level=high` | pass | -| `npm pack --dry-run --json` | pass | -| `node dist/cli/project-brain.js --help` | pass | -| `node dist/cli/project-brain.js go --help` | pass | -| `node dist/cli/project-brain.js status . --output .tmp/final-gate/self` | pass | -| `node dist/cli/project-brain.js resume . --output .tmp/final-gate/self` | pass | -| `node dist/cli/project-brain.js fact-query "qué versión tiene este paquete" . --output .tmp/final-gate/self` | pass | -| `node dist/cli/project-brain.js runbook "release final gate" . --output .tmp/final-gate/self` | pass | -| `node dist/cli/project-brain.js doctor . --output .tmp/final-gate/self` | pass | -| `node dist/cli/project-brain.js console --help` | pass | - -## 4. Tarball clean install gate - -Status: pass. - -Procedure executed: - -```bash -npm pack --pack-destination .tmp/tarball-gate -mkdir temporary install directory under /tmp -npm init -y -npm install /.tmp/tarball-gate/project-brain-0.2.0.tgz -npx project-brain --help -npx project-brain go --help -npx project-brain status . --output /out -npx project-brain doctor . --output /out -``` - -Evidence: - -- Tarball installed successfully. -- `npx project-brain --help` passed. -- `npx project-brain go --help` passed. -- `npx project-brain status` passed. -- `npx project-brain doctor` passed. -- `node_modules/project-brain/dist/cli/project-brain.js` exists. -- `node_modules/project-brain/docs/output-contract.md` exists. -- `node_modules/project-brain/schemas/preflight_facts.schema.json` exists. - -## 5. Secret/runtime safety review - -Status: pass with notes. - -No intended commit command should include: - -- `node_modules/` -- `.tmp/` -- `.claude/` -- `.env` -- private keys -- credentials -- ad hoc runtime doctor reports - -Large files observed under `.tmp/` and existing generated output paths are not part of the recommended `git add` command. - -## 6. GitHub remote gate - -Status: blocked until this fix branch passes remotely and is merged. - -Original failed run: - -- Workflow: `project-brain-ci` -- Run: `25575315699` -- SHA: `c27439f25a81663a67a91f8ca280ffd67840519f` -- Failed job: `quality-gates (20)` -- Passing job: `quality-gates (22)` - -Rerun status: - -- `gh run rerun 25575315699 --failed` was executed. -- `quality-gates (20)` failed again. -- This is not treated as a one-off flake. - -Observed failures: - -- `tests/integration/dev-agent-analysis.test.ts`: default `5000ms` timeout was too low on Node 20 CI; observed runtime was about `5171ms`. -- `tests/smoke/cli-workflows.test.ts`: explicit `15000ms` timeout was too low on Node 20 CI; observed runtime was about `16976ms`. - -Cause: - -- Node 20 GitHub runner executes the full suite more slowly than local Node 25 and Node 22 CI. The failures are timeout budget issues in two integration/smoke tests, not assertion failures and not product behavior failures. - -Fix: - -- Add a specific `15000ms` timeout to `tests/integration/dev-agent-analysis.test.ts`. -- Increase only the affected first CLI smoke workflow timeout to `45000ms`. -- No product code, package version, CLI behavior, assertions, or workflow matrix were changed. - -Post-fix local validation: - -| Command | Result | -|---|---| -| `npm ci` | pass | -| `npm test -- tests/integration/dev-agent-analysis.test.ts` | pass | -| `npm test -- tests/smoke/cli-workflows.test.ts` | pass | -| `npm test` | pass | -| `npm run lint` | pass | -| `npm run typecheck` | pass | -| `npm run build` | pass | -| `npm audit --audit-level=high` | pass, 0 vulnerabilities | -| `npm pack --dry-run --json` | pass | -| `project-brain --help` | pass | -| `project-brain go --help` | pass | -| `project-brain status` | pass | -| `project-brain resume` | pass | -| `project-brain fact-query "qué versión tiene este paquete"` | pass | -| `project-brain runbook` | pass | -| `project-brain doctor` | pass | -| `project-brain console --help` | pass | - -Dependabot gate: - -- `gh api /repos//dependabot/alerts?state=open&severity=critical,high,medium`: returned no open alerts. -- `gh api /repos//dependabot/alerts?state=open`: returned no open alerts. -- `npm audit --audit-level=moderate --json`: `0` critical, high, moderate, low, and total vulnerabilities. -- Open Dependabot PRs exist, but they are not treated as release-blocking security alerts without a matching open alert or local audit finding. Several are stale/conflicting version update PRs. -- Revalidation after this fix branch: no critical/high/medium Dependabot alerts were returned and `npm audit --audit-level=moderate --json` remained at `0` total vulnerabilities. - -Post-fix remote validation: - -- PR workflow result: pass on PR `#17`. -- PR checks observed as passing: - - `dependency-review` - - `quality-gates (20)` - - `quality-gates (22)` - - `security-baseline (20)` - - `security-baseline (22)` -- PR merge status: blocked by base branch policy, not by test failure. -- Auto-merge was enabled with squash merge. -- Main workflow result after merge: pending. -- Dependabot state after merge: pending. - -Post-push checklist: - -- `project-brain-ci.yml` passes on Node 20. -- `project-brain-ci.yml` passes on Node 22. -- `security-baseline.yml` passes on Node 20. -- `security-baseline.yml` passes on Node 22. -- `dependency-review.yml` does not block without reason. -- CI artifacts upload without name collision. -- Dependabot npm alerts refresh. -- Dependabot GitHub Actions alerts refresh. -- No high/moderate vulnerability remains when a non-risky fix is available. -- Branch protection required checks match the release process. - -## 7. Tag criteria - -Allow `v0.2.0` only if all are true: - -- commit is created -- push is complete -- GitHub Actions are green remotely -- Dependabot has no high/moderate fixable alerts without risky major upgrades -- tarball clean install remains green -- no secrets or runtime noise are included -- release notes and changelog are present - -Block tag if any are true: - -- remote CI fails -- Node 20 or Node 22 tests fail -- `npm audit` finds high/moderate fixable issues -- tarball install fails -- CLI bin is broken -- `dist` is missing from package -- secrets or credentials are included - -## 8. Risk classification - -### BLOCKER - -None found locally. - -### WARNING - -- Remote GitHub Actions are pending until push. -- Dependabot alert refresh is pending until push/GitHub rescan. -- Local validation used Node `v25.9.0`; Node 20/22 validation is delegated to CI because no local version manager is available. -- `swarm thorough` was not executed because cost/time was not justified for this gate. -- Non-technical user test has been prepared but not executed. - -### BACKLOG - -- Public beta polish. -- Commercial packaging/signing/provenance. -- Broader OS validation. -- Optional telemetry/metrics. -- More real-world fixtures. - -## 9. Commit plan - -Recommended add command: - -```bash -git add .github package.json package-lock.json README.md AI_REVIEW_START_HERE.md docs reports schemas CHANGELOG.md -``` - -Recommended commit: - -```bash -git commit -m "chore(release): prepare 0.2.0 beta candidate" -``` - -Recommended push after commit: - -```bash -git push origin main -``` - -Do not tag until the remote gate passes. - -## 10. Operational decision - -Decision: can commit and push. - -Decision: do not tag yet. - -Final recommendation: release with warnings. diff --git a/reports/release-candidate-0.2.0.md b/reports/release-candidate-0.2.0.md deleted file mode 100644 index c1d26ab..0000000 --- a/reports/release-candidate-0.2.0.md +++ /dev/null @@ -1,115 +0,0 @@ -# Release candidate 0.2.0 - -Date: 2026-05-08. - -Recommendation: release with warnings. - -## 1. Is it ready for internal beta? - -Yes. It is ready for internal beta with controlled users and real project validation. - -## 2. Is it ready for non-technical users? - -Partially. `project-brain go` and `console` are ready for guided testing, but a real non-technical user test is still required before calling it broadly non-technical-ready. - -## 3. Is it ready for public beta? - -Not yet without warnings. Public beta should wait for remote GitHub Actions and Dependabot refresh. - -## 4. Is it ready for commercial sale? - -No. Commercial sale needs stronger onboarding, packaging validation, support process, signed/provenance releases, broader OS validation, and more real-world fixtures. - -## 5. What is missing by level? - -- Internal beta: remote CI confirmation and one human UX test. -- Public beta: Dependabot refresh, GitHub checks green, package dry-run/install smoke. -- Commercial: support, telemetry/metrics, signed releases, enterprise policy, broader compatibility. - -## 6. What was validated? - -- Backend, frontend, mobile, monorepo, and low-documentation targets. -- `go`, `status`, `resume`, `fact-query`, and `runbook` on all targets. -- `swarm cheap` and `swarm balanced` on representative fixtures. -- Local tests, build, lint, audit, and CLI help during baseline. - -## 7. What was not validated? - -- `swarm thorough` due cost/time. -- Remote GitHub Actions after the current changes. -- Dependabot alert refresh after the current changes. -- A fresh global install from a packed npm tarball. -- A live non-technical user session. - -## 8. Remaining risks - -- GitHub may still show stale Dependabot alerts until it rescans. -- Model-heavy outputs vary by provider/model availability. -- Node 20/22 CI matrix must pass remotely. -- Package publishing should be verified with `npm pack --dry-run` before npm release. - -## 9. Commands executed - -Baseline: - -```bash -git status --short -node --version -npm --version -npm run -npm test -npm run build -npm run lint -npm audit -project-brain --help -project-brain go --help -``` - -Validation matrix: - -```bash -project-brain go -project-brain status -project-brain resume -project-brain fact-query -project-brain runbook -project-brain swarm --preset cheap -project-brain swarm --preset balanced -``` - -## 10. Gate results - -- `npm test`: pass, 44 files and 113 tests in baseline. -- `npm run build`: pass in baseline. -- `npm run lint`: pass in baseline. -- `npm audit`: pass in baseline, 0 vulnerabilities. -- Final `npm ci`: pass. -- Final `npm pack --dry-run --json`: pass; package includes `dist/cli/project-brain.js`, docs, `CHANGELOG.md`, and schemas. -- Final `npm audit --audit-level=high`: pass. -- Final CLI smoke: `--help`, `go`, `status`, `resume`, `fact-query`, `runbook`, `doctor`, and `console --help` all passed. - -## 11. CI and Dependabot - -- Existing GitHub workflows were found. -- CI was hardened for Node 20 and Node 22. -- Main CI now runs `npm audit --audit-level=high`. -- Dependency review now fails high-severity PRs. -- Dependabot is configured for npm and GitHub Actions weekly. - -## 12. Docs status - -Updated/added documentation covers installation, first run, usage, release checklist, release notes, output contracts, user test script, and commercial hardening backlog. - -## 13. Output contract status - -Output contracts are documented in `docs/output-contract.md` with schemas for: - -- `schemas/repository_fact_graph.schema.json` -- `schemas/preflight_facts.schema.json` -- `schemas/validation_results.schema.json` - -## 14. Final recommendation - -Release with warnings. - -Do not call this public/commercial-ready until remote CI, Dependabot refresh, npm package dry-run, and one non-technical user test are complete. diff --git a/reports/templates/doctor.md b/reports/templates/doctor.md deleted file mode 100644 index c16df7d..0000000 --- a/reports/templates/doctor.md +++ /dev/null @@ -1,32 +0,0 @@ -# Doctor Report Template - -This is a source template for generated doctor diagnostics. - -Runtime doctor reports are generated at: - -- `reports/doctor.md` -- `AI_CONTEXT/doctor/doctor.json` - -These generated files can contain local absolute paths, installed model names, -runtime versions, and machine-specific diagnostics. They are intentionally not -versioned. - -## Summary - -- Repository: -- Target: -- Output: -- Passed: -- Warnings: -- Failed: -- Headline: - -## Checks - -### Check name - -- Status: -- Summary: - -Details: -- Evidence or diagnostic details diff --git a/reports/templates/improvement_proposals.md b/reports/templates/improvement_proposals.md deleted file mode 100644 index 5e148f3..0000000 --- a/reports/templates/improvement_proposals.md +++ /dev/null @@ -1,7 +0,0 @@ -# Improvement Proposals - -## Proposed Improvements - -- Problem: -- Recommendation: -- Expected impact: diff --git a/reports/templates/risk_report.md b/reports/templates/risk_report.md deleted file mode 100644 index 9efb801..0000000 --- a/reports/templates/risk_report.md +++ /dev/null @@ -1,12 +0,0 @@ -# Risk Report - -## Highest Risks - -- Risk: -- Impact: -- Owner: - -## Follow-up - -- Immediate mitigation: -- Long-term mitigation: diff --git a/reports/templates/weekly_system_report.md b/reports/templates/weekly_system_report.md deleted file mode 100644 index 634cd31..0000000 --- a/reports/templates/weekly_system_report.md +++ /dev/null @@ -1,13 +0,0 @@ -# Weekly System Report - -## Executive Summary - -- Repository: -- Window: -- Overall risk: - -## Highlights - -- Key architectural changes: -- New risks: -- Recommended actions: diff --git a/reports/test_baseline_report.md b/reports/test_baseline_report.md deleted file mode 100644 index d11351c..0000000 --- a/reports/test_baseline_report.md +++ /dev/null @@ -1,57 +0,0 @@ -# Test Baseline Report - -## Status - -- Baseline established: yes -- Framework: `vitest` -- CLI execution support: `ts-node` -- Business logic modified: no -- Test run status: passing -- Smoke run status: passing - -## Test structure - -```text -tests/ - unit/ - integration/ - smoke/ - fixtures/ -``` - -## Implemented coverage - -### Unit - -- orchestrator initialization -- agent registry loading -- memory store initialization and discovery artifact writes -- CLI command parsing and help surface - -### Integration - -- discovery engine analyzing a simple repository fixture -- orchestrator running a repository-change cycle -- governance feedback updating task state and learnings - -### Smoke - -- `project-brain analyze` -- `project-brain weekly` -- `project-brain report` - -Validated outputs: - -- `AI_CONTEXT/` -- `reports/` -- `docs/` - -## Scripts - -- `npm run test` -- `npm run test:watch` -- `npm run test:smoke` - -## Result - -The repository now has an isolated, reproducible automated testing baseline suitable for protecting future autonomous proposal work. diff --git a/reports/test_coverage_initial.md b/reports/test_coverage_initial.md deleted file mode 100644 index e933fd9..0000000 --- a/reports/test_coverage_initial.md +++ /dev/null @@ -1,50 +0,0 @@ -# Test Coverage Initial - -## Coverage map - -### Covered components - -- `core/orchestrator/*` - - initialization path - - analysis cycle execution -- `governance/*` - - registry loading - - task/result persistence through orchestrator cycle - - feedback loop updating completed tasks and learnings -- `cli/project-brain.ts` - - command surface parsing - - smoke execution of main workflows -- `core/discovery_engine/*` - - repository analysis against a stable fixture -- `memory/context_store/*` - - directory creation - - discovery artifact writes -- `agents/catalog.ts` - - dynamic registry population through `AgentRegistry` - -## Fixture characteristics - -The fixture repository includes: - -- `package.json` -- `Dockerfile` -- `openapi.yaml` -- `schema.graphql` -- GitHub Actions workflow -- TypeScript source -- test file - -This allows stack, API, CI, infrastructure, logging, and metrics detection to be exercised consistently. - -## Current limitations - -- no line coverage percentage is produced yet -- no dedicated coverage instrumentation is enabled -- no HTTP/API contract tests are needed yet because there is no service API layer in this repo -- no snapshot testing is included - -## Recommended next expansion - -- add coverage reporting once the baseline remains stable for several cycles -- add regression fixtures for edge-case repositories -- add failure-mode tests for agent supervisor and council conflict resolution diff --git a/reports/validation-matrix.md b/reports/validation-matrix.md deleted file mode 100644 index 971a943..0000000 --- a/reports/validation-matrix.md +++ /dev/null @@ -1,51 +0,0 @@ -# Beta validation matrix - -Validation date: 2026-05-08. - -Output root used locally: `.tmp/beta-validation/`. - -## Targets - -| ID | Type | Target | Rationale | Recommendation | -|---|---|---|---|---| -| `backend_large` | Backend large | `/backend_denuncia` | Real backend with package manifest and multiple app areas | pass | -| `frontend_modern` | Frontend modern | `/Frontend_Denuncia` | Real frontend project | pass | -| `mobile` | Mobile | `/AppDenunciaenLinea` | Real Flutter mobile app (`pubspec.yaml`) | pass | -| `monorepo` | Monorepo/workspace | `tests/fixtures/multi-repo-workspace` | Representative multi-project fixture | pass | -| `docs_poor` | Low documentation | `tests/fixtures/dev-agent-repo` | Minimal fixture for structure-first analysis | pass | - -## Commands per target - -Each target ran: - -```bash -project-brain go "" --output -project-brain status --output -project-brain resume --output -project-brain fact-query "" --output -project-brain runbook "" --output -``` - -Additional bounded swarm checks: - -```bash -project-brain swarm "extrae hallazgos generales sin modificar archivos" tests/fixtures/dev-agent-repo --preset cheap --parallel 1 --max-queued-tasks 1 --max-retries 1 --run-timeout-ms 25000 -project-brain swarm "resume paquetes principales sin modificar archivos" tests/fixtures/multi-repo-workspace --preset balanced --parallel 1 --max-queued-tasks 1 --max-retries 1 --run-timeout-ms 25000 -``` - -`thorough` was not executed because the beta gate is focused on stability and the time/model cost was not justified. - -## Evaluation criteria - -| Criterion | Result | -|---|---| -| Detects repo structure | pass | -| Generates `MEMORY_BRIEF` | pass | -| Generates `EXECUTIVE_SUMMARY` | pass | -| Generates fact graph | pass | -| `fact-query` works without model-heavy flow | pass | -| `preflightFacts` remains first deterministic gate | pass by existing tests and go/runbook flow | -| Distinguishes fresh/stale memory | pass by regression tests | -| Executive summary is useful for unfamiliar users | warning: needs live user test | -| Review-only behavior avoids target file writes | pass; outputs were isolated in `.tmp/beta-validation` | -| Swarm incremental adds value | warning: cheap/balanced pass, quality should be reviewed by humans on larger projects | diff --git a/reports/validation-results.json b/reports/validation-results.json deleted file mode 100644 index d14c2f8..0000000 --- a/reports/validation-results.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": 1, - "generatedAt": "2026-05-08T00:00:00.000Z", - "overallStatus": "pass", - "projects": [ - { "id": "backend_large", "type": "backend", "target": "/backend_denuncia", "recommendation": "pass", "memoryQuality": "pass", "factQueryQuality": "pass", "executiveSummaryQuality": "pass" }, - { "id": "frontend_modern", "type": "frontend", "target": "/Frontend_Denuncia", "recommendation": "pass", "memoryQuality": "pass", "factQueryQuality": "pass", "executiveSummaryQuality": "pass" }, - { "id": "mobile", "type": "mobile", "target": "/AppDenunciaenLinea", "recommendation": "pass", "memoryQuality": "pass", "factQueryQuality": "pass", "executiveSummaryQuality": "pass" }, - { "id": "monorepo", "type": "monorepo", "target": "tests/fixtures/multi-repo-workspace", "recommendation": "pass", "memoryQuality": "pass", "factQueryQuality": "pass", "executiveSummaryQuality": "pass" }, - { "id": "docs_poor", "type": "low_documentation", "target": "tests/fixtures/dev-agent-repo", "recommendation": "pass", "memoryQuality": "pass", "factQueryQuality": "pass", "executiveSummaryQuality": "pass" } - ], - "commandResults": [ - { "id": "backend_large", "commands": ["go", "status", "resume", "fact-query", "runbook"], "status": "passed" }, - { "id": "frontend_modern", "commands": ["go", "status", "resume", "fact-query", "runbook"], "status": "passed" }, - { "id": "mobile", "commands": ["go", "status", "resume", "fact-query", "runbook"], "status": "passed" }, - { "id": "monorepo", "commands": ["go", "status", "resume", "fact-query", "runbook", "swarm balanced"], "status": "passed" }, - { "id": "docs_poor", "commands": ["go", "status", "resume", "fact-query", "runbook", "swarm cheap"], "status": "passed" } - ], - "modelUsage": "Not precisely measured. Deterministic commands completed quickly; swarm cheap and balanced ran with strict 25s run timeout each.", - "risks": [ - "Swarm thorough was skipped by cost justification.", - "Human quality review of summaries is still needed with a non-technical beta user.", - "GitHub Dependabot alert refresh must be checked after push." - ], - "evidenceRefs": [ - ".tmp/beta-validation/logs/results.log", - ".tmp/beta-validation/*/AI_CONTEXT/MEMORY_BRIEF.md", - ".tmp/beta-validation/*/AI_CONTEXT/EXECUTIVE_SUMMARY.md", - ".tmp/beta-validation/*/memory/knowledge_graph/repository_fact_graph.json" - ] -} diff --git a/schema/context-contract.schema.json b/schema/context-contract.schema.json new file mode 100644 index 0000000..aa8b09c --- /dev/null +++ b/schema/context-contract.schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/ruzer/project-brain/schema/context-contract.schema.json", + "title": "Project Brain Lite context contract", + "description": "Contrato portable para el conjunto mínimo de contexto.", + "type": "object", + "required": [ + "contractVersion", + "files", + "generatedFile", + "markers", + "limits" + ], + "properties": { + "contractVersion": { "const": 1 }, + "files": { + "type": "array", + "prefixItems": [ + { "const": "AGENTS.md" }, + { "const": "AI_CONTEXT/CONTEXT.md" }, + { "const": "AI_CONTEXT/DECISIONS.md" }, + { "const": "AI_CONTEXT/TASKS.md" }, + { "const": "AI_CONTEXT/LEARNINGS.md" } + ], + "minItems": 5, + "maxItems": 5 + }, + "generatedFile": { "const": "AI_CONTEXT/CONTEXT.md" }, + "markers": { + "type": "object", + "required": ["start", "end"], + "properties": { + "start": { "const": "" }, + "end": { "const": "" } + }, + "additionalProperties": false + }, + "limits": { + "type": "object", + "required": ["bytesPerFile", "linesPerFile", "totalBytes"], + "properties": { + "bytesPerFile": { "type": "integer", "minimum": 1 }, + "linesPerFile": { "type": "integer", "minimum": 1 }, + "totalBytes": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "default": { + "contractVersion": 1, + "files": [ + "AGENTS.md", + "AI_CONTEXT/CONTEXT.md", + "AI_CONTEXT/DECISIONS.md", + "AI_CONTEXT/TASKS.md", + "AI_CONTEXT/LEARNINGS.md" + ], + "generatedFile": "AI_CONTEXT/CONTEXT.md", + "markers": { + "start": "", + "end": "" + }, + "limits": { + "bytesPerFile": 12288, + "linesPerFile": 240, + "totalBytes": 40960 + } + } +} diff --git a/schemas/preflight_facts.schema.json b/schemas/preflight_facts.schema.json deleted file mode 100644 index 7f66660..0000000 --- a/schemas/preflight_facts.schema.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://project-brain.local/schemas/preflight_facts.schema.json", - "title": "PreflightFactsResult", - "type": "object", - "required": ["intent", "query", "factsFound", "facts", "evidence", "freshness", "staleIgnored", "confidence", "recommendedNextAction", "readiness", "sources", "unknowns"], - "additionalProperties": true, - "properties": { - "intent": { "type": "string" }, - "query": { "type": "string" }, - "scope": { "type": "string" }, - "factsFound": { "type": "boolean" }, - "facts": { "type": "array", "items": { "type": "string" } }, - "evidence": { "type": "array", "items": { "type": "string" } }, - "freshness": { - "type": "object", - "required": ["freshScopes", "staleScopes", "missingScopes"], - "properties": { - "freshScopes": { "type": "array", "items": { "type": "string" } }, - "staleScopes": { "type": "array", "items": { "type": "string" } }, - "missingScopes": { "type": "array", "items": { "type": "string" } } - } - }, - "staleIgnored": { "type": "array", "items": { "type": "string" } }, - "confidence": { "enum": ["none", "low", "medium", "high"] }, - "recommendedNextAction": { "enum": ["answer-from-memory", "run-code-graph", "run-fact-query", "run-swarm-delta", "continue-workflow"] }, - "readiness": { - "type": "object", - "required": ["hasMemoryBrief", "hasExecutiveSummary", "hasFactGraph", "hasFreshScopeMemory"], - "properties": { - "hasMemoryBrief": { "type": "boolean" }, - "hasExecutiveSummary": { "type": "boolean" }, - "hasFactGraph": { "type": "boolean" }, - "hasFreshScopeMemory": { "type": "boolean" } - } - }, - "sources": { "type": "object", "additionalProperties": { "type": "string" } }, - "unknowns": { "type": "array", "items": { "type": "string" } } - } -} diff --git a/schemas/repository_fact_graph.schema.json b/schemas/repository_fact_graph.schema.json deleted file mode 100644 index 754bc4b..0000000 --- a/schemas/repository_fact_graph.schema.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://project-brain.local/schemas/repository_fact_graph.schema.json", - "title": "RepositoryFactGraphDocument", - "type": "object", - "required": ["version", "generatedAt", "targetPath", "repoName", "nodes", "edges", "stats"], - "additionalProperties": true, - "properties": { - "version": { "const": 1 }, - "generatedAt": { "type": "string" }, - "targetPath": { "type": "string" }, - "repoName": { "type": "string" }, - "nodes": { - "type": "array", - "items": { - "type": "object", - "required": ["id", "kind", "label"], - "additionalProperties": true, - "properties": { - "id": { "type": "string" }, - "kind": { "enum": ["repository", "directory", "file", "symbol", "language", "framework", "manifest", "api_surface", "infra_surface"] }, - "label": { "type": "string" }, - "path": { "type": "string" }, - "attributes": { "type": "object", "additionalProperties": true } - } - } - }, - "edges": { - "type": "array", - "items": { - "type": "object", - "required": ["from", "to", "kind"], - "additionalProperties": true, - "properties": { - "from": { "type": "string" }, - "to": { "type": "string" }, - "kind": { "enum": ["contains", "uses_language", "uses_framework", "has_manifest", "exposes_api", "defines_infra", "declares", "imports", "calls"] }, - "evidence": { "type": "array", "items": { "type": "string" } } - } - } - }, - "stats": { - "type": "object", - "required": ["nodes", "edges", "codeGraphFiles", "codeGraphSymbols", "nodeKinds", "edgeKinds"], - "additionalProperties": true, - "properties": { - "nodes": { "type": "number" }, - "edges": { "type": "number" }, - "codeGraphFiles": { "type": "number" }, - "codeGraphSymbols": { "type": "number" }, - "nodeKinds": { "type": "object", "additionalProperties": { "type": "number" } }, - "edgeKinds": { "type": "object", "additionalProperties": { "type": "number" } } - } - } - } -} diff --git a/schemas/validation_results.schema.json b/schemas/validation_results.schema.json deleted file mode 100644 index 515d1f1..0000000 --- a/schemas/validation_results.schema.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://project-brain.local/schemas/validation_results.schema.json", - "title": "ValidationResults", - "type": "object", - "required": ["version", "generatedAt", "overallStatus", "projects", "commandResults", "risks"], - "additionalProperties": true, - "properties": { - "version": { "const": 1 }, - "generatedAt": { "type": "string" }, - "overallStatus": { "enum": ["pass", "warning", "fail"] }, - "projects": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, - "commandResults": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, - "risks": { "type": "array", "items": { "type": "string" } } - } -} diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 12bea58..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# scripts - -Reserved for repository maintenance, export, and automation helpers. - -This directory now also contains: - -- git hook installers -- repository safety checks -- commit message validation - -These scripts harden contribution flow, but they do not change `project-brain` runtime behavior against analyzed target repositories. diff --git a/scripts/check-commit-message.mjs b/scripts/check-commit-message.mjs deleted file mode 100644 index ec4184f..0000000 --- a/scripts/check-commit-message.mjs +++ /dev/null @@ -1,38 +0,0 @@ -import { readFileSync } from "node:fs"; - -const messageFile = process.argv[2]; - -if (!messageFile) { - console.error("commit message path is required"); - process.exit(1); -} - -const firstLine = readFileSync(messageFile, "utf8") - .split("\n")[0] - .trim(); - -const blockedPrefixes = /^(wip|tmp|test|misc|stuff)([:\s-]|$)/i; - -if (!firstLine) { - console.error("commit message cannot be empty"); - process.exit(1); -} - -if (blockedPrefixes.test(firstLine)) { - console.error(`commit message is too weak: "${firstLine}"`); - console.error("Use a short descriptive summary instead of WIP/tmp placeholders."); - process.exit(1); -} - -if (firstLine.length < 12) { - console.error(`commit message is too short: "${firstLine}"`); - console.error("Use at least 12 characters so the change is understandable in history."); - process.exit(1); -} - -if (!/[A-Za-z]/.test(firstLine)) { - console.error(`commit message must contain readable text: "${firstLine}"`); - process.exit(1); -} - -console.log("commit message check passed."); diff --git a/scripts/check-repo-safety.mjs b/scripts/check-repo-safety.mjs deleted file mode 100644 index 6be8891..0000000 --- a/scripts/check-repo-safety.mjs +++ /dev/null @@ -1,224 +0,0 @@ -import { existsSync, readFileSync, statSync } from "node:fs"; -import { execFileSync } from "node:child_process"; -import path from "node:path"; - -const args = new Set(process.argv.slice(2)); -const scanMode = args.has("--all") ? "all" : "staged"; - -const blockedDirPrefixes = [ - "node_modules/", - "dist/", - "build/", - "coverage/", - "cache/", - ".cache/", - "tmp/", - ".tmp/", - "logs/", - "sample-output/", - "pb-output/", - "project-brain/pb-output/", - ".project-brain-local/" -]; - -const blockedBinarySuffixes = [".pem", ".p12", ".pfx"]; -const blockedBinaryExact = [".envrc"]; -const secretPatterns = [ - { label: "private key material", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ }, - { label: "OpenAI-style secret", regex: /\bsk-[A-Za-z0-9]{20,}\b/ }, - { label: "GitHub personal access token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/ }, - { label: "GitHub fine-grained token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/ }, - { label: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ }, - { label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ }, - { - label: "credential assignment", - regex: /\b(api[_-]?key|access[_-]?token|auth[_-]?token|secret|password)\b\s*[:=]\s*["'][^"'\\n]{10,}["']/i - }, - { - label: "absolute local filesystem path", - regex: /(?:^|[\s"'`(])(?:\/Users\/[^/\s]+\/|\/home\/[^/\s]+\/|[A-Za-z]:\\Users\\[^\\\s]+)/ - } -]; - -const privatePatternsFile = path.join(".project-brain-local", "private-patterns.txt"); - -function runGit(argsToRun) { - return execFileSync("git", argsToRun, { encoding: "utf8" }).trim(); -} - -function escapeRegex(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function loadPrivatePatterns() { - const configPath = path.join(process.cwd(), privatePatternsFile); - if (!existsSync(configPath)) { - return []; - } - - const content = readFileSync(configPath, "utf8"); - return content - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")) - .map((line) => ({ - label: `private pattern (${line})`, - regex: new RegExp(escapeRegex(line), "i") - })); -} - -function listFiles() { - if (scanMode === "all") { - const output = runGit(["ls-files"]); - return output ? output.split("\n").filter(Boolean) : []; - } - - const output = runGit(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]); - return output ? output.split("\n").filter(Boolean) : []; -} - -function isBlockedEnvFile(filePath) { - const baseName = path.basename(filePath); - if (!baseName.startsWith(".env")) { - return false; - } - - return !/\.env\.(example|sample)$/i.test(baseName); -} - -function blockedPathReason(filePath) { - if (isBlockedEnvFile(filePath)) { - return "environment files must not be committed"; - } - - if (blockedBinaryExact.includes(path.basename(filePath))) { - return "shell env files must stay local"; - } - - if (blockedDirPrefixes.some((prefix) => filePath.startsWith(prefix))) { - return "generated or local-only path"; - } - - if (blockedBinarySuffixes.some((suffix) => filePath.toLowerCase().endsWith(suffix))) { - return "binary credential material must not be committed"; - } - - if (filePath.toLowerCase().endsWith(".key") && !filePath.startsWith("tests/fixtures/")) { - return "key files must not be committed outside fixtures"; - } - - return null; -} - -function isProbablyTextFile(filePath) { - const extension = path.extname(filePath).toLowerCase(); - const textExtensions = new Set([ - ".ts", - ".tsx", - ".js", - ".jsx", - ".mjs", - ".cjs", - ".json", - ".md", - ".txt", - ".yml", - ".yaml", - ".sh", - ".env", - ".toml", - ".graphql" - ]); - - return textExtensions.has(extension) || !extension; -} - -function isPlaceholderValue(line) { - return /(example|sample|changeme|replace[-_ ]?me|your[_-]?(key|token|secret)|placeholder|dummy)/i.test(line); -} - -function collectAddedLinesFromStagedDiff(files) { - if (!files.length) { - return []; - } - - const diff = execFileSync("git", ["diff", "--cached", "--unified=0", "--no-color", "--", ...files], { - encoding: "utf8" - }); - - return diff - .split("\n") - .filter((line) => line.startsWith("+") && !line.startsWith("+++")) - .map((line) => line.slice(1)); -} - -function collectAllFileLines(files) { - const lines = []; - - for (const filePath of files) { - if (!isProbablyTextFile(filePath)) { - continue; - } - - try { - const fileStats = statSync(filePath); - if (fileStats.size > 1024 * 1024) { - continue; - } - - const content = readFileSync(filePath, "utf8"); - lines.push(...content.split("\n")); - } catch { - // Ignore unreadable paths. Git should keep the tracked list coherent. - } - } - - return lines; -} - -function findSecretHits(lines) { - const hits = []; - const privatePatterns = loadPrivatePatterns(); - const patterns = [...secretPatterns, ...privatePatterns]; - - for (const line of lines) { - if (isPlaceholderValue(line)) { - continue; - } - - for (const pattern of patterns) { - if (pattern.regex.test(line)) { - hits.push({ label: pattern.label, line: line.trim() }); - } - } - } - - return hits; -} - -const files = listFiles(); -const blockers = []; - -for (const filePath of files) { - const reason = blockedPathReason(filePath); - if (reason) { - blockers.push(`blocked path: ${filePath} (${reason})`); - } -} - -const linesToScan = scanMode === "all" ? collectAllFileLines(files) : collectAddedLinesFromStagedDiff(files); -const secretHits = findSecretHits(linesToScan); - -for (const hit of secretHits) { - blockers.push(`secret-like content: ${hit.label} -> ${hit.line}`); -} - -if (blockers.length > 0) { - console.error(`project-brain repo safety check failed in ${scanMode} mode.`); - for (const blocker of blockers) { - console.error(`- ${blocker}`); - } - process.exit(1); -} - -console.log(`project-brain repo safety check passed in ${scanMode} mode.`); diff --git a/scripts/git-hooks/commit-msg b/scripts/git-hooks/commit-msg deleted file mode 100755 index c390231..0000000 --- a/scripts/git-hooks/commit-msg +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -set -eu - -repo_root="$(git rev-parse --show-toplevel)" -cd "$repo_root" - -node scripts/check-commit-message.mjs "$1" diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit deleted file mode 100755 index 5193c59..0000000 --- a/scripts/git-hooks/pre-commit +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -eu - -repo_root="$(git rev-parse --show-toplevel)" -cd "$repo_root" - -node scripts/check-repo-safety.mjs --staged -npm run lint diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push deleted file mode 100755 index ac8d544..0000000 --- a/scripts/git-hooks/pre-push +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -set -eu - -repo_root="$(git rev-parse --show-toplevel)" -cd "$repo_root" - -npm run verify:quick diff --git a/scripts/install-hooks.mjs b/scripts/install-hooks.mjs deleted file mode 100644 index 2242383..0000000 --- a/scripts/install-hooks.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { chmodSync, existsSync } from "node:fs"; -import { execFileSync } from "node:child_process"; - -function runGit(args) { - return execFileSync("git", args, { encoding: "utf8" }).trim(); -} - -try { - runGit(["rev-parse", "--show-toplevel"]); -} catch { - console.log("Skipping hook installation because this directory is not a git repository."); - process.exit(0); -} - -const hooksPath = "scripts/git-hooks"; -const hookFiles = ["pre-commit", "pre-push", "commit-msg"]; - -if (!existsSync(".git")) { - console.log("Skipping hook installation because .git is not present."); - process.exit(0); -} - -for (const hookFile of hookFiles) { - chmodSync(`${hooksPath}/${hookFile}`, 0o755); -} - -runGit(["config", "--local", "core.hooksPath", hooksPath]); -console.log(`Configured git hooks at ${hooksPath}`); diff --git a/scripts/self-analyze.sh b/scripts/self-analyze.sh deleted file mode 100755 index 9fa0fe5..0000000 --- a/scripts/self-analyze.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -node dist/cli/project-brain.js start "analiza y mejora project-brain" . --output ./BRAIN -node dist/cli/project-brain.js fact-query "workflow registry orchestrator swarm" . --output ./BRAIN -node dist/cli/project-brain.js swarm "identifica deuda técnica y módulos sin tests" . \ - --output ./BRAIN --preset balanced -node dist/cli/project-brain.js plan-improvements . --output ./BRAIN diff --git a/scripts/unused-exports-review.mjs b/scripts/unused-exports-review.mjs deleted file mode 100755 index 30f5698..0000000 --- a/scripts/unused-exports-review.mjs +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node -import { execFileSync } from "node:child_process"; -import { mkdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; - -const outputPath = path.resolve(process.cwd(), process.argv[2] ?? ".tmp/unused-exports-review.md"); - -function runTsPrune() { - try { - return execFileSync("npx", ["ts-prune"], { - cwd: process.cwd(), - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"] - }); - } catch (error) { - const stdout = typeof error.stdout === "string" ? error.stdout : ""; - const stderr = typeof error.stderr === "string" ? error.stderr : ""; - if (stdout.trim()) { - return stdout; - } - throw new Error(stderr.trim() || error.message); - } -} - -function parseLine(line) { - const match = line.match(/^(.+?):(\d+)\s+-\s+(.+?)(?:\s+\((.+)\))?$/); - if (!match) { - return undefined; - } - - return { - filePath: match[1], - line: Number(match[2]), - symbol: match[3], - note: match[4] ?? "" - }; -} - -function categoryFor(entry) { - if (entry.note === "used in module") { - return "Internal type/export used in defining module"; - } - if (/^(shared\/types|shared\/logger|core\/workflow_registry|core\/swarm_runtime|core\/ai_router)/.test(entry.filePath)) { - return "Likely public API or cross-module contract"; - } - if (/^(orchestrator|governance)\//.test(entry.filePath)) { - return "Review candidate: orchestrator/governance"; - } - return "Review candidate"; -} - -function render(entries) { - const generatedAt = new Date().toISOString(); - const groups = entries.reduce((accumulator, entry) => { - const category = categoryFor(entry); - const current = accumulator.get(category) ?? []; - current.push(entry); - accumulator.set(category, current); - return accumulator; - }, new Map()); - - return `# Unused Exports Review - -- Generated: ${generatedAt} -- Source: \`npx ts-prune\` -- Total entries: ${entries.length} - -This report is review-only. Do not delete exports in bulk: entries marked as used in module, shared contracts, or CLI-facing APIs can be intentional public surface. - -${[...groups.entries()] - .map( - ([category, items]) => `## ${category} - -${items.map((item) => `- ${item.filePath}:${item.line} - ${item.symbol}${item.note ? ` (${item.note})` : ""}`).join("\n") || "- None"}` - ) - .join("\n\n")} -`; -} - -const entries = runTsPrune() - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .map(parseLine) - .filter(Boolean); - -mkdirSync(path.dirname(outputPath), { recursive: true }); -writeFileSync(outputPath, render(entries), "utf8"); -console.log(`Unused exports review: ${outputPath}`); -console.log(`Entries: ${entries.length}`); diff --git a/shared/fs-utils.ts b/shared/fs-utils.ts deleted file mode 100644 index 5d56961..0000000 --- a/shared/fs-utils.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { promises as fs } from "node:fs"; -import path from "node:path"; - -const IGNORED_DIRECTORIES = new Set([ - ".git", - "node_modules", - "dist", - "build", - "coverage", - ".next", - ".nuxt", - ".turbo", - ".claude", - ".project-brain", - ".project-brain-local", - ".idea", - ".vscode", - ".venv", - "venv" -]); - -const ROOT_GENERATED_DIRECTORIES = new Set(["AI_CONTEXT", "reports", "tasks", "patch_proposals", "BRAIN"]); - -const IGNORED_PATH_PATTERNS = [ - /(^|\/)__fixtures__(\/|$)/i, - /(^|\/)(tests?|spec)\/fixtures(\/|$)/i -]; - -async function looksLikeProjectBrainRuntimeMemory(memoryPath: string): Promise { - const generatedMarkers = [ - "memory_brief/memory_brief.json", - "executive_summary/executive_summary.json", - "knowledge_graph/repository_fact_graph.json", - "code_graph/code_graph_v2.json", - "firewall/agent_firewall.json" - ]; - - const markerChecks = await Promise.all(generatedMarkers.map((marker) => fileExists(path.join(memoryPath, marker)))); - return markerChecks.some(Boolean); -} - -function isDependencyVendorDirectory(relativeDir: string, entryName: string): boolean { - if (entryName !== "vendor") { - return false; - } - - return relativeDir === "" || relativeDir === "core" || relativeDir === "app" || relativeDir === "default/app"; -} - -export async function fileExists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } -} - -export async function ensureDir(dirPath: string): Promise { - await fs.mkdir(dirPath, { recursive: true }); -} - -export async function readTextSafe(filePath: string): Promise { - try { - return await fs.readFile(filePath, "utf8"); - } catch { - return ""; - } -} - -export async function readJsonSafe(filePath: string): Promise { - const content = await readTextSafe(filePath); - if (!content) { - return undefined; - } - - try { - return JSON.parse(content) as T; - } catch { - return undefined; - } -} - -export async function writeFileEnsured(filePath: string, content: string): Promise { - await ensureDir(path.dirname(filePath)); - await fs.writeFile(filePath, content, "utf8"); -} - -export async function writeJsonEnsured(filePath: string, data: unknown): Promise { - await writeFileEnsured(filePath, JSON.stringify(data, null, 2)); -} - -export async function appendFileEnsured(filePath: string, content: string): Promise { - await ensureDir(path.dirname(filePath)); - await fs.appendFile(filePath, content, "utf8"); -} - -export async function walkDirectory( - rootPath: string, - maxFiles = 8000, - excludedPaths: string[] = [], - options: { includeGeneratedArtifacts?: boolean } = {} -): Promise { - const files: string[] = []; - const queue: string[] = [""]; - const normalizedExclusions = excludedPaths.map((value) => toPosixPath(value).replace(/^\.\/+/, "")); - - while (queue.length > 0) { - const relativeDir = queue.shift() ?? ""; - const absoluteDir = relativeDir ? path.join(rootPath, relativeDir) : rootPath; - let entries; - - try { - entries = await fs.readdir(absoluteDir, { withFileTypes: true }); - } catch { - continue; - } - - for (const entry of entries) { - const relativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name; - const normalizedPath = toPosixPath(relativePath); - const isExcluded = normalizedExclusions.some( - (excludedPath) => - excludedPath !== "" && - (normalizedPath === excludedPath || normalizedPath.startsWith(`${excludedPath}/`)) - ); - const matchesIgnoredPattern = IGNORED_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath)); - - if (isExcluded || matchesIgnoredPattern) { - continue; - } - - if (entry.isDirectory()) { - if (!options.includeGeneratedArtifacts && relativeDir === "" && ROOT_GENERATED_DIRECTORIES.has(entry.name)) { - continue; - } - - if ( - !options.includeGeneratedArtifacts && - relativeDir === "" && - entry.name === "memory" && - (await looksLikeProjectBrainRuntimeMemory(path.join(rootPath, relativePath))) - ) { - continue; - } - - if (!options.includeGeneratedArtifacts && relativeDir === "docs" && entry.name === "codebase_map") { - continue; - } - - if (isDependencyVendorDirectory(toPosixPath(relativeDir), entry.name)) { - continue; - } - - if (IGNORED_DIRECTORIES.has(entry.name)) { - continue; - } - - queue.push(relativePath); - continue; - } - - files.push(normalizedPath); - - if (files.length >= maxFiles) { - return files.sort(); - } - } - } - - return files.sort(); -} - -export function toPosixPath(value: string): string { - return value.split(path.sep).join("/"); -} - -export function uniqueSorted(values: string[]): string[] { - return [...new Set(values)].sort((left, right) => left.localeCompare(right)); -} - -export function relativeTo(basePath: string, targetPath: string): string { - return toPosixPath(path.relative(basePath, targetPath) || "."); -} diff --git a/shared/logger.ts b/shared/logger.ts deleted file mode 100644 index ad87806..0000000 --- a/shared/logger.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { - createCycleId, - getLoggerOptions, - setLoggerOptions, - StructuredLogger, - withLogContext -} from "./logger/logger"; diff --git a/shared/logger/logger.ts b/shared/logger/logger.ts deleted file mode 100644 index 9c06219..0000000 --- a/shared/logger/logger.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; - -type LogLevel = "info" | "warn" | "error" | "debug"; - -export interface LogBindings { - component?: string; - agent?: string | null; - action?: string | null; - cycleId?: string | null; -} - -export interface StructuredLogEvent extends LogBindings { - timestamp: string; - level: LogLevel; - message: string; - [key: string]: unknown; -} - -interface LoggerOptions { - verbose: boolean; -} - -const runtimeContext = new AsyncLocalStorage(); -const loggerOptions: LoggerOptions = { - verbose: false -}; - -function normalizeBindings(bindings?: LogBindings): LogBindings { - return { - component: bindings?.component, - agent: bindings?.agent ?? null, - action: bindings?.action ?? null, - cycleId: bindings?.cycleId ?? null - }; -} - -function currentBindings(): LogBindings { - return normalizeBindings(runtimeContext.getStore()); -} - -export function setLoggerOptions(options: Partial): void { - if (typeof options.verbose === "boolean") { - loggerOptions.verbose = options.verbose; - } -} - -export function getLoggerOptions(): LoggerOptions { - return { ...loggerOptions }; -} - -export async function withLogContext(bindings: LogBindings, run: () => Promise): Promise { - const merged = { - ...currentBindings(), - ...normalizeBindings(bindings) - }; - - return runtimeContext.run(merged, run); -} - -export function createCycleId(prefix: string): string { - return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; -} - -export class StructuredLogger { - constructor( - private readonly component: string, - private readonly defaults: LogBindings = {} - ) {} - - child(bindings: string | LogBindings): StructuredLogger { - if (typeof bindings === "string") { - return new StructuredLogger(`${this.component}:${bindings}`, this.defaults); - } - - return new StructuredLogger(this.component, { - ...this.defaults, - ...bindings - }); - } - - info(message: string, meta: Record = {}): void { - this.emit("info", message, meta); - } - - warn(message: string, meta: Record = {}): void { - this.emit("warn", message, meta); - } - - error(message: string, meta: Record = {}): void { - this.emit("error", message, meta); - } - - debug(message: string, meta: Record = {}): void { - this.emit("debug", message, meta); - } - - private emit(level: LogLevel, message: string, meta: Record): void { - const runtime = currentBindings(); - const payload: StructuredLogEvent = { - timestamp: new Date().toISOString(), - level, - component: String(meta.component ?? this.defaults.component ?? this.component), - agent: (meta.agent as string | null | undefined) ?? this.defaults.agent ?? runtime.agent ?? null, - action: (meta.action as string | null | undefined) ?? this.defaults.action ?? runtime.action ?? null, - cycleId: (meta.cycleId as string | null | undefined) ?? this.defaults.cycleId ?? runtime.cycleId ?? null, - message - }; - - for (const [key, value] of Object.entries(meta)) { - if (!["component", "agent", "action", "cycleId"].includes(key)) { - payload[key] = value; - } - } - - if (loggerOptions.verbose) { - process.stderr.write(`${JSON.stringify(payload)}\n`); - } - } -} diff --git a/shared/types.ts b/shared/types.ts deleted file mode 100644 index b69e54e..0000000 --- a/shared/types.ts +++ /dev/null @@ -1,1312 +0,0 @@ -export type RiskLevel = "low" | "medium" | "high"; -export type AgentAction = "analyze" | "propose" | "report"; -export type FirewallPolicyPack = "safe-readonly" | "review" | "edit-limited" | "deploy"; -export type FirewallDecision = "ALLOW" | "ALLOW_WITH_REVIEW" | "BLOCKED"; -export type ContextTrustLevel = "official" | "maintainer" | "community"; -export type AskWorkflow = - | "resume-project" - | "discover-project" - | "security-audit" - | "critical-gaps" - | "review-latest-changes" - | "inspect-firewall" - | "build-code-graph"; -export type FirewallTool = - | "read-repository" - | "read-generated-context" - | "write-generated-artifacts" - | "run-tests" - | "run-build" - | "write-target-files" - | "delete-target-files" - | "read-git" - | "write-git" - | "network-egress" - | "deploy"; -export type FirewallToolMode = "allow" | "approval-required" | "deny"; -export type GovernanceTrigger = - | "manual" - | "repository-change" - | "weekly-review" - | "security-audit" - | "architecture-review" - | "incident-detection" - | "dependency-update" - | "security-advisory"; -export type AgentMessageType = "ANALYSIS_RESULT" | "PROPOSAL" | "QUESTION" | "FEEDBACK" | "ESCALATION"; -export type TaskState = "NEW" | "ANALYZING" | "PROPOSED" | "APPROVED" | "REJECTED" | "ARCHIVED"; -export type ProposalStatus = "APPROVED" | "REQUIRES_HUMAN_REVIEW" | "REJECTED"; -export type WorkflowStage = "ANALYZE" | "PROPOSE" | "PROPOSE_PATCHES" | "REPORT"; -export type ProposalConsensusState = "strong" | "moderate" | "weak"; -export type CodeGraphNodeKind = - | "file" - | "function" - | "class" - | "method" - | "variable" - | "interface" - | "type" - | "enum" - | "test"; -export type CodeGraphEdgeKind = "imports" | "contains" | "calls"; -export type RepositoryFactGraphNodeKind = - | "repository" - | "directory" - | "file" - | "symbol" - | "language" - | "framework" - | "manifest" - | "api_surface" - | "infra_surface"; -export type RepositoryFactGraphEdgeKind = - | "contains" - | "uses_language" - | "uses_framework" - | "has_manifest" - | "exposes_api" - | "defines_infra" - | "declares" - | "imports" - | "calls"; -export type LearningOutcome = - | "SUCCESSFUL_PROPOSAL" - | "REJECTED_PROPOSAL" - | "FALSE_POSITIVE" - | "MISSED_ISSUE" - | "ARCHITECTURAL_INSIGHT" - | "REPEATED_BUG_PATTERN" - | "PENDING_REVIEW"; -export type AgentPriority = "critical" | "high" | "normal" | "low"; -export type SecurityFindingSeverity = "critical" | "high" | "medium" | "low" | "info"; -export type SecurityFindingProblemType = "code" | "configuration" | "architecture" | "code+configuration"; -export type SecurityFixEffort = "low" | "medium" | "high"; -export type ProjectSeedArchetype = - | "saas-webapp" - | "marketing-site" - | "mobile-app" - | "api-backend" - | "internal-tool" - | "content-platform" - | "custom"; -export type ProjectSeedPriority = "mvp-fast" | "solid-architecture" | "low-cost" | "security-first"; -export type SecurityAuditArea = - | "auth_sessions" - | "authorization" - | "input_validation" - | "web_attacks" - | "http_headers" - | "infra_config" - | "abuse_protection" - | "sensitive_data" - | "observability"; - -export interface SecurityFinding { - area: SecurityAuditArea; - severity: SecurityFindingSeverity; - title: string; - location: string; - evidence: string; - attackVector: string[]; - impact: string; - fix: string; - references: string[]; - effort: SecurityFixEffort; - problemType: SecurityFindingProblemType; - agentId: string; -} - -export interface SecurityCoverageStatus { - area: SecurityAuditArea; - status: "finding" | "ok" | "not-reviewed"; - note: string; - agentId?: string; -} - -export interface VerifiedAppContext { - architectureSummary: string[]; - attackSurface: string[]; - criticalAssets: string[]; - trustBoundaries: string[]; - contextGaps: string[]; -} - -export interface RepoStructure { - topLevelDirectories: string[]; - sampleFiles: string[]; - subrepos: string[]; - submodules: string[]; - fileCount: number; - sourceFileCount: number; - testFileCount: number; -} - -export interface BasicRepoScan { - repoName: string; - targetPath: string; - scannedAt: string; - files: string[]; - languages: string[]; - structure: RepoStructure; -} - -export interface DependencyManifest { - path: string; - ecosystem: string; - dependencies: string[]; -} - -export interface DependencyScanResult { - manifests: string[]; - dependencies: DependencyManifest[]; - frameworks: string[]; - testing: string[]; -} - -export interface ApiScanResult { - apis: string[]; - apiFiles: string[]; -} - -export interface InfraScanResult { - infrastructure: string[]; - infraFiles: string[]; - dockerStageCount: number; -} - -export interface GitInfo { - isGitRepo: boolean; - branch?: string; - latestCommit?: string; - hasSubmodules: boolean; -} - -export interface CiInfo { - providers: string[]; - configFiles: string[]; -} - -export interface LoggingInfo { - frameworks: string[]; - configFiles: string[]; - structured: boolean; -} - -export interface MetricsInfo { - tools: string[]; - configFiles: string[]; - alertsConfigured: boolean; -} - -export interface DiscoveryResult { - repoName: string; - targetPath: string; - scannedAt: string; - files: string[]; - structure: RepoStructure; - languages: string[]; - frameworks: string[]; - apis: string[]; - infrastructure: string[]; - testing: string[]; - dependencies: DependencyManifest[]; - manifests: string[]; - apiFiles: string[]; - infraFiles: string[]; - dockerStageCount: number; - git: GitInfo; - ci: CiInfo; - logging: LoggingInfo; - metrics: MetricsInfo; - recommendations: string[]; -} - -export interface ProjectContext { - repoName: string; - targetPath: string; - outputPath: string; - scannedAt: string; - discovery: DiscoveryResult; - memoryDir: string; - reportsDir: string; - docsDir: string; - runtimeMemoryDir: string; - learningDir: string; - taskBoardDir: string; - proposalDir: string; - patchProposalDir: string; -} - -export interface RepositoryTarget { - repoName: string; - targetPath: string; - relativePath: string; -} - -export interface AgentReport { - agentId: string; - title: string; - summary: string; - findings: string[]; - recommendations: string[]; - riskLevel: RiskLevel; - outputPath: string; - securityFindings?: SecurityFinding[]; - coverage?: SecurityCoverageStatus[]; -} - -export interface AgentDescriptor { - agentId: string; - displayName: string; - version: string; - capabilities: string[]; - allowedActions: AgentAction[]; - triggers: GovernanceTrigger[]; - requiresHumanApprovalFor: string[]; -} - -export interface AgentTask { - taskId: string; - agentId: string; - title: string; - description: string; - trigger: GovernanceTrigger; - priority: AgentPriority; - state: TaskState; - createdAt: string; - claimedAt?: string; - completedAt?: string; - rationale: string; - reportPath?: string; -} - -export interface FirewallToolRule { - tool: FirewallTool; - mode: FirewallToolMode; - rationale: string; -} - -export interface AgentTaskPacket { - taskId: string; - agentId: string; - trigger: GovernanceTrigger; - goal: string; - scopePaths: string[]; - contextPaths: string[]; - constraints: string[]; - expectedOutput: string[]; - policyPack: FirewallPolicyPack; - riskLevel: RiskLevel; - decision: FirewallDecision; - decisionRationale: string; - requiresHumanApproval: boolean; - requiredApprovals: string[]; - toolRules: FirewallToolRule[]; - packetPath: string; -} - -export interface FirewallSummary { - generatedAt: string; - trigger: GovernanceTrigger; - reportPath: string; - policyPath: string; - packetDir: string; - packets: AgentTaskPacket[]; - stats: { - allowed: number; - reviewRequired: number; - blocked: number; - lowRisk: number; - mediumRisk: number; - highRisk: number; - byPolicyPack: Record; - }; -} - -export interface AskRoute { - workflow: AskWorkflow; - reason: string; - trigger: GovernanceTrigger; - followUps: string[]; -} - -export interface AskArtifact { - label: string; - path: string; -} - -export interface AskResult { - intent: string; - workflow: AskWorkflow; - targetPath: string; - outputPath: string; - scopeMode: "repository" | "workspace"; - briefPath: string; - headline: string; - summary: string[]; - artifacts: AskArtifact[]; - followUps: string[]; - routingReason: string; - preflightFacts?: PreflightFactsResult; - guidedExecution?: { - label: string; - command: string; - headline: string; - summary: string[]; - artifacts: AskArtifact[]; - }; - aiAssistance?: { - provider: string; - model: string; - profile: string; - residency: string; - summary: string[]; - suggestedWorkflow?: AskWorkflow; - }; -} - -export type PreflightFactsConfidence = "none" | "low" | "medium" | "high"; -export type PreflightFactsNextAction = - | "answer-from-memory" - | "run-code-graph" - | "run-fact-query" - | "run-swarm-delta" - | "continue-workflow"; - -export interface PreflightFactsResult { - intent: string; - scope?: string; - query: string; - factsFound: boolean; - facts: string[]; - evidence: string[]; - freshness: { - freshScopes: string[]; - staleScopes: string[]; - missingScopes: string[]; - }; - staleIgnored: string[]; - confidence: PreflightFactsConfidence; - recommendedNextAction: PreflightFactsNextAction; - readiness: { - hasMemoryBrief: boolean; - hasExecutiveSummary: boolean; - hasFactGraph: boolean; - hasFreshScopeMemory: boolean; - }; - sources: { - memoryBriefPath: string; - memoryBriefJsonPath: string; - executiveSummaryPath: string; - executiveSummaryJsonPath: string; - repositoryFactGraphPath: string; - scopeMemoryDir: string; - }; - unknowns: string[]; -} - -export interface SwarmPlanTask { - taskId: string; - title: string; - goal: string; - profile: "worker" | "reviewer" | "reasoning" | "planner" | "synthesizer"; - deliverable: string; - dependsOn?: string[]; -} - -export interface SwarmWorkerResult { - taskId: string; - parentTaskId: string; - chunkId: string; - attempt: number; - status: "completed" | "timed_out" | "failed"; - title: string; - profile: "worker" | "reviewer" | "reasoning" | "planner" | "synthesizer"; - scopePaths: string[]; - provider: string; - model: string; - residency: string; - summary: string; - findings: string[]; - recommendations: string[]; - verifiedFacts?: string[]; - unknowns?: string[]; - evidenceRefs?: string[]; - error?: string; -} - -export interface ScopeMemoryFileHash { - path: string; - sha256?: string; - missing?: boolean; -} - -export interface ScopeMemoryRecord { - version: 1; - repoName: string; - targetPath: string; - scope: string; - scopeKey: string; - updatedAt: string; - generatedBy: { - command: "swarm"; - intent: string; - provider?: string; - model?: string; - }; - files: { - count: number; - totalCount?: number; - hashTruncated?: boolean; - hashed: ScopeMemoryFileHash[]; - }; - coverage: { - status: "complete" | "partial" | "failed" | "timed_out"; - workerTaskIds: string[]; - completedWorkers: number; - failedWorkers: number; - timedOutWorkers: number; - }; - freshness: { - status: "fresh" | "stale"; - changedFiles: string[]; - missingFiles: string[]; - unchangedFiles: number; - }; - decisions: string[]; - verifiedFacts: string[]; - unknowns: string[]; - evidenceRefs: string[]; - nextActions: string[]; - sourceArtifacts: string[]; -} - -export interface ScopeMemoryLookupResult { - records: ScopeMemoryRecord[]; - hits: number; - misses: number; - stale: number; -} - -export type SwarmEngine = "bounded" | "deepagents"; - -export interface SwarmRunResult { - engine: SwarmEngine; - context: ProjectContext; - intent: string; - reportPath: string; - memoryPath: string; - resilience: { - runTimeoutMs: number; - requestedRunTimeoutMs?: number; - plannerTimeoutMs: number; - requestedPlannerTimeoutMs?: number; - synthesisTimeoutMs: number; - requestedSynthesisTimeoutMs?: number; - taskTimeoutMs: number; - requestedTaskTimeoutMs?: number; - maxRetries: number; - queueBudget: number; - requestedQueueBudget?: number; - plannerTimedOut: boolean; - synthesisTimedOut: boolean; - runTimedOut: boolean; - timedOutTasks: number; - retriedTasks: number; - splitTasks: number; - failedTasks: number; - droppedTasks: number; - localBudgetMode: boolean; - adaptiveQueueBudget: boolean; - }; - chunking: { - selectedChunkSize: number; - requestedChunkSize?: number; - scopeUnits: number; - scopeChunks: number; - queuedTasks: number; - queueStrategy: "round-robin"; - scopeBias: "balanced" | "source-first"; - scopeHints: string[]; - }; - parallelism: { - selected: number; - requested?: number; - cpuCount: number; - loadAverage1m: number; - freeMemoryMb: number; - totalMemoryMb: number; - pressure: "low" | "medium" | "high"; - }; - planner: { - provider: string; - model: string; - residency: string; - overview: string; - }; - optimization?: { - cacheHits: number; - cacheMisses: number; - cacheWrites: number; - scopeMemoryHits?: number; - scopeMemoryMisses?: number; - scopeMemoryStale?: number; - scopeMemoryWrites?: number; - scopeMemoryReuseCandidates?: number; - scopeMemoryReductionHints?: string[]; - derivedTasksQueued: number; - derivedTasksSkipped: number; - learnedScopeBoosts: string[]; - }; - tasks: SwarmPlanTask[]; - workerResults: SwarmWorkerResult[]; - synthesis: { - provider: string; - model: string; - residency: string; - headline: string; - summary: string; - priorities: string[]; - nextSteps: string[]; - verifiedFacts?: string[]; - unknowns?: string[]; - evidenceRefs?: string[]; - }; -} - -export type DoctorCheckStatus = "pass" | "warn" | "fail"; -export type SuggestedActionPriority = "high" | "medium" | "low"; -export type DoctorSetupTier = "required" | "recommended" | "optional"; -export type DoctorSetupStatus = "installed" | "missing"; - -export interface DoctorCheck { - id: string; - label: string; - status: DoctorCheckStatus; - summary: string; - details: string[]; -} - -export interface DoctorSetupItem { - id: string; - label: string; - tier: DoctorSetupTier; - status: DoctorSetupStatus; - summary: string; - installHint: string; - details: string[]; -} - -export interface SuggestedAction { - label: string; - command: string; - rationale: string; - priority: SuggestedActionPriority; -} - -export interface DoctorResult { - context: ProjectContext; - reportPath: string; - memoryPath: string; - summary: { - passed: number; - warnings: number; - failed: number; - headline: string; - }; - checks: DoctorCheck[]; - setupItems: DoctorSetupItem[]; - suggestions: SuggestedAction[]; -} - -export interface StatusArtifactSummary { - label: string; - path: string; - exists: boolean; - updatedAt?: string; -} - -export type MemoryReadinessStatus = "ready" | "missing" | "stale" | "invalid"; - -export interface MemoryReadinessResult { - status: MemoryReadinessStatus; - memoryBriefPath: string; - memoryBriefJsonPath: string; - generatedAt?: string; - ageHours?: number; - maxAgeHours: number; - factsCount: number; - evidenceCount: number; - tokenGuidanceCount: number; - reason: string; -} - -export interface ExecutiveSummaryScopeStatus { - scope: string; - freshness: "fresh" | "stale"; - coverage: "complete" | "partial" | "failed" | "timed_out"; - facts: number; - unknowns: number; - evidenceRefs: number; - hashTruncated: boolean; -} - -export interface ExecutiveSummaryResult { - context: ProjectContext; - generatedAt: string; - reportPath: string; - memoryPath: string; - identity: { - repoName: string; - targetPath: string; - outputPath: string; - projectType: string; - }; - stack: { - languages: string[]; - frameworks: string[]; - apis: string[]; - infrastructure: string[]; - testing: string[]; - }; - architecture: { - topLevelDirectories: string[]; - sourceFileCount: number; - testFileCount: number; - }; - status: { - scopeCount: number; - completeFreshScopes: number; - staleScopes: number; - partialScopes: number; - latestSwarmIntent?: string; - latestSwarmHeadline?: string; - }; - decisions: string[]; - learnings: string[]; - risksAndUnknowns: string[]; - scopeStatuses: ExecutiveSummaryScopeStatus[]; - nextActions: string[]; - evidenceRefs: string[]; -} - -export interface StatusResult { - context: ProjectContext; - reportPath: string; - memoryPath: string; - git: { - isGitRepo: boolean; - branch?: string; - }; - summary: { - headline: string; - artifactCount: number; - doctorStatus: DoctorCheckStatus | "unknown"; - swarmStatus: "available" | "missing"; - planStatus: "available" | "missing"; - }; - memoryReadiness: MemoryReadinessResult; - executiveSummary: ExecutiveSummaryResult; - artifacts: StatusArtifactSummary[]; - suggestions: SuggestedAction[]; -} - -export type ResumeStage = - | "bootstrap" - | "start" - | "doctor" - | "ask" - | "map-codebase" - | "fact-query" - | "runbook" - | "harness-audit" - | "firewall" - | "review-delta" - | "swarm" - | "plan-improvements"; - -export interface ArchitecturePlanResult { - context: ProjectContext; - planDir: string; - blueprintPath: string; - statePath: string; - claudeContextPath: string; - memoryPath: string; -} - -export interface ProjectSeedInput { - projectName: string; - problem: string; - audience: string; - archetype: ProjectSeedArchetype; - stackPreference: string; - features: string[]; - authRequired: boolean; - roles: string[]; - dataEntities: string[]; - integrations: string[]; - priority: ProjectSeedPriority; - language: string; - notes: string[]; - contextOnly: boolean; - overwrite?: boolean; -} - -export interface ProjectSeedResult { - targetPath: string; - projectName: string; - archetype: ProjectSeedArchetype; - contextOnly: boolean; - artifactPaths: { - projectCharterPath: string; - requirementsPath: string; - blueprintPath: string; - decisionsPath: string; - memoryBriefPath: string; - runbookPath: string; - architectureBlueprintPath: string; - architectureStatePath: string; - projectSeedMemoryPath: string; - backlogPath: string; - claudePath: string; - }; - nextSteps: string[]; -} - -export interface ResumeResult { - context: ProjectContext; - reportPath: string; - memoryPath: string; - git: { - isGitRepo: boolean; - branch?: string; - }; - summary: { - headline: string; - stage: ResumeStage; - artifactCount: number; - latestArtifactLabel?: string; - latestArtifactUpdatedAt?: string; - }; - latestArtifact?: StatusArtifactSummary; - memoryReadiness: MemoryReadinessResult; - executiveSummary: ExecutiveSummaryResult; - artifacts: StatusArtifactSummary[]; - notes: string[]; - suggestions: SuggestedAction[]; -} - -export interface StartStep { - id: string; - label: string; - status: "done" | "skipped" | "suggested"; - command: string; - summary: string; -} - -export interface StartResult { - context: ProjectContext; - intent: string; - reportPath: string; - memoryPath: string; - headline: string; - memoryReadiness: MemoryReadinessResult; - executiveSummary: ExecutiveSummaryResult; - executedSteps: StartStep[]; - nextCommand?: string; - artifacts: StatusArtifactSummary[]; - suggestions: SuggestedAction[]; -} - -export interface ImprovementPlanResult { - context: ProjectContext; - planDir: string; - summaryPath: string; - statePath: string; - risksPath: string; - roadmapPath: string; - tracksPath: string; -} - -export interface SecurityAuditResult { - context: ProjectContext; - trigger: GovernanceTrigger; - reportPath: string; - memoryPath: string; - contextLiteReportPath?: string; - verifiedContext: VerifiedAppContext; - findings: SecurityFinding[]; - coverage: SecurityCoverageStatus[]; - checklist: string[]; - securityDebt: string[]; - sourceReports: string[]; - verdict: "No apta para producción" | "Apta con remediaciones obligatorias" | "Apta con hardening recomendado"; - headline: string; -} - -export interface ContextRegistryEntry { - id: string; - title: string; - category: string; - trustLevel: ContextTrustLevel; - source: string; - sourceUrl: string; - summary: string; - tags: string[]; - guidance: string[]; - relatedIds: string[]; -} - -export interface ContextSearchHit { - entry: ContextRegistryEntry; - score: number; - matchedTags: string[]; -} - -export interface ContextSearchResult { - context: ProjectContext; - query: string; - reportPath: string; - cachePath: string; - hits: ContextSearchHit[]; -} - -export interface ContextGetResult { - context: ProjectContext; - entry: ContextRegistryEntry; - artifactPath: string; - cachePath: string; -} - -export interface ContextSourcesResult { - context: ProjectContext; - reportPath: string; - sources: Array<{ - source: string; - trustLevel: ContextTrustLevel; - entries: number; - }>; -} - -export interface EcosystemRadarCandidate { - entry: ContextRegistryEntry; - repoFullName: string; - bucketId: string; - score: number; - stars: number; - forks: number; - primaryLanguage?: string; - pushedAt?: string; - reasons: string[]; -} - -export interface EcosystemRadarResult { - context: ProjectContext; - reportPath: string; - cachePath: string; - candidates: EcosystemRadarCandidate[]; - notes: string[]; -} - -export interface AgentMessage { - messageId: string; - sender: string; - recipient: string; - taskId: string; - type: AgentMessageType; - payload: Record; - priority: AgentPriority; - timestamp: string; -} - -export interface AgentEvaluationScore { - agentId: string; - taskId: string; - outputQuality: number; - proposalQuality: number; - signalStrength: number; - riskAlignment: number; - overallScore: number; - rank: number; - notes: string[]; -} - -export interface LearningRecord { - lessonId: string; - agentId: string; - taskId: string; - context: string; - detectedProblem: string; - actionTaken: string; - outcome: LearningOutcome; - confidenceScore: number; - createdAt: string; -} - -export interface ProposalArtifact { - proposalId: string; - agentId: string; - title: string; - summary: string; - status: ProposalStatus; - consensusScore: number; - consensusState: ProposalConsensusState; - supportingAgents: string[]; - consensusThemes: string[]; - filePath: string; - riskLevel: RiskLevel; - affectedFiles: string[]; - expectedBenefit: string; - implementationSketch: string; - decisionRationale: string; - sourceReportPath: string; - createdAt: string; -} - -export interface PatchProposalArtifact { - patchId: string; - agentId: string; - stage: WorkflowStage; - title: string; - filePath: string; - targetFile: string; - sourceTaskPath: string; - riskLevel: RiskLevel; - effort: "Low" | "Medium" | "High"; - requiresHumanApproval: boolean; - createdAt: string; -} - -export interface AgentExecutionRecord { - agentId: string; - taskId: string; - startedAt: string; - completedAt?: string; - status: "running" | "completed" | "failed"; - error?: string; -} - -export interface GovernanceSummary { - trigger: GovernanceTrigger; - tasks: AgentTask[]; - messages: AgentMessage[]; - evaluations: AgentEvaluationScore[]; - learnings: LearningRecord[]; - proposals: ProposalArtifact[]; - patchProposals?: PatchProposalArtifact[]; - executionRecords: AgentExecutionRecord[]; - agentActivityReportPath: string; - improvementReportPath: string; - firewall?: FirewallSummary; -} - -export interface AgentEvaluation { - title: string; - summary: string; - findings: string[]; - recommendations: string[]; - riskLevel: RiskLevel; - deterministicFindings?: string[]; - aiInsights?: string[]; - combinedRecommendations?: string[]; - content?: string; - securityFindings?: SecurityFinding[]; - coverage?: SecurityCoverageStatus[]; -} - -export interface ReportManifest { - memoryFiles: string[]; - reportFiles: string[]; - docFiles: string[]; - learningFiles: string[]; - taskFiles: string[]; - swarmFiles?: string[]; - firewallFiles?: string[]; - securityFiles?: string[]; - contextRegistryFiles?: string[]; - proposalFiles: string[]; - knowledgeFiles?: string[]; - patchProposalFiles?: string[]; -} - -export interface CodebaseMapArtifact { - repoName: string; - outputPath: string; - codebaseMapDir: string; - files: string[]; - summaryPath: string; -} - -export interface ContextAnnotation { - scope: string; - note: string; - createdAt: string; - updatedAt: string; -} - -export interface CodeGraphSymbol { - id: string; - name: string; - qualifiedName: string; - kind: CodeGraphNodeKind; - filePath: string; - exported: boolean; - lineStart: number; - lineEnd: number; - parentSymbolId?: string; -} - -export interface CodeGraphEdge { - kind: CodeGraphEdgeKind; - from: string; - to: string; - filePath: string; - line: number; -} - -export interface CodeGraphFileRecord { - filePath: string; - hash: string; - language: string; - isTest: boolean; - imports: string[]; - symbols: CodeGraphSymbol[]; - edges: CodeGraphEdge[]; -} - -export interface CodeGraphDocument { - version: 2; - generatedAt: string; - targetPath: string; - nodes: string[]; - edges: CodeGraphEdge[]; - files: CodeGraphFileRecord[]; - symbols: CodeGraphSymbol[]; - build: { - mode: "full" | "incremental"; - updatedFiles: string[]; - removedFiles: string[]; - unchangedFiles: number; - }; - stats: { - files: number; - symbols: number; - nodes: number; - edges: number; - edgeKinds: Partial>; - }; -} - -export interface CodeGraphBuildResult { - graphPath: string; - graph: CodeGraphDocument; - factGraphPath?: string; - factReportPath?: string; - factGraph?: RepositoryFactGraphDocument; -} - -export interface RepositoryFactGraphNode { - id: string; - label: string; - kind: RepositoryFactGraphNodeKind; - attributes?: Record; -} - -export interface RepositoryFactGraphEdge { - kind: RepositoryFactGraphEdgeKind; - from: string; - to: string; - evidencePath?: string; - line?: number; -} - -export interface RepositoryFactGraphDocument { - version: 1; - generatedAt: string; - targetPath: string; - repoName: string; - nodes: RepositoryFactGraphNode[]; - edges: RepositoryFactGraphEdge[]; - stats: { - nodes: number; - edges: number; - codeGraphFiles: number; - codeGraphSymbols: number; - nodeKinds: Partial>; - edgeKinds: Partial>; - }; -} - -export interface ImpactAnalysisResult { - targetPath: string; - outputPath: string; - changedFiles: string[]; - directDependents: string[]; - transitiveDependents: string[]; - impactedTests: string[]; - reviewFiles: string[]; - unresolvedImports: string[]; - graphPath: string; - reportPath: string; - graphStats: { - nodes: number; - edges: number; - files: number; - symbols: number; - buildMode: "full" | "incremental"; - updatedFiles: number; - }; -} - -export interface CodebaseMapResult extends CodebaseMapArtifact { - context: ProjectContext; -} - -export interface ContextLiteResult { - context: ProjectContext; - reportPath: string; - artifactPaths: string[]; - summary: string[]; - openQuestions: string[]; -} - -export interface FactQueryResult { - query: string; - answer: string; - tokens: string[]; - reportPath: string; - memoryPath: string; - sources: { - memoryBriefPath: string; - memoryBriefJsonPath: string; - repositoryFactGraphPath: string; - scopeMemoryDir?: string; - }; - scopeMemoryMatches?: Array<{ - scope: string; - kind: string; - text: string; - score: number; - evidenceRefs: string[]; - }>; - memoryMatches: Array<{ - kind: string; - text: string; - score: number; - }>; - nodeMatches: Array<{ - id: string; - kind: RepositoryFactGraphNodeKind; - label: string; - score: number; - attributes?: Record; - }>; - edgeMatches: Array<{ - kind: RepositoryFactGraphEdgeKind; - from: string; - to: string; - evidencePath?: string; - line?: number; - score: number; - }>; - evidenceRefs: string[]; - unknowns: string[]; -} - -export interface RunbookStep { - id: string; - title: string; - status: "pending" | "ready" | "blocked" | "done"; - command: string; - rationale: string; - cheap: boolean; - usesModel: boolean; - evidence: string[]; -} - -export interface RunbookResult { - context: ProjectContext; - intent: string; - generatedAt: string; - reportPath: string; - memoryPath: string; - executiveSummary: ExecutiveSummaryResult; - steps: RunbookStep[]; -} - -export interface HarnessAuditCheck { - id: string; - label: string; - status: "pass" | "warn" | "fail"; - summary: string; - evidence: string[]; - recommendation?: string; -} - -export interface HarnessAuditMemoryLayer { - id: string; - label: string; - status: "ready" | "partial" | "missing"; - tokenCost: "low" | "medium" | "high"; - artifacts: string[]; - purpose: string; -} - -export interface HarnessAuditResult { - context: ProjectContext; - generatedAt: string; - reportPath: string; - memoryPath: string; - score: number; - tokenRisk: "low" | "medium" | "high"; - memoryReadiness: MemoryReadinessResult; - checks: HarnessAuditCheck[]; - memoryLayers: HarnessAuditMemoryLayer[]; - suggestedCommands: string[]; -} - -export interface EcosystemCodebaseMapRepositoryResult extends CodebaseMapArtifact { - relativePath: string; - targetPath: string; -} - -export interface EcosystemCodebaseMapResult { - rootPath: string; - outputPath: string; - repositories: EcosystemCodebaseMapRepositoryResult[]; - summaryPath: string; -} - -export interface OrchestrationResult { - context: ProjectContext; - agentReports: AgentReport[]; - weeklyReportPath: string; - riskReportPath: string; - reportQualityPath?: string; - governanceSummary?: GovernanceSummary; -} - -export interface FirewallInspectionResult { - context: ProjectContext; - firewall: FirewallSummary; -} - -export interface EcosystemRepositoryResult { - repoName: string; - relativePath: string; - targetPath: string; - outputPath: string; - result: OrchestrationResult; -} - -export interface EcosystemAnalysisResult { - rootPath: string; - outputPath: string; - trigger: GovernanceTrigger; - repositories: EcosystemRepositoryResult[]; - knowledgeGraphPath: string; - ecosystemReportPath: string; - telemetryPath: string; - runtimeObservabilityPath: string; - proposalPaths: string[]; -} diff --git a/src/cli.mjs b/src/cli.mjs new file mode 100644 index 0000000..e06c165 --- /dev/null +++ b/src/cli.mjs @@ -0,0 +1,75 @@ +import { doctorRepository } from "./doctor.mjs"; +import { initRepository } from "./init.mjs"; +import { syncRepository } from "./sync.mjs"; + +export const VERSION = "0.3.0"; + +const HELP = `Project Brain Lite ${VERSION} + +Uso: + brain init [ruta] crea el contexto mínimo sin sobrescribirlo + brain sync [ruta] refresca únicamente hechos verificables + brain doctor [ruta] valida tamaño, enlaces, duplicados y datos sensibles + +Opciones: + --json salida estructurada + -h, --help muestra esta ayuda + -v, --version muestra la versión`; + +function parse(args) { + const unknownOption = args.find((value) => value.startsWith("-") && value !== "--json"); + if (unknownOption) throw new Error(`Opción desconocida: ${unknownOption}. Usa brain --help.`); + const json = args.includes("--json"); + const values = args.filter((value) => value !== "--json"); + if (values.length > 2) throw new Error("Se esperaba como máximo un comando y una ruta."); + return { command: values[0], target: values[1] ?? ".", json }; +} + +function printResult(io, json, value, human) { + io.log(json ? JSON.stringify(value, null, 2) : human); +} + +export async function runCli(args = process.argv.slice(2), io = console) { + const wantsJson = args.includes("--json"); + if (args.length === 0 || args.includes("-h") || args.includes("--help")) { + io.log(HELP); + return 0; + } + if (args.includes("-v") || args.includes("--version")) { + io.log(VERSION); + return 0; + } + + try { + const { command, target, json } = parse(args); + if (command === "init") { + const result = await initRepository(target); + printResult(io, json, result, `Contexto listo: ${result.created.length} creados, ${result.preserved.length} preservados.`); + return 0; + } + if (command === "sync") { + const result = await syncRepository(target); + printResult(io, json, result, result.changed ? "Hechos verificables actualizados." : "El contexto ya estaba actualizado."); + return 0; + } + if (command === "doctor") { + const result = await doctorRepository(target); + if (json) io.log(JSON.stringify(result, null, 2)); + else { + for (const issue of result.errors) io.error(`ERROR ${issue.code}: ${issue.message}`); + for (const issue of result.warnings) io.warn(`AVISO ${issue.code}: ${issue.message}`); + io.log(result.ok ? `Contexto sano (${result.checks.length} comprobaciones).` : `Contexto inválido (${result.errors.length} errores).`); + } + return result.ok ? 0 : 1; + } + throw new Error(`Comando desconocido: ${command ?? ""}. Usa brain --help.`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (wantsJson) { + io.log(JSON.stringify({ ok: false, error: { code: "COMMAND_FAILED", message } }, null, 2)); + } else { + io.error(message); + } + return 1; + } +} diff --git a/src/context.mjs b/src/context.mjs new file mode 100644 index 0000000..1ed1cc5 --- /dev/null +++ b/src/context.mjs @@ -0,0 +1,92 @@ +import { END_MARKER, START_MARKER } from "./contract.mjs"; + +function asList(value) { + if (Array.isArray(value)) return value.map(String); + if (value && typeof value === "object") { + return Object.entries(value).map(([name, count]) => `${name} (${count})`); + } + return []; +} + +function singleLine(value) { + return String(value) + .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu, " ") + .replace(/\s+/gu, " ") + .trim(); +} + +function inlineText(value) { + return singleLine(value).replace(/([\\`*_{}\[\]<>#+|])/gu, "\\$1"); +} + +function codeSpan(value) { + const content = singleLine(value); + const longestRun = Math.max(0, ...[...content.matchAll(/`+/gu)].map((match) => match[0].length)); + const fence = "`".repeat(longestRun + 1); + const padded = content.startsWith("`") || content.endsWith("`") ? ` ${content} ` : content; + return `${fence}${padded}${fence}`; +} + +function compact(items, fallback, limit = 8) { + const values = asList(items).map(inlineText); + if (values.length === 0) return fallback; + const visible = values.slice(0, limit); + const remaining = values.length - visible.length; + return `${visible.join(", ")}${remaining > 0 ? `, +${remaining} más` : ""}`; +} + +function markdownLink(relativePath) { + const value = String(relativePath); + const label = inlineText(value); + const target = `../${value.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`; + return `[${label}](${target})`; +} + +function fingerprintLabel(value) { + const fingerprint = String(value ?? "desconocida"); + return fingerprint.startsWith("sha256:") ? fingerprint : `sha256:${fingerprint}`; +} + +export function renderGeneratedBlock(facts) { + const manifests = asList(facts.manifests); + const commands = asList(facts.validationCommands); + const lines = [ + START_MARKER, + "## Hechos verificados del repositorio", + "", + `- Archivos analizados: ${Number(facts.fileCount ?? 0)}`, + `- Huella del inventario: ${codeSpan(fingerprintLabel(facts.fingerprint))}`, + `- Stack: ${compact(facts.stack, "no detectado")}`, + `- Raíces principales: ${compact(facts.roots, "ninguna")}`, + `- Lenguajes: ${compact(facts.languages, "no detectados")}`, + `- Manifiestos: ${manifests.length > 0 ? manifests.map(markdownLink).join(", ") : "ninguno detectado"}`, + "", + "### Comandos de validación detectados", + "" + ]; + + if (commands.length === 0) lines.push("- Ninguno detectado"); + else for (const command of commands) lines.push(`- ${codeSpan(command)}`); + + lines.push(END_MARKER); + return lines.join("\n"); +} + +function occurrences(text, marker) { + return text.split(marker).length - 1; +} + +export function assertGeneratedBlock(text) { + if (occurrences(text, START_MARKER) !== 1 || occurrences(text, END_MARKER) !== 1) { + throw new Error("CONTEXT.md debe contener exactamente un bloque generado por Project Brain."); + } + const start = text.indexOf(START_MARKER); + const end = text.indexOf(END_MARKER, start); + if (end < start) throw new Error("Los marcadores generados de CONTEXT.md están desordenados."); + return { start, end }; +} + +export function replaceGeneratedBlock(text, facts) { + const { start, end } = assertGeneratedBlock(text); + return text.slice(0, start) + renderGeneratedBlock(facts) + text.slice(end + END_MARKER.length); +} diff --git a/src/contract.mjs b/src/contract.mjs new file mode 100644 index 0000000..4a1189d --- /dev/null +++ b/src/contract.mjs @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; + +const schemaUrl = new URL("../schema/context-contract.schema.json", import.meta.url); +export const CONTRACT_SCHEMA = JSON.parse(readFileSync(schemaUrl, "utf8")); + +const value = CONTRACT_SCHEMA.default; +if ( + !value || + value.contractVersion !== 1 || + !Array.isArray(value.files) || + value.files.length !== 5 || + !value.markers?.start || + !value.markers?.end +) { + throw new Error("El contrato de contexto incluido no es válido."); +} + +export const CONTRACT = Object.freeze({ + ...value, + files: Object.freeze([...value.files]), + markers: Object.freeze({ ...value.markers }), + limits: Object.freeze({ ...value.limits }) +}); + +export const REQUIRED_FILES = CONTRACT.files; +export const GENERATED_FILE = CONTRACT.generatedFile; +export const START_MARKER = CONTRACT.markers.start; +export const END_MARKER = CONTRACT.markers.end; +export const LIMITS = CONTRACT.limits; diff --git a/src/doctor.mjs b/src/doctor.mjs new file mode 100644 index 0000000..3f85d63 --- /dev/null +++ b/src/doctor.mjs @@ -0,0 +1,944 @@ +import { lstat, readFile, readdir, realpath } from "node:fs/promises"; +import path from "node:path"; + +import { + END_MARKER, + GENERATED_FILE, + LIMITS, + REQUIRED_FILES, + START_MARKER +} from "./contract.mjs"; +import { insideRoot, managedPath, resolveRoot } from "./fs.mjs"; + +const CHECK_IDS = Object.freeze([ + "canonical-files", + "generated-markers", + "size-limits", + "extra-context-files", + "links", + "duplicates", + "sensitive-data" +]); + +const CONTEXT_DIRECTORY = "AI_CONTEXT"; +const MAX_AUDIT_FILE_BYTES = 1024 * 1024; +const AUDITABLE_EXTRA_NAMES = /(?:\.md|\.markdown|\.txt|\.json|\.ya?ml|\.toml|\.ini|\.cfg|\.conf|\.env)$/iu; +const REQUIRED_CONTEXT_FILES = new Set( + REQUIRED_FILES.filter((file) => file.startsWith(`${CONTEXT_DIRECTORY}/`)) +); + +function compareText(left = "", right = "") { + if (left < right) return -1; + if (left > right) return 1; + return 0; +} + +function compareDiagnostics(left, right) { + return ( + compareText(left.file, right.file) || + (left.line ?? 0) - (right.line ?? 0) || + compareText(left.code, right.code) || + compareText(left.target, right.target) || + compareText(left.relatedFile, right.relatedFile) + ); +} + +function createRecorder() { + const errors = []; + const warnings = []; + const seen = new Set(); + const counts = new Map(CHECK_IDS.map((id) => [id, { errors: 0, warnings: 0 }])); + + function add(kind, check, diagnostic) { + const normalized = { + code: diagnostic.code, + file: diagnostic.file ?? ".", + message: diagnostic.message, + ...diagnostic + }; + const key = [ + kind, + check, + normalized.code, + normalized.file, + normalized.line ?? "", + normalized.target ?? "", + normalized.relatedFile ?? "" + ].join("\u0000"); + if (seen.has(key)) return; + seen.add(key); + (kind === "errors" ? errors : warnings).push(normalized); + counts.get(check)[kind] += 1; + } + + return { + error(check, diagnostic) { + add("errors", check, diagnostic); + }, + warning(check, diagnostic) { + add("warnings", check, diagnostic); + }, + result() { + errors.sort(compareDiagnostics); + warnings.sort(compareDiagnostics); + return { + ok: errors.length === 0, + errors, + warnings, + checks: CHECK_IDS.map((id) => { + const count = counts.get(id); + return { + id, + ok: count.errors === 0, + errors: count.errors, + warnings: count.warnings + }; + }) + }; + } + }; +} + +async function lstatIfPresent(target) { + try { + return await lstat(target); + } catch (error) { + if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return null; + throw error; + } +} + +function lineCount(content) { + if (content.length === 0) return 0; + const lines = content.split(/\r\n|\r|\n/); + if (lines.at(-1) === "") lines.pop(); + return lines.length; +} + +function countOccurrences(content, marker) { + let count = 0; + let offset = 0; + while (offset <= content.length - marker.length) { + const index = content.indexOf(marker, offset); + if (index === -1) break; + count += 1; + offset = index + marker.length; + } + return count; +} + +async function collectExtraContextFiles(root, contextPath, recorder) { + const files = []; + + async function visit(directory, relativeDirectory) { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch (error) { + recorder.error("canonical-files", { + code: "UNREADABLE_CONTEXT_DIRECTORY", + file: relativeDirectory, + message: "No se pudo leer el directorio de contexto.", + reason: error?.code ?? error?.name ?? "UNKNOWN" + }); + return; + } + + entries.sort((left, right) => compareText(left.name, right.name)); + for (const entry of entries) { + const relative = path.posix.join(relativeDirectory.split(path.sep).join("/"), entry.name); + const absolute = managedPath(root, relative); + if (entry.isDirectory()) { + await visit(absolute, relative); + } else { + files.push(relative); + } + } + } + + await visit(contextPath, CONTEXT_DIRECTORY); + return files.sort(compareText); +} + +function markdownLines(content) { + const output = []; + const lines = content.split(/\r\n|\r|\n/); + let fence = null; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const marker = line.match(/^\s*(`{3,}|~{3,})/u)?.[1] ?? null; + if (marker) { + if (!fence) fence = marker[0]; + else if (marker[0] === fence) fence = null; + continue; + } + if (fence) continue; + output.push({ line: index + 1, text: stripInlineCode(line) }); + } + return output; +} + +function stripInlineCode(line) { + const characters = line.split(""); + let offset = 0; + while (offset < line.length) { + if (line[offset] !== "`") { + offset += 1; + continue; + } + let openingLength = 1; + while (line[offset + openingLength] === "`") openingLength += 1; + let candidate = offset + openingLength; + let closing = -1; + while (candidate < line.length) { + if (line[candidate] !== "`") { + candidate += 1; + continue; + } + let candidateLength = 1; + while (line[candidate + candidateLength] === "`") candidateLength += 1; + if (candidateLength === openingLength) { + closing = candidate; + break; + } + candidate += candidateLength; + } + if (closing === -1) { + offset += openingLength; + continue; + } + characters.fill(" ", offset, closing + openingLength); + offset = closing + openingLength; + } + return characters.join(""); +} + +function isEscaped(text, index) { + let slashes = 0; + for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor -= 1) slashes += 1; + return slashes % 2 === 1; +} + +function hasOpeningLabel(text, closer) { + let depth = 0; + for (let cursor = closer - 1; cursor >= 0; cursor -= 1) { + if (isEscaped(text, cursor)) continue; + if (text[cursor] === "]") depth += 1; + else if (text[cursor] === "[") { + if (depth === 0) return true; + depth -= 1; + } + } + return false; +} + +function extractLinks(content) { + const links = []; + const referencePattern = /^\s{0,3}\[[^\]]+\]:\s*(<[^>]+>|(?:\\ |\S)+)/u; + const wikiPattern = /!?\[\[([^\]]+)\]\]/gu; + + function inlineTargets(text) { + const targets = []; + let offset = 0; + while (offset < text.length) { + const opener = text.indexOf("](", offset); + if (opener === -1) break; + if (isEscaped(text, opener) || !hasOpeningLabel(text, opener)) { + offset = opener + 2; + continue; + } + let cursor = opener + 2; + while (/\s/u.test(text[cursor] ?? "")) cursor += 1; + const start = cursor; + + if (text[cursor] === "<") { + cursor += 1; + let escaped = false; + while (cursor < text.length) { + const character = text[cursor]; + if (escaped) escaped = false; + else if (character === "\\") escaped = true; + else if (character === ">") { + cursor += 1; + break; + } + cursor += 1; + } + } else { + let depth = 0; + let escaped = false; + while (cursor < text.length) { + const character = text[cursor]; + if (escaped) escaped = false; + else if (character === "\\") escaped = true; + else if (character === "(") depth += 1; + else if (character === ")") { + if (depth === 0) break; + depth -= 1; + } else if (/\s/u.test(character) && depth === 0) break; + cursor += 1; + } + } + + const target = text.slice(start, cursor); + if (target) targets.push({ target, column: opener }); + offset = Math.max(cursor + 1, opener + 2); + } + return targets; + } + + for (const entry of markdownLines(content)) { + let match; + for (const inline of inlineTargets(entry.text)) { + links.push({ type: "markdown", target: inline.target, line: entry.line, column: inline.column }); + } + + const reference = entry.text.match(referencePattern); + if (reference) { + links.push({ + type: "markdown", + target: reference[1], + line: entry.line, + column: entry.text.indexOf(reference[1]) + }); + } + + wikiPattern.lastIndex = 0; + while ((match = wikiPattern.exec(entry.text))) { + links.push({ type: "wiki", target: match[1], line: entry.line, column: match.index }); + } + } + + links.sort( + (left, right) => + left.line - right.line || left.column - right.column || compareText(left.type, right.type) + ); + return links; +} + +function unwrapLinkTarget(value) { + const trimmed = value.trim(); + const unwrapped = trimmed.startsWith("<") && trimmed.endsWith(">") + ? trimmed.slice(1, -1) + : trimmed; + return unwrapped.replace(/\\ /gu, " "); +} + +function decodeLinkPath(value) { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function stripQueryAndFragment(value) { + const query = value.indexOf("?"); + const fragment = value.indexOf("#"); + const indexes = [query, fragment].filter((index) => index >= 0); + return indexes.length === 0 ? value : value.slice(0, Math.min(...indexes)); +} + +function isNonRelativeLink(target) { + return ( + target === "" || + target.startsWith("#") || + target.startsWith("?") || + target.startsWith("/") || + target.startsWith("//") || + target.startsWith("~") || + /^[a-z][a-z\d+.-]*:/iu.test(target) || + path.win32.isAbsolute(target) + ); +} + +async function physicalTarget(root, target, cache) { + if (!cache.has(target)) { + cache.set(target, (async () => { + try { + const resolved = await realpath(target); + return insideRoot(root, resolved) + ? { ok: true } + : { ok: false, outside: true }; + } catch (error) { + if (["ENOENT", "ENOTDIR", "ELOOP"].includes(error?.code)) return { ok: false }; + return { ok: false, unreadable: true, reason: error?.code ?? error?.name ?? "UNKNOWN" }; + } + })()); + } + return cache.get(target); +} + +async function checkMarkdownTarget(root, sourceAbsolute, rawTarget, existenceCache) { + const target = unwrapLinkTarget(rawTarget); + if (isNonRelativeLink(target)) return { ok: true }; + const encodedFilePart = stripQueryAndFragment(target); + if (encodedFilePart === "") return { ok: true }; + const filePart = decodeLinkPath(encodedFilePart); + const absolute = path.resolve(path.dirname(sourceAbsolute), filePart); + if (!insideRoot(root, absolute)) return { ok: false, outside: true, target }; + return { ...(await physicalTarget(root, absolute, existenceCache)), target }; +} + +function wikiPage(rawTarget) { + const withoutAlias = rawTarget.split("|", 1)[0].trim(); + const withoutHeading = withoutAlias.split("#", 1)[0].split("^", 1)[0].trim(); + return decodeLinkPath(unwrapLinkTarget(withoutHeading)); +} + +async function checkWikiTarget( + root, + sourceAbsolute, + rawTarget, + knownPaths, + existenceCache +) { + const page = wikiPage(rawTarget); + if (page === "") return { ok: true }; + + const hasKnownExtension = /\.(?:md|markdown|canvas|pdf|png|jpe?g|gif|svg|webp)$/iu.test(page); + const variants = hasKnownExtension ? [page] : [`${page}.md`, page]; + const candidates = []; + for (const variant of variants) { + candidates.push(path.resolve(path.dirname(sourceAbsolute), variant)); + candidates.push(path.resolve(root, variant)); + candidates.push(path.resolve(root, CONTEXT_DIRECTORY, variant)); + } + + let inaccessible; + for (const candidate of candidates) { + if (!insideRoot(root, candidate)) continue; + const outcome = await physicalTarget(root, candidate, existenceCache); + if (outcome.ok) return { ok: true }; + if (outcome.unreadable || outcome.outside) inaccessible ??= outcome; + } + + const expectedBasenames = new Set(variants.map((variant) => path.basename(variant))); + const basenameMatches = knownPaths.filter((relative) => expectedBasenames.has(path.basename(relative))); + if (basenameMatches.length === 1) { + const candidate = managedPath(root, basenameMatches[0]); + const outcome = await physicalTarget(root, candidate, existenceCache); + if (outcome.ok) return { ok: true }; + if (outcome.unreadable || outcome.outside) inaccessible ??= outcome; + } + + return { ok: false, target: rawTarget.trim(), ...inaccessible }; +} + +async function inspectLinks(root, canonical, knownPaths, recorder) { + const existenceCache = new Map(); + + for (const [relative, note] of [...canonical.entries()].sort(([left], [right]) => compareText(left, right))) { + const sourceAbsolute = managedPath(root, relative); + for (const link of extractLinks(note.content)) { + const outcome = link.type === "wiki" + ? await checkWikiTarget(root, sourceAbsolute, link.target, knownPaths, existenceCache) + : await checkMarkdownTarget(root, sourceAbsolute, link.target, existenceCache); + if (outcome.ok) continue; + + recorder.error("links", { + code: outcome.outside ? "RELATIVE_LINK_OUTSIDE_ROOT" : outcome.unreadable + ? "UNREADABLE_LINK_TARGET" : link.type === "wiki" + ? "BROKEN_WIKILINK" + : "BROKEN_MARKDOWN_LINK", + file: relative, + line: link.line, + target: outcome.target, + message: outcome.outside + ? "El enlace relativo sale del repositorio." + : outcome.unreadable + ? "No se pudo comprobar el destino del enlace." + : link.type === "wiki" + ? "El wikilink no tiene un destino existente." + : "El enlace Markdown relativo no tiene un destino existente." + }); + } + } +} + +function escapeRegularExpression(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function duplicateSource(content) { + let value = content; + if (value.startsWith("---")) value = value.replace(/^---\s*\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$)/u, ""); + const generatedPattern = new RegExp( + `${escapeRegularExpression(START_MARKER)}[\\s\\S]*?${escapeRegularExpression(END_MARKER)}`, + "gu" + ); + return value + .replace(generatedPattern, "") + .replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/gu, "") + .replace(//gu, "") + .replace(/!?\[([^\]]*)\]\([^)]+\)/gu, "$1") + .replace(/!?\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/gu, "$1"); +} + +function normalizeWords(value) { + return value + .normalize("NFKC") + .toLocaleLowerCase("es") + .replace(/https?:\/\/\S+/gu, " ") + .replace(/[^\p{L}\p{N}]+/gu, " ") + .trim() + .replace(/\s+/gu, " "); +} + +function duplicateProfile(content) { + const source = duplicateSource(content); + const paragraphs = source + .split(/(?:\r?\n){2,}/u) + .map(normalizeWords) + .filter((paragraph) => paragraph.length >= 100 && paragraph.split(" ").length >= 12); + const normalized = normalizeWords(source); + const tokens = normalized === "" ? [] : normalized.split(" "); + const shingles = new Set(); + for (let index = 0; index <= tokens.length - 7; index += 1) { + shingles.add(tokens.slice(index, index + 7).join(" ")); + } + return { paragraphs: new Set(paragraphs), tokens, shingles }; +} + +function intersectionSize(left, right) { + let count = 0; + const smaller = left.size <= right.size ? left : right; + const larger = smaller === left ? right : left; + for (const value of smaller) if (larger.has(value)) count += 1; + return count; +} + +function inspectDuplicates(canonical, recorder) { + const notes = [...canonical.entries()] + .sort(([left], [right]) => compareText(left, right)) + .map(([relative, note]) => [relative, duplicateProfile(note.content)]); + + for (let leftIndex = 0; leftIndex < notes.length; leftIndex += 1) { + for (let rightIndex = leftIndex + 1; rightIndex < notes.length; rightIndex += 1) { + const [leftFile, left] = notes[leftIndex]; + const [rightFile, right] = notes[rightIndex]; + const sharedParagraphs = [...left.paragraphs] + .filter((paragraph) => right.paragraphs.has(paragraph)) + .sort((first, second) => second.length - first.length || compareText(first, second)); + + const sharedShingles = intersectionSize(left.shingles, right.shingles); + const smallestShingleSet = Math.min(left.shingles.size, right.shingles.size); + const containment = smallestShingleSet === 0 ? 0 : sharedShingles / smallestShingleSet; + const paragraphDuplicate = sharedParagraphs.length > 0; + const broadDuplicate = sharedShingles >= 10 && containment >= 0.6; + if (!paragraphDuplicate && !broadDuplicate) continue; + + recorder.warning("duplicates", { + code: "SIGNIFICANT_DUPLICATE", + file: leftFile, + relatedFile: rightFile, + message: "Las notas repiten contenido significativo que debería tener una sola fuente.", + similarity: Number(containment.toFixed(3)), + sharedWords: paragraphDuplicate ? sharedParagraphs[0].split(" ").length : undefined + }); + } + } +} + +function isPlaceholder(value) { + const normalized = value + .trim() + .replace(/^["'`]|["'`,;]$/gu, "") + .trim() + .toLocaleLowerCase("en"); + if (normalized === "") return true; + if (/^<[^>]+>$|^\$\{[^}]+\}$|^\{\{[^}]+\}\}$/u.test(normalized)) return true; + if (/^(?:process\.env\.|env\.)[a-z_][a-z\d_]*$/iu.test(normalized)) return true; + return /^(?:change-?me|replace-?me|placeholder|your[-_].*|example(?:[-_].*)?|dummy|sample|test(?:ing)?|redacted|masked|none|null|undefined|pending|pendiente|x{3,}|\*{3,}|0{4,}|\.\.\.)$/u.test(normalized); +} + +function lineOf(content, offset) { + return content.slice(0, offset).split(/\r\n|\r|\n/).length; +} + +function placeholderEmail(address) { + const [local = "", domain = ""] = address.toLocaleLowerCase("en").split("@"); + return ( + /^(?:example\.(?:com|org|net)|example|invalid|localhost|.*\.example\.com)$/u.test(domain) || + /^(?:user(?:name)?|name|email|correo|test|your(?:\.?name)?|noreply|placeholder)$/u.test(local) + ); +} + +function placeholderPhone(value) { + const digits = value.replace(/\D/gu, ""); + return ( + digits.length < 10 || + digits.length > 15 || + /^(\d)\1+$/u.test(digits) || + /^(?:0123456789|1234567890|5555555555)$/u.test(digits) || + /55501\d{2}$/u.test(digits) + ); +} + +function inspectSensitiveData(canonical, recorder) { + const privateKeyPattern = /-----BEGIN(?:(?: [A-Z0-9]+)* PRIVATE KEY| PGP PRIVATE KEY BLOCK)-----/gu; + const knownTokenPatterns = [ + /\bgh[pousr]_[A-Za-z\d]{20,}\b/gu, + /\bgithub_pat_[A-Za-z\d_]{20,}\b/gu, + /\bnpm_[A-Za-z\d]{20,}\b/gu, + /\bsk-(?:proj-)?[A-Za-z\d_-]{20,}\b/gu, + /\bAKIA[A-Z\d]{16}\b/gu, + /\bAIza[A-Za-z\d_-]{30,}\b/gu, + /\bxox[baprs]-[A-Za-z\d-]{16,}\b/gu, + /\beyJ[A-Za-z\d_-]{10,}\.[A-Za-z\d_-]{10,}\.[A-Za-z\d_-]{10,}\b/gu, + /\bBearer\s+[A-Za-z\d._~+/=-]{16,}\b/giu + ]; + const assignmentPattern = /(?:^|[\s{,;"'`])((?:[a-z\d]+[-_])*(?:password|passwd|pwd|client[-_]?secret|api[-_]?key|access[-_]?key|access[-_]?token|auth[-_]?token|refresh[-_]?token|secret[-_]?access[-_]?key|secret[-_]?key|private[-_]?key|token))\s*[:=]\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`|([^\s,;#]+))/giu; + const urlCredentialPattern = /\b[a-z][a-z\d+.-]*:\/\/[^\s/:@]+:([^\s/@]+)@/giu; + const emailPattern = /\b[A-Z\d._%+-]+@[A-Z\d.-]+\.[A-Z]{2,63}\b/giu; + const internationalPhonePattern = /(? compareText(left, right))) { + const content = note.content; + const sensitiveLines = new Set(); + + privateKeyPattern.lastIndex = 0; + let match; + while ((match = privateKeyPattern.exec(content))) { + const line = lineOf(content, match.index); + sensitiveLines.add(line); + recorder.error("sensitive-data", { + code: "PRIVATE_KEY", + file: relative, + line, + message: "Se detectó material de llave privada en el contexto." + }); + } + + const lines = content.split(/\r\n|\r|\n/); + for (let index = 0; index < lines.length; index += 1) { + const lineNumber = index + 1; + const rawLine = lines[index]; + const assignmentLine = rawLine.replace(/[*]/gu, ""); + + assignmentPattern.lastIndex = 0; + while ((match = assignmentPattern.exec(assignmentLine))) { + const key = match[1].toLocaleLowerCase("en"); + const value = match[2] ?? match[3] ?? match[4] ?? match[5] ?? ""; + if (isPlaceholder(value) || sensitiveLines.has(lineNumber)) continue; + sensitiveLines.add(lineNumber); + recorder.error("sensitive-data", { + code: key.includes("token") || key.includes("api") ? "EXPOSED_TOKEN" : "EXPOSED_CREDENTIAL", + file: relative, + line: lineNumber, + message: "Se detectó una credencial o token con valor material en el contexto." + }); + } + + for (const pattern of knownTokenPatterns) { + pattern.lastIndex = 0; + if (pattern.test(rawLine) && !sensitiveLines.has(lineNumber)) { + sensitiveLines.add(lineNumber); + recorder.error("sensitive-data", { + code: "EXPOSED_TOKEN", + file: relative, + line: lineNumber, + message: "Se detectó un token con formato reconocible en el contexto." + }); + } + } + + urlCredentialPattern.lastIndex = 0; + while ((match = urlCredentialPattern.exec(rawLine))) { + if (isPlaceholder(match[1]) || sensitiveLines.has(lineNumber)) continue; + sensitiveLines.add(lineNumber); + recorder.error("sensitive-data", { + code: "EXPOSED_CREDENTIAL", + file: relative, + line: lineNumber, + message: "Se detectaron credenciales incrustadas en una URL." + }); + } + + emailPattern.lastIndex = 0; + while ((match = emailPattern.exec(rawLine))) { + if (placeholderEmail(match[0])) continue; + recorder.warning("sensitive-data", { + code: "PERSONAL_EMAIL", + file: relative, + line: lineNumber, + message: "Se detectó una dirección de correo posiblemente personal." + }); + } + + const phoneCandidates = []; + for (const pattern of [internationalPhonePattern, formattedPhonePattern]) { + pattern.lastIndex = 0; + while ((match = pattern.exec(rawLine))) phoneCandidates.push(match[0]); + } + labelledPhonePattern.lastIndex = 0; + while ((match = labelledPhonePattern.exec(rawLine))) phoneCandidates.push(match[1]); + if (phoneCandidates.some((candidate) => !placeholderPhone(candidate))) { + recorder.warning("sensitive-data", { + code: "PERSONAL_PHONE", + file: relative, + line: lineNumber, + message: "Se detectó un número telefónico posiblemente personal." + }); + } + } + } +} + +/** + * Audita el contrato mínimo de Project Brain sin escribir archivos ni terminar el proceso. + * + * @param {string} inputRoot Directorio del repositorio que se debe revisar. + * @returns {Promise<{ok: boolean, errors: object[], warnings: object[], checks: object[]}>} + */ +export async function doctor(inputRoot = ".") { + const recorder = createRecorder(); + let root; + try { + root = await resolveRoot(inputRoot); + } catch (error) { + recorder.error("canonical-files", { + code: "INVALID_ROOT", + file: ".", + message: "No se pudo abrir el directorio solicitado.", + reason: error?.code ?? error?.name ?? "UNKNOWN" + }); + return recorder.result(); + } + + const canonical = new Map(); + const contextPath = managedPath(root, CONTEXT_DIRECTORY); + let contextDirectorySafe = false; + let contextInfo; + try { + contextInfo = await lstatIfPresent(contextPath); + } catch (error) { + recorder.error("canonical-files", { + code: "UNREADABLE_CONTEXT_DIRECTORY", + file: CONTEXT_DIRECTORY, + message: "No se pudo inspeccionar el directorio de contexto.", + reason: error?.code ?? error?.name ?? "UNKNOWN" + }); + } + if (contextInfo?.isSymbolicLink()) { + recorder.error("canonical-files", { + code: "SYMLINK_CONTEXT_DIRECTORY", + file: CONTEXT_DIRECTORY, + message: "El directorio de contexto no puede ser un enlace simbólico." + }); + } else if (contextInfo && !contextInfo.isDirectory()) { + recorder.error("canonical-files", { + code: "INVALID_CONTEXT_DIRECTORY", + file: CONTEXT_DIRECTORY, + message: "La ruta de contexto existe, pero no es un directorio." + }); + } else if (contextInfo?.isDirectory()) { + contextDirectorySafe = true; + } + + for (const relative of REQUIRED_FILES) { + if (relative.startsWith(`${CONTEXT_DIRECTORY}/`) && contextInfo?.isSymbolicLink()) continue; + const absolute = managedPath(root, relative); + let info; + try { + info = await lstatIfPresent(absolute); + } catch (error) { + recorder.error("canonical-files", { + code: "UNREADABLE_CANONICAL_FILE", + file: relative, + message: "No se pudo inspeccionar el archivo canónico.", + reason: error?.code ?? error?.name ?? "UNKNOWN" + }); + continue; + } + + if (!info) { + recorder.error("canonical-files", { + code: "MISSING_CANONICAL_FILE", + file: relative, + message: "Falta un archivo canónico del contrato." + }); + continue; + } + if (info.isSymbolicLink()) { + recorder.error("canonical-files", { + code: "SYMLINK_CANONICAL_FILE", + file: relative, + message: "Un archivo canónico no puede ser un enlace simbólico." + }); + continue; + } + if (!info.isFile()) { + recorder.error("canonical-files", { + code: "INVALID_CANONICAL_FILE", + file: relative, + message: "La ruta canónica existe, pero no es un archivo regular." + }); + continue; + } + + if (info.size > MAX_AUDIT_FILE_BYTES) { + recorder.error("size-limits", { + code: "CONTENT_TOO_LARGE_TO_AUDIT", + file: relative, + message: "El archivo es demasiado grande para auditarlo de forma segura.", + actual: info.size, + limit: MAX_AUDIT_FILE_BYTES + }); + canonical.set(relative, { content: "", bytes: info.size, lines: 0 }); + continue; + } + + try { + const content = await readFile(absolute, "utf8"); + canonical.set(relative, { + content, + bytes: info.size, + lines: lineCount(content) + }); + } catch (error) { + recorder.error("canonical-files", { + code: "UNREADABLE_CANONICAL_FILE", + file: relative, + message: "No se pudo leer el archivo canónico como UTF-8.", + reason: error?.code ?? error?.name ?? "UNKNOWN" + }); + } + } + + const generated = canonical.get(GENERATED_FILE); + if (generated) { + const startCount = countOccurrences(generated.content, START_MARKER); + const endCount = countOccurrences(generated.content, END_MARKER); + if (startCount !== 1) { + recorder.error("generated-markers", { + code: "GENERATED_START_MARKER_COUNT", + file: GENERATED_FILE, + message: "El marcador inicial generado debe aparecer exactamente una vez.", + actual: startCount, + expected: 1 + }); + } + if (endCount !== 1) { + recorder.error("generated-markers", { + code: "GENERATED_END_MARKER_COUNT", + file: GENERATED_FILE, + message: "El marcador final generado debe aparecer exactamente una vez.", + actual: endCount, + expected: 1 + }); + } + if ( + startCount === 1 && + endCount === 1 && + generated.content.indexOf(START_MARKER) > generated.content.indexOf(END_MARKER) + ) { + recorder.error("generated-markers", { + code: "GENERATED_MARKER_ORDER", + file: GENERATED_FILE, + message: "El marcador final aparece antes del marcador inicial." + }); + } + } + + let totalBytes = 0; + for (const relative of REQUIRED_FILES) { + const note = canonical.get(relative); + if (!note) continue; + totalBytes += note.bytes; + if (note.bytes > LIMITS.bytesPerFile) { + recorder.warning("size-limits", { + code: "FILE_BYTES_EXCEEDED", + file: relative, + message: "El archivo supera el límite de bytes del contrato.", + actual: note.bytes, + limit: LIMITS.bytesPerFile + }); + } + if (note.lines > LIMITS.linesPerFile) { + recorder.warning("size-limits", { + code: "FILE_LINES_EXCEEDED", + file: relative, + message: "El archivo supera el límite de líneas del contrato.", + actual: note.lines, + limit: LIMITS.linesPerFile + }); + } + } + if (totalBytes > LIMITS.totalBytes) { + recorder.warning("size-limits", { + code: "TOTAL_BYTES_EXCEEDED", + file: CONTEXT_DIRECTORY, + message: "El conjunto canónico supera el límite total de bytes.", + actual: totalBytes, + limit: LIMITS.totalBytes + }); + } + + let contextEntries = []; + if (contextDirectorySafe) { + contextEntries = await collectExtraContextFiles(root, contextPath, recorder); + for (const relative of contextEntries) { + if (REQUIRED_CONTEXT_FILES.has(relative)) continue; + recorder.warning("extra-context-files", { + code: "EXTRA_CONTEXT_FILE", + file: relative, + message: "El archivo no forma parte del contrato mínimo de AI_CONTEXT." + }); + } + } + + const knownPaths = [...new Set([...canonical.keys(), ...contextEntries])].sort(compareText); + const auditedNotes = new Map(canonical); + for (const relative of contextEntries) { + if (REQUIRED_CONTEXT_FILES.has(relative)) continue; + const absolute = managedPath(root, relative); + let info; + try { + info = await lstatIfPresent(absolute); + } catch (error) { + recorder.warning("extra-context-files", { + code: "UNREADABLE_EXTRA_CONTEXT_FILE", + file: relative, + message: "No se pudo inspeccionar el archivo extra.", + reason: error?.code ?? error?.name ?? "UNKNOWN" + }); + continue; + } + if (!info?.isFile() || info.isSymbolicLink()) continue; + if (!AUDITABLE_EXTRA_NAMES.test(path.posix.basename(relative))) continue; + if (info.size > MAX_AUDIT_FILE_BYTES) { + recorder.error("sensitive-data", { + code: "CONTENT_TOO_LARGE_TO_AUDIT", + file: relative, + message: "El archivo extra es demasiado grande para descartar datos sensibles de forma segura.", + actual: info.size, + limit: MAX_AUDIT_FILE_BYTES + }); + continue; + } + try { + auditedNotes.set(relative, { content: await readFile(absolute, "utf8") }); + } catch (error) { + recorder.warning("extra-context-files", { + code: "UNREADABLE_EXTRA_CONTEXT_FILE", + file: relative, + message: "No se pudo auditar el contenido del archivo extra.", + reason: error?.code ?? error?.name ?? "UNKNOWN" + }); + } + } + + await inspectLinks(root, auditedNotes, knownPaths, recorder); + inspectDuplicates(auditedNotes, recorder); + inspectSensitiveData(auditedNotes, recorder); + + return recorder.result(); +} + +export const runDoctor = doctor; +export const doctorRepository = doctor; +export default doctor; diff --git a/src/fs.mjs b/src/fs.mjs new file mode 100644 index 0000000..f62c451 --- /dev/null +++ b/src/fs.mjs @@ -0,0 +1,110 @@ +import { link, lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises"; +import path from "node:path"; + +export async function pathExists(filePath) { + try { + await lstat(filePath); + return true; + } catch (error) { + if (error?.code === "ENOENT") return false; + throw error; + } +} + +export async function resolveRoot(input = ".", { create = false } = {}) { + const absolute = path.resolve(input); + if (create) await mkdir(absolute, { recursive: true }); + const resolved = await realpath(absolute); + const info = await lstat(resolved); + if (!info.isDirectory()) throw new Error(`No es un directorio: ${absolute}`); + return resolved; +} + +export function insideRoot(root, target) { + const relative = path.relative(root, target); + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); +} + +export function managedPath(root, relativePath) { + const target = path.resolve(root, relativePath); + if (!insideRoot(root, target)) throw new Error(`Ruta fuera del repositorio: ${relativePath}`); + return target; +} + +export async function ensureManagedDirectory(root, relativePath, { create = false } = {}) { + const directory = managedPath(root, relativePath); + if (create && !(await pathExists(directory))) await mkdir(directory); + const info = await lstat(directory); + if (info.isSymbolicLink()) throw new Error(`No se permiten directorios simbólicos administrados: ${directory}`); + if (!info.isDirectory()) throw new Error(`No es un directorio: ${directory}`); + const resolved = await realpath(directory); + if (!insideRoot(root, resolved)) throw new Error(`El directorio administrado sale del repositorio: ${relativePath}`); + return directory; +} + +export async function ensureRegularFile(filePath) { + const info = await lstat(filePath); + if (info.isSymbolicLink()) throw new Error(`No se permiten enlaces simbólicos administrados: ${filePath}`); + if (!info.isFile()) throw new Error(`No es un archivo regular: ${filePath}`); + return info; +} + +export async function readText(filePath) { + await ensureRegularFile(filePath); + return readFile(filePath, "utf8"); +} + +export async function writeNewFile(filePath, content) { + await mkdir(path.dirname(filePath), { recursive: true }); + const temporary = path.join( + path.dirname(filePath), + `.${path.basename(filePath)}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp` + ); + try { + await writeFile(temporary, content, { encoding: "utf8", flag: "wx", mode: 0o644 }); + await link(temporary, filePath); + } finally { + await unlink(temporary).catch(() => {}); + } +} + +async function managedParentIdentity(root, filePath) { + const parent = path.dirname(filePath); + const info = await lstat(parent); + if (info.isSymbolicLink() || !info.isDirectory()) { + throw new Error(`Directorio padre administrado inválido: ${parent}`); + } + const resolved = await realpath(parent); + if (!insideRoot(root, resolved)) throw new Error(`El directorio padre sale del repositorio: ${parent}`); + return { dev: info.dev, ino: info.ino }; +} + +function sameIdentity(left, right) { + return left.dev === right.dev && left.ino === right.ino; +} + +export async function writeFileAtomic(root, filePath, content) { + const parentIdentity = await managedParentIdentity(root, filePath); + let mode = 0o644; + if (await pathExists(filePath)) mode = (await ensureRegularFile(filePath)).mode & 0o777; + await mkdir(path.dirname(filePath), { recursive: true }); + const temporary = path.join( + path.dirname(filePath), + `.${path.basename(filePath)}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp` + ); + try { + await writeFile(temporary, content, { encoding: "utf8", flag: "wx", mode }); + const afterWriteIdentity = await managedParentIdentity(root, filePath); + if (!sameIdentity(parentIdentity, afterWriteIdentity)) { + throw new Error("El directorio administrado cambió durante la escritura."); + } + await ensureRegularFile(filePath); + await rename(temporary, filePath); + } catch (error) { + const currentIdentity = await managedParentIdentity(root, filePath).catch(() => null); + if (currentIdentity && sameIdentity(parentIdentity, currentIdentity)) { + await unlink(temporary).catch(() => {}); + } + throw error; + } +} diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 0000000..d2836ea --- /dev/null +++ b/src/index.mjs @@ -0,0 +1,5 @@ +export { doctor, doctorRepository } from "./doctor.mjs"; +export { initRepository } from "./init.mjs"; +export { scanRepository } from "./scanner.mjs"; +export { syncRepository } from "./sync.mjs"; +export { CONTRACT, CONTRACT_SCHEMA } from "./contract.mjs"; diff --git a/src/init.mjs b/src/init.mjs new file mode 100644 index 0000000..b9d0155 --- /dev/null +++ b/src/init.mjs @@ -0,0 +1,39 @@ +import { GENERATED_FILE, REQUIRED_FILES } from "./contract.mjs"; +import { assertGeneratedBlock } from "./context.mjs"; +import { ensureManagedDirectory, ensureRegularFile, managedPath, pathExists, readText, resolveRoot, writeNewFile } from "./fs.mjs"; +import { scanRepository } from "./scanner.mjs"; +import { syncRepository } from "./sync.mjs"; +import { loadTemplates } from "./templates.mjs"; + +export async function initRepository(input = ".") { + const root = await resolveRoot(input, { create: true }); + const existing = []; + + if (await pathExists(managedPath(root, "AI_CONTEXT"))) { + await ensureManagedDirectory(root, "AI_CONTEXT"); + } + + for (const relativePath of REQUIRED_FILES) { + const filePath = managedPath(root, relativePath); + if (await pathExists(filePath)) { + await ensureRegularFile(filePath); + if (relativePath === GENERATED_FILE) assertGeneratedBlock(await readText(filePath)); + existing.push(relativePath); + } + } + + // Descubre problemas de lectura antes de publicar una parte del contrato. + await scanRepository(root); + + const templates = await loadTemplates(); + const created = []; + await ensureManagedDirectory(root, "AI_CONTEXT", { create: true }); + for (const relativePath of REQUIRED_FILES) { + if (existing.includes(relativePath)) continue; + await writeNewFile(managedPath(root, relativePath), templates.get(relativePath)); + created.push(relativePath); + } + + const synced = await syncRepository(root); + return { root, created, preserved: existing, changed: synced.changed, facts: synced.facts }; +} diff --git a/src/scanner.mjs b/src/scanner.mjs new file mode 100644 index 0000000..78e8f75 --- /dev/null +++ b/src/scanner.mjs @@ -0,0 +1,535 @@ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { lstat, readFile, readdir, realpath } from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; + +import { insideRoot, resolveRoot } from "./fs.mjs"; + +const execFileAsync = promisify(execFile); +const MAX_SEMANTIC_FILE_BYTES = 1024 * 1024; + +const ALWAYS_IGNORED_DIRECTORIES = new Set([ + ".git", + "node_modules", + "dist", + "build", + "coverage", + "brain", + ".brain", + "graphify-out", + ".graphify", + ".obsidian", + "ai_context", + ".cache", + "cache", + "caches", + "__pycache__", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", + ".tox", + ".nox", + ".turbo", + ".vite", + ".next", + ".nuxt", + ".svelte-kit", + ".angular", + ".parcel-cache", + ".pnpm-store", + ".dart_tool", + ".gradle", + ".build", + "deriveddata", + "target" +]); + +const ALWAYS_IGNORED_FILES = new Set([ + ".ds_store", + ".eslintcache", + ".stylelintcache" +]); + +const LANGUAGE_BY_EXTENSION = new Map([ + [".bash", "Shell"], + [".c", "C"], + [".cc", "C++"], + [".cjs", "JavaScript"], + [".cpp", "C++"], + [".cs", "C#"], + [".css", "CSS"], + [".cts", "TypeScript"], + [".dart", "Dart"], + [".go", "Go"], + [".h", "C/C++"], + [".hpp", "C++"], + [".html", "HTML"], + [".java", "Java"], + [".js", "JavaScript"], + [".jsx", "JavaScript"], + [".kt", "Kotlin"], + [".kts", "Kotlin"], + [".mjs", "JavaScript"], + [".mts", "TypeScript"], + [".php", "PHP"], + [".py", "Python"], + [".rb", "Ruby"], + [".rs", "Rust"], + [".sh", "Shell"], + [".sql", "SQL"], + [".swift", "Swift"], + [".ts", "TypeScript"], + [".tsx", "TypeScript"], + [".vue", "Vue"], + [".zsh", "Shell"] +]); + +const MANIFEST_NAMES = new Set([ + "bun.lock", + "bun.lockb", + "cargo.lock", + "cargo.toml", + "composer.json", + "composer.lock", + "docker-compose.yml", + "docker-compose.yaml", + "gemfile", + "gemfile.lock", + "go.mod", + "go.sum", + "gradle.properties", + "npm-shrinkwrap.json", + "package-lock.json", + "package.json", + "package.swift", + "pipfile", + "pipfile.lock", + "pnpm-lock.yaml", + "poetry.lock", + "pom.xml", + "pubspec.lock", + "pubspec.yaml", + "pyproject.toml", + "pytest.ini", + "requirements.txt", + "setup.cfg", + "setup.py", + "settings.gradle", + "settings.gradle.kts", + "tox.ini", + "yarn.lock" +]); + +const STACK_ORDER = [ + "Node.js", + "TypeScript", + "React", + "Next.js", + "Vite", + "Express", + "Flutter", + "Python", + "Go", + "Rust", + "Gradle", + "Kotlin", + "Java", + "Swift", + "Docker" +]; + +const VALIDATION_SCRIPT_PRIORITY = [ + "check", + "verify", + "validate", + "lint", + "typecheck", + "type-check", + "check:types", + "test", + "build", + "format:check", + "check:format", + "audit" +]; + +function lexicalCompare(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function toPosix(relativePath) { + return relativePath.split(path.sep).join("/"); +} + +function normalizeRelativePath(relativePath) { + const normalized = path.posix.normalize(toPosix(relativePath)).replace(/^\.\//, ""); + if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) { + return undefined; + } + return normalized; +} + +function isIgnoredPath(relativePath) { + const normalized = normalizeRelativePath(relativePath); + if (!normalized) return true; + + const segments = normalized.split("/"); + if (segments.some((segment) => ALWAYS_IGNORED_DIRECTORIES.has(segment.toLowerCase()))) return true; + + const basename = segments.at(-1)?.toLowerCase() ?? ""; + return ( + ALWAYS_IGNORED_FILES.has(basename) || + basename.endsWith(".pyc") || + basename.endsWith(".pyo") || + basename.endsWith(".cache") + ); +} + +async function listWithGit(root) { + try { + const { stdout } = await execFileAsync( + "git", + ["-C", root, "-c", "core.fsmonitor=false", "ls-files", "-co", "--exclude-standard", "-z"], + { encoding: "buffer", maxBuffer: 64 * 1024 * 1024 } + ); + + return stdout + .toString("utf8") + .split("\0") + .map(normalizeRelativePath) + .filter(Boolean); + } catch { + return undefined; + } +} + +async function walkLocal(root) { + const files = []; + + async function visit(directory, prefix) { + const entries = await readdir(directory, { withFileTypes: true }); + entries.sort((left, right) => lexicalCompare(left.name, right.name)); + + for (const entry of entries) { + const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; + if (isIgnoredPath(relativePath) || entry.isSymbolicLink()) continue; + + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) { + await visit(absolutePath, relativePath); + } else if (entry.isFile()) { + files.push(relativePath); + } + } + } + + await visit(root, ""); + return files; +} + +async function collectRecords(root) { + const candidates = (await listWithGit(root)) ?? (await walkLocal(root)); + const uniquePaths = [...new Set(candidates.filter((filePath) => !isIgnoredPath(filePath)))].sort(lexicalCompare); + const records = []; + + for (const relativePath of uniquePaths) { + const absolutePath = path.join(root, ...relativePath.split("/")); + try { + const info = await lstat(absolutePath); + if (!info.isFile() || info.isSymbolicLink()) continue; + const physicalPath = await realpath(absolutePath); + if (!insideRoot(root, physicalPath)) continue; + records.push({ path: relativePath, size: info.size }); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } + } + + return records; +} + +function extensionFor(filePath) { + return path.posix.extname(filePath).toLowerCase() || "(none)"; +} + +function buildExtensions(records) { + const counts = new Map(); + for (const record of records) { + const extension = extensionFor(record.path); + counts.set(extension, (counts.get(extension) ?? 0) + 1); + } + + return Object.fromEntries([...counts].sort(([left], [right]) => lexicalCompare(left, right))); +} + +function buildLanguages(records) { + const languages = new Set(); + for (const record of records) { + const language = LANGUAGE_BY_EXTENSION.get(extensionFor(record.path)); + if (language) languages.add(language); + } + return [...languages].sort(lexicalCompare); +} + +function isManifest(filePath) { + const basename = path.posix.basename(filePath).toLowerCase(); + return ( + MANIFEST_NAMES.has(basename) || + /^dockerfile(?:\..+)?$/.test(basename) || + /^(?:compose|docker-compose)(?:\.[^.]+)?\.ya?ml$/.test(basename) || + /^requirements(?:[-_.][^/]*)?\.txt$/.test(basename) || + /^tsconfig(?:\.[^/]*)?\.json$/.test(basename) || + /^build\.gradle(?:\.kts)?$/.test(basename) + ); +} + +function buildRoots(records) { + const roots = new Set(); + for (const record of records) { + const separator = record.path.indexOf("/"); + if (separator > 0) roots.add(record.path.slice(0, separator)); + } + return [...roots].sort(lexicalCompare); +} + +function buildFingerprint(records) { + const input = records.map((record) => `${record.path}:${record.size}`).join("\n"); + return createHash("sha256").update(input, "utf8").digest("hex"); +} + +async function readRecord(root, relativePath) { + const absolutePath = path.join(root, ...relativePath.split("/")); + const lexicalInfo = await lstat(absolutePath); + if (!lexicalInfo.isFile() || lexicalInfo.isSymbolicLink()) throw new Error("Manifest no regular."); + const physicalPath = await realpath(absolutePath); + if (!insideRoot(root, physicalPath)) throw new Error("Manifest fuera del repositorio."); + const info = await lstat(physicalPath); + if (info.size > MAX_SEMANTIC_FILE_BYTES) throw new Error("Manifest demasiado grande para análisis semántico."); + return readFile(physicalPath, "utf8"); +} + +async function readPackageManifests(root, manifests) { + const packages = []; + for (const manifest of manifests.filter((filePath) => path.posix.basename(filePath).toLowerCase() === "package.json")) { + try { + const parsed = JSON.parse(await readRecord(root, manifest)); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + packages.push({ path: manifest, value: parsed }); + } + } catch { + // El manifest sigue siendo evidencia de Node, aunque no sea JSON válido. + } + } + return packages; +} + +function packageDependencyNames(packages) { + const names = new Set(); + for (const { value } of packages) { + for (const group of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) { + const dependencies = value[group]; + if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue; + for (const name of Object.keys(dependencies)) names.add(name.toLowerCase()); + } + } + return names; +} + +async function detectStack(root, records, manifests, packages, languages) { + const basenames = new Set(records.map((record) => path.posix.basename(record.path).toLowerCase())); + const manifestBasenames = new Set(manifests.map((filePath) => path.posix.basename(filePath).toLowerCase())); + const dependencies = packageDependencyNames(packages); + const detected = new Set(); + + if (manifestBasenames.has("package.json") || [...manifestBasenames].some((name) => ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"].includes(name))) { + detected.add("Node.js"); + } + if (languages.includes("TypeScript") || [...manifestBasenames].some((name) => /^tsconfig(?:\..+)?\.json$/.test(name))) detected.add("TypeScript"); + if (dependencies.has("react") || dependencies.has("react-dom") || dependencies.has("next")) detected.add("React"); + if (dependencies.has("next") || [...basenames].some((name) => /^next\.config\./.test(name))) detected.add("Next.js"); + if (dependencies.has("vite") || [...basenames].some((name) => /^vite\.config\./.test(name))) detected.add("Vite"); + if (dependencies.has("express")) detected.add("Express"); + + const pubspecs = manifests.filter((filePath) => path.posix.basename(filePath).toLowerCase() === "pubspec.yaml"); + for (const pubspec of pubspecs) { + const content = await readRecord(root, pubspec).catch(() => ""); + if (/\bsdk\s*:\s*flutter\b/i.test(content) || /^\s*flutter\s*:/m.test(content)) detected.add("Flutter"); + } + + if (languages.includes("Python") || [...manifestBasenames].some((name) => ["pyproject.toml", "pipfile", "setup.py", "setup.cfg"].includes(name) || /^requirements.*\.txt$/.test(name))) detected.add("Python"); + if (languages.includes("Go") || manifestBasenames.has("go.mod")) detected.add("Go"); + if (languages.includes("Rust") || manifestBasenames.has("cargo.toml")) detected.add("Rust"); + if ([...manifestBasenames].some((name) => /^build\.gradle(?:\.kts)?$/.test(name) || /^settings\.gradle(?:\.kts)?$/.test(name))) detected.add("Gradle"); + if (languages.includes("Kotlin")) detected.add("Kotlin"); + if (languages.includes("Java")) detected.add("Java"); + if (languages.includes("Swift") || manifestBasenames.has("package.swift")) detected.add("Swift"); + if ([...manifestBasenames].some((name) => /^dockerfile(?:\..+)?$/.test(name) || /^(?:compose|docker-compose)(?:\.[^.]+)?\.ya?ml$/.test(name))) detected.add("Docker"); + + return STACK_ORDER.filter((name) => detected.has(name)); +} + +function directoryOf(filePath) { + const directory = path.posix.dirname(filePath); + return directory === "." ? "" : directory; +} + +function pathInDirectory(directory, child) { + return directory ? `${directory}/${child}` : child; +} + +function shellQuote(value) { + if (/^[A-Za-z0-9_./-]+$/.test(value)) return value; + return `'${value.replaceAll("'", `'"'"'`)}'`; +} + +function commandInDirectory(directory, command) { + return directory ? `cd ${shellQuote(directory)} && ${command}` : command; +} + +function findNearestPackageManager(directory, filePaths) { + let current = directory; + for (;;) { + if (filePaths.has(pathInDirectory(current, "pnpm-lock.yaml"))) return "pnpm"; + if (filePaths.has(pathInDirectory(current, "yarn.lock"))) return "yarn"; + if (filePaths.has(pathInDirectory(current, "bun.lock")) || filePaths.has(pathInDirectory(current, "bun.lockb"))) return "bun"; + if (filePaths.has(pathInDirectory(current, "package-lock.json")) || filePaths.has(pathInDirectory(current, "npm-shrinkwrap.json"))) return "npm"; + if (!current) break; + const parent = path.posix.dirname(current); + current = parent === "." ? "" : parent; + } + return "npm"; +} + +function isValidationScript(name) { + return /^(?:check|verify|validate|lint|typecheck|type-check|test|build|audit|security)(?::[A-Za-z0-9._-]+)?$/.test(name); +} + +function validationScriptCompare(left, right) { + const leftRank = VALIDATION_SCRIPT_PRIORITY.indexOf(left); + const rightRank = VALIDATION_SCRIPT_PRIORITY.indexOf(right); + if (leftRank >= 0 || rightRank >= 0) { + if (leftRank < 0) return 1; + if (rightRank < 0) return -1; + if (leftRank !== rightRank) return leftRank - rightRank; + } + return lexicalCompare(left, right); +} + +function hasFilesUnder(records, directory, predicate) { + const prefix = directory ? `${directory}/` : ""; + return records.some((record) => record.path.startsWith(prefix) && predicate(record.path.slice(prefix.length))); +} + +async function detectValidationCommands(root, records, manifests, packages) { + const commands = []; + const filePaths = new Set(records.map((record) => record.path)); + + for (const { path: packagePath, value } of [...packages].sort((left, right) => lexicalCompare(left.path, right.path))) { + const scripts = value.scripts; + if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) continue; + + const directory = directoryOf(packagePath); + const manager = findNearestPackageManager(directory, filePaths); + const names = Object.keys(scripts).filter((name) => { + const body = scripts[name]; + return ( + isValidationScript(name) && + typeof body === "string" && + body.trim().length > 0 && + !/no test specified/i.test(body) + ); + }).sort(validationScriptCompare); + + for (const name of names) { + commands.push(commandInDirectory(directory, `${manager} run ${name}`)); + } + } + + const pubspecs = manifests.filter((filePath) => path.posix.basename(filePath).toLowerCase() === "pubspec.yaml"); + for (const pubspec of pubspecs) { + const directory = directoryOf(pubspec); + const content = await readRecord(root, pubspec).catch(() => ""); + if (!/\bsdk\s*:\s*flutter\b/i.test(content) && !/^\s*flutter\s*:/m.test(content)) continue; + commands.push(commandInDirectory(directory, "flutter analyze")); + if (hasFilesUnder(records, directory, (filePath) => /(?:^|\/)test\/.*_test\.dart$/.test(filePath) || /_test\.dart$/.test(filePath))) { + commands.push(commandInDirectory(directory, "flutter test")); + } + } + + const pythonManifestPattern = /^(?:pyproject\.toml|requirements(?:[-_.].*)?\.txt|pipfile|setup\.cfg|tox\.ini|pytest\.ini)$/i; + const pythonDirectories = [...new Set(manifests.filter((filePath) => pythonManifestPattern.test(path.posix.basename(filePath))).map(directoryOf))].sort(lexicalCompare); + for (const directory of pythonDirectories) { + const relevant = manifests.filter((filePath) => directoryOf(filePath) === directory && pythonManifestPattern.test(path.posix.basename(filePath))); + const content = (await Promise.all(relevant.map((filePath) => readRecord(root, filePath).catch(() => "")))).join("\n").toLowerCase(); + if (content.includes("pytest")) commands.push(commandInDirectory(directory, "python -m pytest")); + if (/\bruff\b/.test(content)) commands.push(commandInDirectory(directory, "ruff check .")); + if (/\bmypy\b/.test(content)) commands.push(commandInDirectory(directory, "mypy .")); + } + + for (const goMod of manifests.filter((filePath) => path.posix.basename(filePath).toLowerCase() === "go.mod")) { + const directory = directoryOf(goMod); + commands.push(commandInDirectory(directory, "go test ./...")); + commands.push(commandInDirectory(directory, "go vet ./...")); + } + + for (const cargoToml of manifests.filter((filePath) => path.posix.basename(filePath).toLowerCase() === "cargo.toml")) { + const directory = directoryOf(cargoToml); + commands.push(commandInDirectory(directory, "cargo check")); + commands.push(commandInDirectory(directory, "cargo test")); + } + + const gradleBuilds = manifests.filter((filePath) => /^build\.gradle(?:\.kts)?$/i.test(path.posix.basename(filePath))); + for (const buildFile of gradleBuilds) { + const directory = directoryOf(buildFile); + if (!filePaths.has(pathInDirectory(directory, "gradlew"))) continue; + const content = await readRecord(root, buildFile).catch(() => ""); + if (!/(?:\bjava\b|\bkotlin\b|com\.android)/i.test(content)) continue; + commands.push(commandInDirectory(directory, "./gradlew check")); + if (hasFilesUnder(records, directory, (filePath) => /(?:^|\/)src\/test\//.test(filePath))) { + commands.push(commandInDirectory(directory, "./gradlew test")); + } + } + + for (const packageSwift of manifests.filter((filePath) => path.posix.basename(filePath).toLowerCase() === "package.swift")) { + const directory = directoryOf(packageSwift); + commands.push(commandInDirectory(directory, "swift build")); + if (hasFilesUnder(records, directory, (filePath) => /(?:^|\/)tests\//i.test(filePath))) { + commands.push(commandInDirectory(directory, "swift test")); + } + } + + for (const composeFile of manifests.filter((filePath) => /^(?:compose|docker-compose)(?:\.[^.]+)?\.ya?ml$/i.test(path.posix.basename(filePath)))) { + const directory = directoryOf(composeFile); + const basename = path.posix.basename(composeFile); + const defaultName = /^(?:compose|docker-compose)\.ya?ml$/i.test(basename); + commands.push(commandInDirectory(directory, defaultName ? "docker compose config" : `docker compose -f ${shellQuote(basename)} config`)); + } + + for (const dockerfile of manifests.filter((filePath) => /^dockerfile(?:\..+)?$/i.test(path.posix.basename(filePath)))) { + const directory = directoryOf(dockerfile); + const basename = path.posix.basename(dockerfile); + commands.push(commandInDirectory(directory, `docker build -f ${shellQuote(basename)} .`)); + } + + return [...new Set(commands)]; +} + +export async function scanRepository(root = ".") { + const resolvedRoot = await resolveRoot(root); + const records = await collectRecords(resolvedRoot); + const manifests = records.map((record) => record.path).filter(isManifest).sort(lexicalCompare); + const languages = buildLanguages(records); + const packages = await readPackageManifests(resolvedRoot, manifests); + + return { + fileCount: records.length, + fingerprint: buildFingerprint(records), + roots: buildRoots(records), + extensions: buildExtensions(records), + languages, + manifests, + stack: await detectStack(resolvedRoot, records, manifests, packages, languages), + validationCommands: await detectValidationCommands(resolvedRoot, records, manifests, packages) + }; +} diff --git a/src/sync.mjs b/src/sync.mjs new file mode 100644 index 0000000..6b2cc29 --- /dev/null +++ b/src/sync.mjs @@ -0,0 +1,19 @@ +import { GENERATED_FILE } from "./contract.mjs"; +import { replaceGeneratedBlock } from "./context.mjs"; +import { ensureManagedDirectory, managedPath, readText, resolveRoot, writeFileAtomic } from "./fs.mjs"; +import { scanRepository } from "./scanner.mjs"; + +export async function syncRepository(input = ".") { + const root = await resolveRoot(input); + await ensureManagedDirectory(root, "AI_CONTEXT"); + const contextPath = managedPath(root, GENERATED_FILE); + const before = await readText(contextPath); + const facts = await scanRepository(root); + await ensureManagedDirectory(root, "AI_CONTEXT"); + const current = await readText(contextPath); + if (current !== before) throw new Error("CONTEXT.md cambió durante la sincronización; vuelve a intentarlo."); + const after = replaceGeneratedBlock(current, facts); + const changed = after !== before; + if (changed) await writeFileAtomic(root, contextPath, after); + return { root, changed, facts }; +} diff --git a/src/templates.mjs b/src/templates.mjs new file mode 100644 index 0000000..50b45b6 --- /dev/null +++ b/src/templates.mjs @@ -0,0 +1,17 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { REQUIRED_FILES } from "./contract.mjs"; + +const templateRoot = fileURLToPath(new URL("../templates/", import.meta.url)); + +export async function loadTemplates() { + const entries = await Promise.all( + REQUIRED_FILES.map(async (relativePath) => { + const filePath = path.join(templateRoot, relativePath); + const content = await readFile(filePath, "utf8"); + return [relativePath, content]; + }) + ); + return new Map(entries); +} diff --git a/templates/AGENTS.md b/templates/AGENTS.md new file mode 100644 index 0000000..a77b9bb --- /dev/null +++ b/templates/AGENTS.md @@ -0,0 +1,30 @@ +# Instrucciones del repositorio + +Empieza por [AI_CONTEXT/CONTEXT.md](AI_CONTEXT/CONTEXT.md). Consulta decisiones, tareas o aprendizajes solo cuando sean relevantes para el trabajo actual. + +## Contexto mínimo + +- [Contexto verificable](AI_CONTEXT/CONTEXT.md): propósito, stack e inventario comprobable. +- [Decisiones](AI_CONTEXT/DECISIONS.md): decisiones vigentes y su motivo. +- [Tareas](AI_CONTEXT/TASKS.md): trabajo activo; no es un historial. +- [Aprendizajes](AI_CONTEXT/LEARNINGS.md): hallazgos reutilizables y confirmados. + +## Responsabilidades de las herramientas + +- **Project Brain** actualiza únicamente los hechos verificables dentro del bloque generado de `CONTEXT.md`. +- **Graphify** deriva relaciones y visualizaciones a partir del repositorio; `graphify-out/` es reconstruible y no es contexto fuente. +- **Obsidian** navega y permite editar estos mismos archivos Markdown; ninguna función depende de wikilinks ni de plugins. + +## Reglas de trabajo + +- Distingue hechos observados de propuestas o supuestos. +- No edites el bloque generado de `CONTEXT.md`; usa `brain sync .`. +- Conserva el contenido manual fuera de los marcadores generados. +- Actualiza únicamente el archivo cuyo propósito corresponda al cambio. +- Usa el historial de Git para el pasado; evita duplicar bitácoras o reportes. +- No guardes credenciales, tokens, llaves privadas ni evidencia restringida. +- No borres datos, publiques ramas ni despliegues sin autorización explícita. + +## Validación + +Ejecuta `brain sync .` y después `brain doctor .` cuando cambien hechos verificables del repositorio. diff --git a/templates/AI_CONTEXT/CONTEXT.md b/templates/AI_CONTEXT/CONTEXT.md new file mode 100644 index 0000000..2d6839d --- /dev/null +++ b/templates/AI_CONTEXT/CONTEXT.md @@ -0,0 +1,31 @@ +--- +project_brain: 1 +role: context +--- + +# Contexto + + +## Hechos verificados del repositorio + +- Archivos analizados: 0 +- Huella del inventario: `sha256:pendiente` +- Stack: pendiente de sincronización +- Raíces principales: pendiente de sincronización +- Lenguajes: pendiente de sincronización +- Manifiestos: ninguno detectado + +### Comandos de validación detectados + +- Ninguno detectado + + +## Contexto manual + +- **Propósito:** describe en una frase qué resuelve el proyecto. +- **Alcance actual:** anota solo los límites que cambian cómo se debe trabajar. +- **Restricciones:** registra condiciones operativas o de privacidad vigentes. + +## Navegación + +Las reglas de trabajo viven en [AGENTS.md](../AGENTS.md). Continúa con [Decisiones](DECISIONS.md), [Tareas](TASKS.md) o [Aprendizajes](LEARNINGS.md) según lo que necesites. diff --git a/templates/AI_CONTEXT/DECISIONS.md b/templates/AI_CONTEXT/DECISIONS.md new file mode 100644 index 0000000..0660c6e --- /dev/null +++ b/templates/AI_CONTEXT/DECISIONS.md @@ -0,0 +1,19 @@ +--- +project_brain: 1 +role: decisions +--- + +# Decisiones + +Registra únicamente decisiones vigentes que condicionen trabajo futuro. + +## Plantilla + +### Título breve + +- **Estado:** propuesta | aceptada | reemplazada +- **Decisión:** qué se decidió. +- **Motivo:** evidencia o restricción que la justifica. +- **Consecuencia:** qué cambia al trabajar en el repositorio. + +Consulta primero el [Contexto](CONTEXT.md). diff --git a/templates/AI_CONTEXT/LEARNINGS.md b/templates/AI_CONTEXT/LEARNINGS.md new file mode 100644 index 0000000..831e0df --- /dev/null +++ b/templates/AI_CONTEXT/LEARNINGS.md @@ -0,0 +1,18 @@ +--- +project_brain: 1 +role: learnings +--- + +# Aprendizajes + +Guarda hallazgos confirmados que eviten repetir investigación o errores. + +## Plantilla + +### Hallazgo + +- **Evidencia:** dónde se comprobó. +- **Aplicación:** cuándo debe reutilizarse. +- **Límite:** en qué casos podría dejar de ser válido. + +Relaciona el hallazgo con una [Decisión](DECISIONS.md) solo si cambia una regla vigente. diff --git a/templates/AI_CONTEXT/TASKS.md b/templates/AI_CONTEXT/TASKS.md new file mode 100644 index 0000000..1d3daba --- /dev/null +++ b/templates/AI_CONTEXT/TASKS.md @@ -0,0 +1,18 @@ +--- +project_brain: 1 +role: tasks +--- + +# Tareas + +Mantén aquí solo trabajo activo. Cierra o elimina entradas terminadas; Git conserva el historial. + +## En curso + +- [ ] Resultado concreto — responsable — siguiente paso verificable. + +## Bloqueos + +- Ninguno. + +Las restricciones vigentes viven en [Contexto](CONTEXT.md) y las elecciones duraderas en [Decisiones](DECISIONS.md). diff --git a/test-support/helpers.mjs b/test-support/helpers.mjs new file mode 100644 index 0000000..a69f00e --- /dev/null +++ b/test-support/helpers.mjs @@ -0,0 +1,20 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +export async function temporaryRepository(t, name = "brain-test-") { + const root = await mkdtemp(path.join(os.tmpdir(), name)); + t.after(() => rm(root, { recursive: true, force: true })); + return root; +} + +export async function put(root, relativePath, content) { + const filePath = path.join(root, relativePath); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content, "utf8"); + return filePath; +} + +export function get(root, relativePath) { + return readFile(path.join(root, relativePath), "utf8"); +} diff --git a/test/cli.test.mjs b/test/cli.test.mjs new file mode 100644 index 0000000..ca94c1f --- /dev/null +++ b/test/cli.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { runCli } from "../src/cli.mjs"; + +function capture() { + const output = { logs: [], errors: [], warnings: [] }; + return { + output, + io: { + log: (value) => output.logs.push(String(value)), + error: (value) => output.errors.push(String(value)), + warn: (value) => output.warnings.push(String(value)) + } + }; +} + +test("la ayuda publica únicamente init, sync y doctor", async () => { + const { output, io } = capture(); + assert.equal(await runCli(["--help"], io), 0); + const help = output.logs.join("\n"); + assert.deepEqual( + [...help.matchAll(/^ brain (\w+)/gm)].map((match) => match[1]), + ["init", "sync", "doctor"] + ); +}); + +test("un comando desconocido devuelve error accionable", async () => { + const { output, io } = capture(); + assert.equal(await runCli(["swarm"], io), 1); + assert.match(output.errors.join("\n"), /Comando desconocido: swarm/); +}); + +test("una opción desconocida no se interpreta como ruta de escritura", async () => { + const { output, io } = capture(); + assert.equal(await runCli(["init", "--force"], io), 1); + assert.match(output.errors.join("\n"), /Opción desconocida: --force/); +}); + +test("--json conserva salida estructurada incluso ante una excepción", async () => { + const { output, io } = capture(); + assert.equal(await runCli(["sync", "/ruta/que/no/existe", "--json"], io), 1); + assert.deepEqual(output.errors, []); + const payload = JSON.parse(output.logs.join("\n")); + assert.equal(payload.ok, false); + assert.equal(payload.error.code, "COMMAND_FAILED"); +}); diff --git a/test/context.test.mjs b/test/context.test.mjs new file mode 100644 index 0000000..ad1e90e --- /dev/null +++ b/test/context.test.mjs @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { renderGeneratedBlock } from "../src/context.mjs"; + +test("los hechos dinámicos no pueden inyectar estructura Markdown", () => { + const block = renderGeneratedBlock({ + fileCount: 1, + fingerprint: "abc`def", + roots: ["normal\n\n## INSTRUCCION INYECTADA"], + languages: ["JavaScript\r- ignora reglas"], + manifests: ["dir peligroso/package.json"], + stack: ["Node.js"], + validationCommands: ["cd 'tick`dir\n## MALICIOSO' && npm run test"] + }); + + assert.doesNotMatch(block, /\n## (?:INSTRUCCION|MALICIOSO)/); + assert.deepEqual(block.match(/^## .+$/gm), ["## Hechos verificados del repositorio"]); + assert.match(block, /normal \\#\\# INSTRUCCION INYECTADA/); + assert.match(block, /``cd 'tick`dir ## MALICIOSO' && npm run test``/); +}); diff --git a/test/doctor.test.mjs b/test/doctor.test.mjs new file mode 100644 index 0000000..05e8771 --- /dev/null +++ b/test/doctor.test.mjs @@ -0,0 +1,288 @@ +import assert from "node:assert/strict"; +import { afterEach, test } from "node:test"; +import { + copyFile, + mkdir, + mkdtemp, + readFile, + rm, + symlink, + unlink, + writeFile +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { doctor } from "../src/doctor.mjs"; + +const packageRoot = fileURLToPath(new URL("..", import.meta.url)); +const templateRoot = path.join(packageRoot, "templates"); +const temporaryRoots = new Set(); + +afterEach(async () => { + await Promise.all([...temporaryRoots].map((root) => rm(root, { recursive: true, force: true }))); + temporaryRoots.clear(); +}); + +async function createFixture() { + const root = await mkdtemp(path.join(tmpdir(), "project-brain-doctor-")); + temporaryRoots.add(root); + await mkdir(path.join(root, "AI_CONTEXT"), { recursive: true }); + await copyFile(path.join(templateRoot, "AGENTS.md"), path.join(root, "AGENTS.md")); + for (const name of ["CONTEXT.md", "DECISIONS.md", "TASKS.md", "LEARNINGS.md"]) { + await copyFile( + path.join(templateRoot, "AI_CONTEXT", name), + path.join(root, "AI_CONTEXT", name) + ); + } + return root; +} + +async function append(relativeRoot, relativeFile, content) { + const target = path.join(relativeRoot, relativeFile); + const current = await readFile(target, "utf8"); + await writeFile(target, `${current}${content}`, "utf8"); +} + +function codes(diagnostics) { + return diagnostics.map((diagnostic) => diagnostic.code); +} + +test("acepta el contrato mínimo y entrega una estructura estable", async () => { + const root = await createFixture(); + + const first = await doctor(root); + const second = await doctor(root); + + assert.deepEqual(second, first); + assert.equal(first.ok, true); + assert.deepEqual(first.errors, []); + assert.deepEqual(first.warnings, []); + assert.deepEqual( + first.checks.map((check) => check.id), + [ + "canonical-files", + "generated-markers", + "size-limits", + "extra-context-files", + "links", + "duplicates", + "sensitive-data" + ] + ); + assert.ok(first.checks.every((check) => check.ok)); +}); + +test("reporta archivos canónicos ausentes y enlaces simbólicos sin seguirlos", async () => { + const root = await createFixture(); + await unlink(path.join(root, "AI_CONTEXT", "TASKS.md")); + await unlink(path.join(root, "AI_CONTEXT", "LEARNINGS.md")); + await symlink("DECISIONS.md", path.join(root, "AI_CONTEXT", "LEARNINGS.md")); + + const result = await doctor(root); + + assert.equal(result.ok, false); + assert.ok(codes(result.errors).includes("MISSING_CANONICAL_FILE")); + assert.ok(codes(result.errors).includes("SYMLINK_CANONICAL_FILE")); + assert.ok( + result.errors.some( + (diagnostic) => + diagnostic.code === "MISSING_CANONICAL_FILE" && + diagnostic.file === "AI_CONTEXT/TASKS.md" + ) + ); +}); + +test("exige una sola pareja ordenada de marcadores generados", async () => { + const root = await createFixture(); + await append(root, "AI_CONTEXT/CONTEXT.md", "\n\n"); + + const result = await doctor(root); + + assert.equal(result.ok, false); + assert.ok(codes(result.errors).includes("GENERATED_START_MARKER_COUNT")); +}); + +test("advierte límites por bytes, líneas y tamaño total", async () => { + const root = await createFixture(); + await append(root, "AI_CONTEXT/DECISIONS.md", `\n${"línea\n".repeat(241)}`); + await append(root, "AI_CONTEXT/LEARNINGS.md", `\n${"x".repeat(13_000)}\n`); + for (const file of [ + "AGENTS.md", + "AI_CONTEXT/CONTEXT.md", + "AI_CONTEXT/DECISIONS.md", + "AI_CONTEXT/TASKS.md", + "AI_CONTEXT/LEARNINGS.md" + ]) { + await append(root, file, `\n${"z".repeat(8_000)}\n`); + } + + const result = await doctor(root); + const warningCodes = codes(result.warnings); + + assert.ok(warningCodes.includes("FILE_BYTES_EXCEEDED")); + assert.ok(warningCodes.includes("FILE_LINES_EXCEEDED")); + assert.ok(warningCodes.includes("TOTAL_BYTES_EXCEEDED")); +}); + +test("detecta archivos extra y enlaces Markdown y wikilinks rotos", async () => { + const root = await createFixture(); + await writeFile(path.join(root, "AI_CONTEXT", "EXTRA.md"), "# Extra\n", "utf8"); + await append( + root, + "AI_CONTEXT/TASKS.md", + "\n- [Documento ausente](missing.md)\n- [[Nota inexistente]]\n" + ); + + const result = await doctor(root); + + assert.equal(result.ok, false); + assert.ok(codes(result.warnings).includes("EXTRA_CONTEXT_FILE")); + assert.ok(codes(result.errors).includes("BROKEN_MARKDOWN_LINK")); + assert.ok(codes(result.errors).includes("BROKEN_WIKILINK")); +}); + +test("rechaza enlaces que escapan mediante symlinks y reconoce destinos CommonMark", async () => { + const root = await createFixture(); + const outside = await mkdtemp(path.join(tmpdir(), "project-brain-outside-")); + temporaryRoots.add(outside); + await writeFile(path.join(outside, "outside.md"), "# Fuera\n", "utf8"); + await symlink(outside, path.join(root, "linked")); + await symlink("destino-inexistente", path.join(root, "dangling")); + await append( + root, + "AI_CONTEXT/TASKS.md", + [ + "", + "- [Escape](../linked/outside.md)", + "- [Colgante](../dangling/missing.md)", + "- [Paréntesis](missing(1).md)", + "- [Espacio](missing\\ file.md)", + "" + ].join("\n") + ); + + const result = await doctor(root); + assert.ok(codes(result.errors).includes("RELATIVE_LINK_OUTSIDE_ROOT")); + assert.ok(result.errors.filter((diagnostic) => diagnostic.code === "BROKEN_MARKDOWN_LINK").length >= 3); +}); + +test("resuelve caracteres reservados codificados antes de interpretar fragmentos", async () => { + const root = await createFixture(); + await writeFile(path.join(root, "AI_CONTEXT", "file#name.md"), "# Hash\n", "utf8"); + const links = ["- [Hash](file%23name.md)"]; + if (process.platform !== "win32") { + await writeFile(path.join(root, "AI_CONTEXT", "file?name.md"), "# Query\n", "utf8"); + links.push("- [Query](file%3Fname.md)"); + } + await append(root, "AI_CONTEXT/TASKS.md", `\n${links.join("\n")}\n`); + + const result = await doctor(root); + assert.equal(result.errors.some((diagnostic) => diagnostic.code === "BROKEN_MARKDOWN_LINK"), false); +}); + +test("no confunde texto o código inline con enlaces Markdown", async () => { + const root = await createFixture(); + await append( + root, + "AI_CONTEXT/TASKS.md", + "\nTexto literal array](missing.md)\nCódigo: ``array](also-missing.md)``\n" + ); + + const result = await doctor(root); + assert.equal(result.errors.some((diagnostic) => diagnostic.code === "BROKEN_MARKDOWN_LINK"), false); +}); + +test("advierte duplicados y datos personales, y rechaza secretos materiales", async () => { + const root = await createFixture(); + const repeated = + "Este bloque confirmado explica una restricción operativa importante y debe existir en una sola nota para evitar versiones contradictorias durante el trabajo futuro."; + await append(root, "AI_CONTEXT/DECISIONS.md", `\n## Regla repetida\n\n${repeated}\n`); + await append(root, "AI_CONTEXT/LEARNINGS.md", `\n## Regla repetida\n\n${repeated}\n`); + await append( + root, + "AI_CONTEXT/TASKS.md", + [ + "", + "ACCESS_TOKEN=\"ghp_abcdefghijklmnopqrstuvwxyz123456\"", + "AWS_SECRET_ACCESS_KEY=material-aws-secret-value", + "STRIPE_SECRET_KEY=material-stripe-secret-value", + "GITHUB_TOKEN=material-github-token-value", + "Correo operativo: persona@dominio.mx", + "Teléfono: +52 662 123 4567", + "Ejemplos inocuos: user@example.com, +1 202-555-0123 y TOKEN=", + "-----BEGIN OPENSSH PRIVATE KEY-----", + "contenido-no-real", + "-----END OPENSSH PRIVATE KEY-----", + "" + ].join("\n") + ); + await writeFile( + path.join(root, "AI_CONTEXT", "EXTRA.md"), + "API_KEY=sk-proj-abcdefghijklmnopqrstuvwxyz123456\n", + "utf8" + ); + + const result = await doctor(root); + const errorCodes = codes(result.errors); + const warningCodes = codes(result.warnings); + + assert.equal(result.ok, false); + assert.ok(errorCodes.includes("EXPOSED_TOKEN")); + assert.ok(errorCodes.includes("PRIVATE_KEY")); + assert.ok(errorCodes.includes("EXPOSED_CREDENTIAL")); + assert.ok( + result.errors.some( + (diagnostic) => diagnostic.code === "EXPOSED_TOKEN" && diagnostic.file === "AI_CONTEXT/EXTRA.md" + ) + ); + assert.ok(warningCodes.includes("SIGNIFICANT_DUPLICATE")); + assert.ok(warningCodes.includes("PERSONAL_EMAIL")); + assert.ok(warningCodes.includes("PERSONAL_PHONE")); + assert.equal( + result.warnings.filter((diagnostic) => diagnostic.code === "PERSONAL_EMAIL").length, + 1 + ); + assert.equal( + result.warnings.filter((diagnostic) => diagnostic.code === "PERSONAL_PHONE").length, + 1 + ); +}); + +test("no carga archivos extra de texto desproporcionados", async () => { + const root = await createFixture(); + await writeFile( + path.join(root, "AI_CONTEXT", "HUGE.md"), + "x".repeat(1024 * 1024 + 1), + "utf8" + ); + + const result = await doctor(root); + assert.equal(result.ok, false); + assert.ok(codes(result.errors).includes("CONTENT_TOO_LARGE_TO_AUDIT")); +}); + +test("detecta bloques de llave privada PGP", async () => { + const root = await createFixture(); + await append( + root, + "AI_CONTEXT/LEARNINGS.md", + "\n-----BEGIN PGP PRIVATE KEY BLOCK-----\nmaterial\n-----END PGP PRIVATE KEY BLOCK-----\n" + ); + + const result = await doctor(root); + assert.ok(codes(result.errors).includes("PRIVATE_KEY")); +}); + +test("detecta tokens npm en sintaxis npmrc", async () => { + const root = await createFixture(); + await append( + root, + "AI_CONTEXT/TASKS.md", + "\n//registry.npmjs.org/:_authToken=npm_abcdefghijklmnopqrstuvwxyz1234567890\n" + ); + + const result = await doctor(root); + assert.ok(codes(result.errors).includes("EXPOSED_TOKEN")); +}); diff --git a/test/init.test.mjs b/test/init.test.mjs new file mode 100644 index 0000000..d55592b --- /dev/null +++ b/test/init.test.mjs @@ -0,0 +1,80 @@ +import assert from "node:assert/strict"; +import { chmod, mkdir, readdir, symlink } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; +import { REQUIRED_FILES } from "../src/contract.mjs"; +import { initRepository } from "../src/init.mjs"; +import { get, put, temporaryRepository } from "../test-support/helpers.mjs"; + +test("init crea exactamente el contrato mínimo y sincroniza hechos", async (t) => { + const parent = await temporaryRepository(t); + const root = path.join(parent, "nuevo"); + const result = await initRepository(root); + + assert.deepEqual(result.created, REQUIRED_FILES); + const rootEntries = (await readdir(root)).sort(); + assert.deepEqual(rootEntries, ["AGENTS.md", "AI_CONTEXT"]); + assert.deepEqual((await readdir(path.join(root, "AI_CONTEXT"))).sort(), [ + "CONTEXT.md", "DECISIONS.md", "LEARNINGS.md", "TASKS.md" + ]); + const context = await get(root, "AI_CONTEXT/CONTEXT.md"); + assert.match(context, /Huella del inventario: `sha256:[a-f0-9]{64}`/); +}); + +test("init nunca sobrescribe contenido manual existente", async (t) => { + const root = await temporaryRepository(t); + await initRepository(root); + const manual = "# Tareas\n\n- [ ] Mantener esta línea exactamente.\n"; + await put(root, "AI_CONTEXT/TASKS.md", manual); + + const result = await initRepository(root); + assert.deepEqual(result.created, []); + assert.deepEqual(result.preserved, REQUIRED_FILES); + assert.equal(await get(root, "AI_CONTEXT/TASKS.md"), manual); +}); + +test("init rechaza enlaces simbólicos canónicos antes de escribir", async (t) => { + const root = await temporaryRepository(t); + await put(root, "destino.md", "no tocar\n"); + await symlink(path.join(root, "destino.md"), path.join(root, "AGENTS.md")); + + await assert.rejects(() => initRepository(root), /enlaces simbólicos/); + await assert.rejects(() => readdir(path.join(root, "AI_CONTEXT")), /ENOENT/); +}); + +test("init rechaza AI_CONTEXT simbólico sin escribir fuera del contrato", async (t) => { + const root = await temporaryRepository(t); + await mkdir(path.join(root, "destino")); + await symlink("destino", path.join(root, "AI_CONTEXT")); + + await assert.rejects(() => initRepository(root), /directorios simbólicos/); + await assert.rejects(() => get(root, "AGENTS.md"), /ENOENT/); + assert.deepEqual(await readdir(path.join(root, "destino")), []); +}); + +test("init valida marcadores existentes antes de crear archivos faltantes", async (t) => { + const root = await temporaryRepository(t); + await put(root, "AI_CONTEXT/CONTEXT.md", "# Contexto inválido\n"); + + await assert.rejects(() => initRepository(root), /exactamente un bloque/); + assert.deepEqual(await readdir(root), ["AI_CONTEXT"]); + assert.deepEqual(await readdir(path.join(root, "AI_CONTEXT")), ["CONTEXT.md"]); +}); + +test("init comprueba que el repositorio sea legible antes de publicar archivos", async (t) => { + if (process.platform === "win32") { + t.skip("los permisos POSIX no aplican en Windows"); + return; + } + const root = await temporaryRepository(t); + const blocked = path.join(root, "blocked"); + await mkdir(blocked); + await put(root, "blocked/file.txt", "privado\n"); + await chmod(blocked, 0o000); + try { + await assert.rejects(() => initRepository(root), /EACCES|permission denied/i); + } finally { + await chmod(blocked, 0o700); + } + assert.deepEqual(await readdir(root), ["blocked"]); +}); diff --git a/test/scanner.test.mjs b/test/scanner.test.mjs new file mode 100644 index 0000000..e80a807 --- /dev/null +++ b/test/scanner.test.mjs @@ -0,0 +1,239 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { access, chmod, mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { scanRepository } from "../src/scanner.mjs"; + +async function temporaryRepository(t) { + const directory = await mkdtemp(path.join(os.tmpdir(), "brain-scanner-")); + t.after(() => rm(directory, { recursive: true, force: true })); + return directory; +} + +async function put(root, relativePath, content = "") { + const filePath = path.join(root, ...relativePath.split("/")); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content); +} + +test("genera un inventario determinista y excluye artefactos, builds y cachés", async (t) => { + const root = await temporaryRepository(t); + await put(root, "package.json", JSON.stringify({ + scripts: { + lint: "eslint .", + test: "node --test", + build: "tsc" + }, + dependencies: { + express: "1.0.0", + next: "1.0.0", + react: "1.0.0", + vite: "1.0.0" + } + })); + await put(root, "package-lock.json", "{}\n"); + await put(root, "tsconfig.json", "{}\n"); + await put(root, "src/app.tsx", "export const app = true;\n"); + await put(root, "test/app.test.ts", "export const test = true;\n"); + await put(root, "Dockerfile", "FROM scratch\n"); + await put(root, "compose.yaml", "services: {}\n"); + + for (const ignored of [ + ".git/internal", + "node_modules/pkg/index.js", + "dist/app.js", + "build/app.js", + "coverage/result.json", + "BRAIN/context.md", + ".brain/state.json", + "graphify-out/graph.json", + ".graphify/cache.json", + ".obsidian/workspace.json", + "AI_CONTEXT/CONTEXT.md", + ".cache/cache.bin", + "src/__pycache__/module.pyc" + ]) await put(root, ignored, "ignored"); + + const first = await scanRepository(root); + const second = await scanRepository(root); + + assert.deepEqual(second, first); + assert.equal(first.fileCount, 7); + assert.match(first.fingerprint, /^[a-f0-9]{64}$/); + assert.deepEqual(first.roots, ["src", "test"]); + assert.deepEqual(first.extensions, { + "(none)": 1, + ".json": 3, + ".ts": 1, + ".tsx": 1, + ".yaml": 1 + }); + assert.deepEqual(first.languages, ["TypeScript"]); + assert.deepEqual(first.manifests, ["Dockerfile", "compose.yaml", "package-lock.json", "package.json", "tsconfig.json"]); + assert.deepEqual(first.stack, ["Node.js", "TypeScript", "React", "Next.js", "Vite", "Express", "Docker"]); + assert.deepEqual(first.validationCommands, [ + "npm run lint", + "npm run test", + "npm run build", + "docker compose config", + "docker build -f Dockerfile ." + ]); + + await put(root, "BRAIN/new-output.md", "un cambio ignorado"); + assert.equal((await scanRepository(root)).fingerprint, first.fingerprint); +}); + +test("la huella usa rutas y tamaños, no timestamps ni contenido del mismo tamaño", async (t) => { + const root = await temporaryRepository(t); + await put(root, "src/value.js", "abc"); + const initial = await scanRepository(root); + + await put(root, "src/value.js", "xyz"); + const sameSize = await scanRepository(root); + assert.equal(sameSize.fingerprint, initial.fingerprint); + + await put(root, "src/value.js", "longer"); + const differentSize = await scanRepository(root); + assert.notEqual(differentSize.fingerprint, initial.fingerprint); +}); + +test("prefiere git ls-files y respeta exclusiones estándar", async (t) => { + try { + execFileSync("git", ["--version"], { stdio: "ignore" }); + } catch { + t.skip("git no está disponible"); + return; + } + + const root = await temporaryRepository(t); + execFileSync("git", ["init", "-q", root]); + await put(root, ".gitignore", "ignored-by-git.txt\n"); + await put(root, "src/tracked.js", "tracked\n"); + await put(root, "src/untracked.js", "untracked\n"); + await put(root, "ignored-by-git.txt", "ignored\n"); + execFileSync("git", ["-C", root, "add", ".gitignore", "src/tracked.js"]); + + const scan = await scanRepository(root); + assert.equal(scan.fileCount, 3); + assert.deepEqual(scan.roots, ["src"]); + + await put(root, "ignored-by-git.txt", "ignored, incluso si cambia de tamaño\n"); + assert.equal((await scanRepository(root)).fingerprint, scan.fingerprint); +}); + +test("neutraliza hooks fsmonitor al consultar el inventario de Git", async (t) => { + try { + execFileSync("git", ["--version"], { stdio: "ignore" }); + } catch { + t.skip("git no está disponible"); + return; + } + + const root = await temporaryRepository(t); + const marker = path.join(root, "fsmonitor-ran"); + const hook = path.join(root, "fsmonitor-hook.sh"); + execFileSync("git", ["init", "-q", root]); + await writeFile(hook, `#!/bin/sh\nprintf ran > ${JSON.stringify(marker)}\n`, "utf8"); + await chmod(hook, 0o755); + execFileSync("git", ["-C", root, "config", "core.fsmonitor", "./fsmonitor-hook.sh"]); + + await scanRepository(root); + await assert.rejects(() => access(marker), /ENOENT/); +}); + +test("descarta rutas Git que escapan mediante un directorio simbólico", async (t) => { + try { + execFileSync("git", ["--version"], { stdio: "ignore" }); + } catch { + t.skip("git no está disponible"); + return; + } + + const root = await temporaryRepository(t); + const outside = await temporaryRepository(t); + execFileSync("git", ["init", "-q", root]); + await put(root, "dir/package.json", "{}\n"); + execFileSync("git", ["-C", root, "add", "dir/package.json"]); + await rm(path.join(root, "dir"), { recursive: true }); + await put(outside, "package.json", JSON.stringify({ dependencies: { express: "latest" } })); + await symlink(outside, path.join(root, "dir")); + + const scan = await scanRepository(root); + assert.equal(scan.manifests.includes("dir/package.json"), false); + assert.equal(scan.stack.includes("Express"), false); +}); + +test("no carga semánticamente manifests desproporcionados", async (t) => { + const root = await temporaryRepository(t); + const padding = "x".repeat(1024 * 1024 + 1); + await put(root, "package.json", JSON.stringify({ dependencies: { express: "latest" }, padding })); + + const scan = await scanRepository(root); + assert.ok(scan.manifests.includes("package.json")); + assert.deepEqual(scan.stack, ["Node.js"]); +}); + +test("detecta stacks y validaciones solo cuando hay evidencia real", async (t) => { + const root = await temporaryRepository(t); + await put(root, "flutter/pubspec.yaml", "dependencies:\n flutter:\n sdk: flutter\n"); + await put(root, "flutter/lib/main.dart", "void main() {}\n"); + await put(root, "flutter/test/widget_test.dart", "void main() {}\n"); + + await put(root, "python/pyproject.toml", "[project]\ndependencies = ['pytest', 'ruff', 'mypy']\n"); + await put(root, "python/tests/test_app.py", "def test_app(): pass\n"); + + await put(root, "go/go.mod", "module example.test/app\n"); + await put(root, "go/main.go", "package main\n"); + + await put(root, "rust/Cargo.toml", "[package]\nname = 'app'\nversion = '0.1.0'\n"); + await put(root, "rust/src/lib.rs", "pub fn app() {}\n"); + + await put(root, "gradle/build.gradle.kts", "plugins { kotlin(\"jvm\") version \"2.0.0\" }\n"); + await put(root, "gradle/gradlew", "#!/bin/sh\n"); + await put(root, "gradle/src/main/kotlin/App.kt", "class App\n"); + await put(root, "gradle/src/main/java/App.java", "class App {}\n"); + await put(root, "gradle/src/test/kotlin/AppTest.kt", "class AppTest\n"); + + await put(root, "swift/Package.swift", "// swift-tools-version: 6.0\n"); + await put(root, "swift/Sources/App/main.swift", "print(\"ok\")\n"); + await put(root, "swift/Tests/AppTests/AppTests.swift", "// test\n"); + + const scan = await scanRepository(root); + assert.deepEqual(scan.stack, ["Flutter", "Python", "Go", "Rust", "Gradle", "Kotlin", "Java", "Swift"]); + for (const command of [ + "cd flutter && flutter analyze", + "cd flutter && flutter test", + "cd python && python -m pytest", + "cd python && ruff check .", + "cd python && mypy .", + "cd go && go test ./...", + "cd go && go vet ./...", + "cd rust && cargo check", + "cd rust && cargo test", + "cd gradle && ./gradlew check", + "cd gradle && ./gradlew test", + "cd swift && swift build", + "cd swift && swift test" + ]) assert.ok(scan.validationCommands.includes(command), `Falta comando detectado: ${command}`); +}); + +test("no inventa scripts de Node y omite enlaces simbólicos", async (t) => { + const root = await temporaryRepository(t); + await put(root, "package.json", JSON.stringify({ + scripts: { + dev: "node app.js", + test: "echo 'Error: no test specified' && exit 1" + } + })); + await put(root, "app.js", "console.log('ok');\n"); + await put(root, "outside.txt", "outside\n"); + await symlink(path.join(root, "outside.txt"), path.join(root, "linked.txt")); + + const scan = await scanRepository(root); + assert.deepEqual(scan.validationCommands, []); + assert.equal(scan.fileCount, 3); + assert.deepEqual(scan.stack, ["Node.js"]); +}); diff --git a/test/scope.test.mjs b/test/scope.test.mjs new file mode 100644 index 0000000..f118d26 --- /dev/null +++ b/test/scope.test.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const ignored = new Set([".git", "node_modules", "coverage", "dist", "build"]); + +async function projectFiles(directory = root) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + if (ignored.has(entry.name)) continue; + const target = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...(await projectFiles(target))); + else if (entry.isFile()) files.push(path.relative(root, target)); + } + return files.sort(); +} + +test("el producto completo permanece debajo de 50 archivos", async () => { + const files = await projectFiles(); + assert.ok(files.length < 50, `el repositorio volvió a crecer a ${files.length} archivos`); +}); + +test("el paquete no instala runtimes ni publica otros comandos", async () => { + const packageJson = JSON.parse(await readFile(path.join(root, "package.json"), "utf8")); + assert.deepEqual(Object.keys(packageJson.bin), ["brain"]); + assert.equal(packageJson.dependencies, undefined); + assert.equal(packageJson.devDependencies, undefined); + assert.equal(packageJson.main, "./src/index.mjs"); + assert.equal(packageJson.exports["."], "./src/index.mjs"); + + const forbiddenRoots = ["agents", "analysis", "core", "governance", "memory", "orchestrator", "patches", "reports"]; + const files = await projectFiles(); + for (const directory of forbiddenRoots) { + assert.equal( + files.some((file) => file === directory || file.startsWith(`${directory}/`)), + false, + `el runtime retirado reapareció: ${directory}/` + ); + } +}); + +test("la API pública expone operaciones estables sin añadir comandos", async () => { + const api = await import("../src/index.mjs"); + for (const name of ["doctor", "initRepository", "scanRepository", "syncRepository"]) { + assert.equal(typeof api[name], "function", `falta export público: ${name}`); + } +}); diff --git a/test/sync.test.mjs b/test/sync.test.mjs new file mode 100644 index 0000000..05639dc --- /dev/null +++ b/test/sync.test.mjs @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { chmod, stat } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; +import { END_MARKER } from "../src/contract.mjs"; +import { initRepository } from "../src/init.mjs"; +import { syncRepository } from "../src/sync.mjs"; +import { get, put, temporaryRepository } from "../test-support/helpers.mjs"; + +test("sync cambia solo el bloque generado, preserva lo manual y es idempotente", async (t) => { + const root = await temporaryRepository(t); + await put(root, "package.json", JSON.stringify({ + scripts: { check: "node --check src/index.js", test: "node --test" }, + dependencies: { react: "latest" } + }, null, 2)); + await put(root, "src/index.js", "export const answer = 42;\n"); + await initRepository(root); + + const original = await get(root, "AI_CONTEXT/CONTEXT.md"); + const withManual = original.replace(END_MARKER, `${END_MARKER}\n\nNota manual indeleble.`); + await put(root, "AI_CONTEXT/CONTEXT.md", withManual); + await put(root, "src/extra.js", "export const extra = true;\n"); + + const first = await syncRepository(root); + const after = await get(root, "AI_CONTEXT/CONTEXT.md"); + assert.equal(first.changed, true); + assert.match(after, /Nota manual indeleble\./); + assert.match(after, /Node\.js/); + assert.match(after, /React/); + assert.match(after, /`npm run check`/); + + const second = await syncRepository(root); + assert.equal(second.changed, false); + assert.equal(await get(root, "AI_CONTEXT/CONTEXT.md"), after); +}); +test("sync falla de forma segura si faltan marcadores", async (t) => { + const root = await temporaryRepository(t); + await initRepository(root); + const invalid = "# Contexto\n\nContenido manual sin bloque.\n"; + await put(root, "AI_CONTEXT/CONTEXT.md", invalid); + + await assert.rejects(() => syncRepository(root), /exactamente un bloque/); + assert.equal(await get(root, "AI_CONTEXT/CONTEXT.md"), invalid); +}); + +test("sync preserva los permisos del archivo de contexto", async (t) => { + const root = await temporaryRepository(t); + await initRepository(root); + const contextPath = path.join(root, "AI_CONTEXT", "CONTEXT.md"); + await chmod(contextPath, 0o600); + await put(root, "nuevo.js", "export default true;\n"); + + await syncRepository(root); + assert.equal((await stat(contextPath)).mode & 0o777, 0o600); +}); diff --git a/test/templates.test.mjs b/test/templates.test.mjs new file mode 100644 index 0000000..949a171 --- /dev/null +++ b/test/templates.test.mjs @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +import { readFile, readdir, stat } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { CONTRACT, REQUIRED_FILES } from "../src/contract.mjs"; + +const projectRoot = fileURLToPath(new URL("../", import.meta.url)); +const templatesRoot = path.join(projectRoot, "templates"); + +async function listFiles(directory, prefix = "") { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const relativePath = path.posix.join(prefix, entry.name); + if (entry.isDirectory()) { + files.push(...(await listFiles(path.join(directory, entry.name), relativePath))); + } else if (entry.isFile()) { + files.push(relativePath); + } + } + + return files.sort(); +} + +function parseFrontmatter(content) { + const lines = content.split(/\r?\n/); + assert.equal(lines[0], "---", "el frontmatter debe iniciar en la primera línea"); + const end = lines.indexOf("---", 1); + assert.ok(end > 1, "el frontmatter debe cerrar con ---"); + + const entries = Object.fromEntries( + lines.slice(1, end).map((line) => { + const separator = line.indexOf(":"); + assert.ok(separator > 0, `entrada YAML simple inválida: ${line}`); + return [line.slice(0, separator).trim(), line.slice(separator + 1).trim()]; + }) + ); + + return { entries, end }; +} + +function markdownTargets(content) { + return [...content.matchAll(/(? match[1]); +} + +test("las plantillas implementan exactamente el contrato mínimo", async () => { + assert.deepEqual(await listFiles(templatesRoot), [...REQUIRED_FILES].sort()); + + let totalBytes = 0; + for (const relativePath of REQUIRED_FILES) { + const filePath = path.join(templatesRoot, relativePath); + const info = await stat(filePath); + const content = await readFile(filePath, "utf8"); + const lineCount = content.split(/\r?\n/).length; + + assert.ok(info.size <= CONTRACT.limits.bytesPerFile, `${relativePath} excede el límite por archivo`); + assert.ok(lineCount <= CONTRACT.limits.linesPerFile, `${relativePath} excede el límite de líneas`); + totalBytes += info.size; + } + + assert.ok(totalBytes <= CONTRACT.limits.totalBytes, "las plantillas exceden el límite total"); +}); + +test("las notas usan frontmatter simple y roles únicos", async () => { + const expectedRoles = new Map([ + ["AI_CONTEXT/CONTEXT.md", "context"], + ["AI_CONTEXT/DECISIONS.md", "decisions"], + ["AI_CONTEXT/TASKS.md", "tasks"], + ["AI_CONTEXT/LEARNINGS.md", "learnings"] + ]); + + for (const [relativePath, role] of expectedRoles) { + const content = await readFile(path.join(templatesRoot, relativePath), "utf8"); + const { entries } = parseFrontmatter(content); + assert.deepEqual(Object.keys(entries).sort(), ["project_brain", "role"]); + assert.equal(entries.project_brain, "1"); + assert.equal(entries.role, role); + } +}); + +test("todos los enlaces Markdown internos existen y conectan el contexto", async () => { + const contents = new Map( + await Promise.all( + REQUIRED_FILES.map(async (relativePath) => [ + relativePath, + await readFile(path.join(templatesRoot, relativePath), "utf8") + ]) + ) + ); + + const graph = new Map(REQUIRED_FILES.map((relativePath) => [relativePath, []])); + for (const [relativePath, content] of contents) { + assert.doesNotMatch(content, /\[\[[^\]]+\]\]/, `${relativePath} no debe requerir wikilinks`); + + for (const target of markdownTargets(content)) { + assert.doesNotMatch(target, /^[a-z][a-z0-9+.-]*:/i, `${relativePath} contiene un enlace externo`); + const cleanTarget = decodeURIComponent(target.split("#", 1)[0]); + const absoluteTarget = path.resolve(path.dirname(path.join(templatesRoot, relativePath)), cleanTarget); + const templateRelativeTarget = path.relative(templatesRoot, absoluteTarget).split(path.sep).join("/"); + assert.ok( + REQUIRED_FILES.includes(templateRelativeTarget), + `${relativePath} enlaza fuera del contrato mínimo: ${target}` + ); + assert.ok((await stat(absoluteTarget)).isFile(), `${relativePath} enlaza a un archivo inexistente: ${target}`); + graph.get(relativePath).push(templateRelativeTarget); + } + } + + const visited = new Set(); + const queue = ["AGENTS.md"]; + while (queue.length > 0) { + const current = queue.shift(); + if (visited.has(current)) continue; + visited.add(current); + queue.push(...graph.get(current)); + } + + assert.deepEqual([...visited].sort(), [...REQUIRED_FILES].sort()); +}); + +test("las responsabilidades de Project Brain, Graphify y Obsidian son explícitas", async () => { + const agents = await readFile(path.join(templatesRoot, "AGENTS.md"), "utf8"); + assert.match(agents, /Project Brain.*hechos verificables/i); + assert.match(agents, /Graphify.*relaciones y visualizaciones/i); + assert.match(agents, /Obsidian.*archivos Markdown/i); + assert.match(agents, /no es contexto fuente/i); + + const contextNotes = ( + await Promise.all( + REQUIRED_FILES.filter((relativePath) => relativePath !== "AGENTS.md").map((relativePath) => + readFile(path.join(templatesRoot, relativePath), "utf8") + ) + ) + ).join("\n"); + assert.doesNotMatch( + contextNotes, + /\b(?:Project Brain|Graphify|Obsidian)\b/i, + "la explicación de herramientas debe vivir solo en AGENTS.md" + ); + + const context = await readFile(path.join(templatesRoot, CONTRACT.generatedFile), "utf8"); + assert.equal(context.match(//g)?.length, 1); + assert.equal(context.match(//g)?.length, 1); +}); diff --git a/tests/fixtures/dev-agent-repo/package.json b/tests/fixtures/dev-agent-repo/package.json deleted file mode 100644 index 44388f0..0000000 --- a/tests/fixtures/dev-agent-repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "dev-agent-fixture", - "version": "1.0.0", - "private": true -} diff --git a/tests/fixtures/dev-agent-repo/src/app.ts b/tests/fixtures/dev-agent-repo/src/app.ts deleted file mode 100644 index 220afce..0000000 --- a/tests/fixtures/dev-agent-repo/src/app.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { runService } from "./service"; -import { formatRepeatedA } from "./repeated-a"; - -export async function runApp(input: string): Promise { - const result = await runService(input); - return formatRepeatedA(result); -} diff --git a/tests/fixtures/dev-agent-repo/src/repeated-a.ts b/tests/fixtures/dev-agent-repo/src/repeated-a.ts deleted file mode 100644 index b2cb553..0000000 --- a/tests/fixtures/dev-agent-repo/src/repeated-a.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function formatRepeatedA(input: string): string { - const value = input.trim(); - const normalized = value.toLowerCase(); - const pieces = normalized.split(":"); - return pieces.join("-"); -} diff --git a/tests/fixtures/dev-agent-repo/src/repeated-b.ts b/tests/fixtures/dev-agent-repo/src/repeated-b.ts deleted file mode 100644 index 56cb7df..0000000 --- a/tests/fixtures/dev-agent-repo/src/repeated-b.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function formatRepeatedB(input: string): string { - const value = input.trim(); - const normalized = value.toLowerCase(); - const pieces = normalized.split(":"); - return pieces.join("-"); -} diff --git a/tests/fixtures/dev-agent-repo/src/service.ts b/tests/fixtures/dev-agent-repo/src/service.ts deleted file mode 100644 index d7fd9b6..0000000 --- a/tests/fixtures/dev-agent-repo/src/service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { runApp } from "./app"; -import { formatRepeatedB } from "./repeated-b"; -import { unusedFlag } from "./shared"; - -export async function runService(input: string): Promise { - if (unusedFlag) { - return formatRepeatedB(input); - } - - return runApp(input); -} diff --git a/tests/fixtures/dev-agent-repo/src/shared.ts b/tests/fixtures/dev-agent-repo/src/shared.ts deleted file mode 100644 index 286f1e7..0000000 --- a/tests/fixtures/dev-agent-repo/src/shared.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const unusedFlag = false; - -export function unusedHelper(input: string): string { - return input.trim().toUpperCase(); -} diff --git a/tests/fixtures/dev-agent-repo/tsconfig.json b/tests/fixtures/dev-agent-repo/tsconfig.json deleted file mode 100644 index cd2e0a4..0000000 --- a/tests/fixtures/dev-agent-repo/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", - "strict": true, - "esModuleInterop": true - }, - "include": ["src/**/*.ts"] -} diff --git a/tests/fixtures/multi-repo-workspace/CashCalculator/app.py b/tests/fixtures/multi-repo-workspace/CashCalculator/app.py deleted file mode 100644 index 439396b..0000000 --- a/tests/fixtures/multi-repo-workspace/CashCalculator/app.py +++ /dev/null @@ -1,8 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - - -@app.get("/health") -def health(): - return {"ok": True} diff --git a/tests/fixtures/multi-repo-workspace/CashCalculator/requirements.txt b/tests/fixtures/multi-repo-workspace/CashCalculator/requirements.txt deleted file mode 100644 index 0647450..0000000 --- a/tests/fixtures/multi-repo-workspace/CashCalculator/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -flask==3.0.3 diff --git a/tests/fixtures/multi-repo-workspace/ERP/package.json b/tests/fixtures/multi-repo-workspace/ERP/package.json deleted file mode 100644 index b03263d..0000000 --- a/tests/fixtures/multi-repo-workspace/ERP/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "erp", - "version": "1.0.0", - "dependencies": { - "express": "^4.19.2" - } -} diff --git a/tests/fixtures/multi-repo-workspace/ERP/src/server.ts b/tests/fixtures/multi-repo-workspace/ERP/src/server.ts deleted file mode 100644 index 622bb33..0000000 --- a/tests/fixtures/multi-repo-workspace/ERP/src/server.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express from "express"; - -const app = express(); - -app.get("/health", (_req, res) => { - res.json({ ok: true }); -}); - -export default app; diff --git a/tests/fixtures/multi-repo-workspace/FrontendPortal/package.json b/tests/fixtures/multi-repo-workspace/FrontendPortal/package.json deleted file mode 100644 index 63cd4b2..0000000 --- a/tests/fixtures/multi-repo-workspace/FrontendPortal/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "frontend-portal", - "version": "1.0.0", - "dependencies": { - "react": "^18.3.1" - } -} diff --git a/tests/fixtures/multi-repo-workspace/FrontendPortal/src/app.tsx b/tests/fixtures/multi-repo-workspace/FrontendPortal/src/app.tsx deleted file mode 100644 index 5c0f2bd..0000000 --- a/tests/fixtures/multi-repo-workspace/FrontendPortal/src/app.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function App() { - return
FrontendPortal
; -} diff --git a/tests/fixtures/multi-repo-workspace/project-brain/cli/main.ts b/tests/fixtures/multi-repo-workspace/project-brain/cli/main.ts deleted file mode 100644 index 9739394..0000000 --- a/tests/fixtures/multi-repo-workspace/project-brain/cli/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function runCli(): string { - return "project-brain"; -} diff --git a/tests/fixtures/multi-repo-workspace/project-brain/package.json b/tests/fixtures/multi-repo-workspace/project-brain/package.json deleted file mode 100644 index ef89b08..0000000 --- a/tests/fixtures/multi-repo-workspace/project-brain/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "project-brain", - "version": "1.0.0", - "dependencies": { - "commander": "^12.1.0" - } -} diff --git a/tests/fixtures/next-prisma-repo/AI_CONTEXT/ARCHITECTURE.md b/tests/fixtures/next-prisma-repo/AI_CONTEXT/ARCHITECTURE.md deleted file mode 100644 index 2b9d854..0000000 --- a/tests/fixtures/next-prisma-repo/AI_CONTEXT/ARCHITECTURE.md +++ /dev/null @@ -1,3 +0,0 @@ -# Previous AI Context - -This is stale generated context and should not be treated as a primary source. diff --git a/tests/fixtures/next-prisma-repo/AI_CONTEXT/vendor_notes.md b/tests/fixtures/next-prisma-repo/AI_CONTEXT/vendor_notes.md deleted file mode 100644 index a50e2bd..0000000 --- a/tests/fixtures/next-prisma-repo/AI_CONTEXT/vendor_notes.md +++ /dev/null @@ -1,3 +0,0 @@ -# Vendor Notes - -Legacy generated notes that should not be treated as runtime code surfaces. diff --git a/tests/fixtures/next-prisma-repo/README.md b/tests/fixtures/next-prisma-repo/README.md deleted file mode 100644 index 4d21980..0000000 --- a/tests/fixtures/next-prisma-repo/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Next Prisma Fixture - -Repositorio de prueba para validar una app Next.js full-stack con Prisma y rutas públicas. diff --git a/tests/fixtures/next-prisma-repo/app/API.md b/tests/fixtures/next-prisma-repo/app/API.md deleted file mode 100644 index 543bb2c..0000000 --- a/tests/fixtures/next-prisma-repo/app/API.md +++ /dev/null @@ -1,29 +0,0 @@ -# API Documentation - -## Auth - -### POST /api/auth/login - -- Auth: No. - -## Profiles - -### GET /api/profile/public - -- Auth: Required. - -### PUT /api/profile/public - -- Auth: Required. - -### GET /api/users/[username]/public - -- Auth: No. - -## Reports - -### POST /api/reports - -- Auth: Required. -- Targets: vendor, listing. -- Status: open, reviewing, resolved. diff --git a/tests/fixtures/next-prisma-repo/app/ARCHITECTURE.md b/tests/fixtures/next-prisma-repo/app/ARCHITECTURE.md deleted file mode 100644 index 6b3ec32..0000000 --- a/tests/fixtures/next-prisma-repo/app/ARCHITECTURE.md +++ /dev/null @@ -1,13 +0,0 @@ -# Architecture Overview - -## Runtime Architecture - -- Next.js App Router serves UI and API routes from the same app package. - -## Service Layer - -- Profile reads and writes flow through dedicated services before reaching route handlers. - -## Authentication & Authorization - -- Session checks gate vendor dashboard access. diff --git a/tests/fixtures/next-prisma-repo/app/BUSINESS_RULES.md b/tests/fixtures/next-prisma-repo/app/BUSINESS_RULES.md deleted file mode 100644 index 4d82e6d..0000000 --- a/tests/fixtures/next-prisma-repo/app/BUSINESS_RULES.md +++ /dev/null @@ -1,25 +0,0 @@ -# Business Rules - -## Scope & Sources - -- Rules below are extracted from live code only. -- Primary sources: - - `src/app/api/*` - - `src/services/*` - - `prisma/schema.prisma` - -## Roles, Profiles, Permissions - -- Admin -- Vendor -- Public - -## Profiles - -- Public profile data is updated through `/api/profile/public`. -- Public profile visibility is resolved through `/api/users/[username]/public`. - -## Reports - -- Any authenticated user can create a report about a published listing. -- Only admins can resolve or dismiss reports. diff --git a/tests/fixtures/next-prisma-repo/app/FLOWS.md b/tests/fixtures/next-prisma-repo/app/FLOWS.md deleted file mode 100644 index b5f78ea..0000000 --- a/tests/fixtures/next-prisma-repo/app/FLOWS.md +++ /dev/null @@ -1,14 +0,0 @@ -# Flows - -## 1) Visitor -> Exploration - -- Public traffic can browse published listings. - -## 2) Vendor -> Activation - -- Approved vendors complete profile data before publication. - -## Reports - -- Authenticated users submit a report. -- Admin reviews the report and updates its status. diff --git a/tests/fixtures/next-prisma-repo/app/backups/vendor_snapshot.json b/tests/fixtures/next-prisma-repo/app/backups/vendor_snapshot.json deleted file mode 100644 index 4421364..0000000 --- a/tests/fixtures/next-prisma-repo/app/backups/vendor_snapshot.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "module": "vendor-dashboard", - "note": "legacy snapshot" -} diff --git a/tests/fixtures/next-prisma-repo/app/docs/FEATURES/2fa.md b/tests/fixtures/next-prisma-repo/app/docs/FEATURES/2fa.md deleted file mode 100644 index 20e7f3d..0000000 --- a/tests/fixtures/next-prisma-repo/app/docs/FEATURES/2fa.md +++ /dev/null @@ -1,5 +0,0 @@ -# Two-Factor Authentication - -## Admin override - -- Existing admins can reset recovery access without replacing the login flow. diff --git a/tests/fixtures/next-prisma-repo/app/docs/README.md b/tests/fixtures/next-prisma-repo/app/docs/README.md deleted file mode 100644 index 0a4e3c0..0000000 --- a/tests/fixtures/next-prisma-repo/app/docs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# App Docs - -Operational source for the runtime behavior of the fixture app. diff --git a/tests/fixtures/next-prisma-repo/app/docs/technical/ACCESS_CONTROL.md b/tests/fixtures/next-prisma-repo/app/docs/technical/ACCESS_CONTROL.md deleted file mode 100644 index c3fb96c..0000000 --- a/tests/fixtures/next-prisma-repo/app/docs/technical/ACCESS_CONTROL.md +++ /dev/null @@ -1,4 +0,0 @@ -# Access Control - -- Approved vendors can access `/dashboard/vendor`. -- Unauthenticated traffic is redirected to `/login`. diff --git a/tests/fixtures/next-prisma-repo/app/prisma/migrations/20260101000000_init/migration.sql b/tests/fixtures/next-prisma-repo/app/prisma/migrations/20260101000000_init/migration.sql deleted file mode 100644 index 426fc1f..0000000 --- a/tests/fixtures/next-prisma-repo/app/prisma/migrations/20260101000000_init/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE "User" ( - "id" TEXT PRIMARY KEY, - "email" TEXT NOT NULL UNIQUE -); diff --git a/tests/fixtures/next-prisma-repo/app/prisma/schema.prisma b/tests/fixtures/next-prisma-repo/app/prisma/schema.prisma deleted file mode 100644 index 08a8c37..0000000 --- a/tests/fixtures/next-prisma-repo/app/prisma/schema.prisma +++ /dev/null @@ -1,25 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id String @id @default(cuid()) - email String @unique -} - -<<<<<<< ours -model Report { - id String @id @default(cuid()) - status String @default("OPEN") -} -======= -model Report { - id String @id @default(cuid()) - state String @default("open") -} ->>>>>>> theirs diff --git a/tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/dashboard/vendor/page.tsx b/tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/dashboard/vendor/page.tsx deleted file mode 100644 index 43364ab..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/dashboard/vendor/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function VendorDashboardPage() { - return
Vendor dashboard
; -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/layout.tsx b/tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/layout.tsx deleted file mode 100644 index b90bab6..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/(dashboard-vendor)/layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { redirect } from "next/navigation"; - -import VendorSidebar from "@/app/components/vendor/VendorSidebar"; -import { canAccessVendorDashboard } from "@/lib/auth/permissions"; -import { getCurrentUser } from "@/lib/auth/session"; - -export default async function VendorLayout({ - children -}: { - children: React.ReactNode; -}) { - const user = await getCurrentUser(); - if (!user) { - redirect("/login"); - } - - if (!canAccessVendorDashboard(user)) { - redirect("/"); - } - - return ( -
- -
{children}
-
- ); -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/(public)/page.tsx b/tests/fixtures/next-prisma-repo/app/src/app/(public)/page.tsx deleted file mode 100644 index 7f8010d..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/(public)/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function PublicHomePage() { - return
Public home
; -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/(public)/u/[username]/page.tsx b/tests/fixtures/next-prisma-repo/app/src/app/(public)/u/[username]/page.tsx deleted file mode 100644 index 3706348..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/(public)/u/[username]/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function PublicProfilePage() { - return
Public profile
; -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/api/auth/login/route.ts b/tests/fixtures/next-prisma-repo/app/src/app/api/auth/login/route.ts deleted file mode 100644 index 79e101f..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/api/auth/login/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function POST() { - return Response.json({ ok: true }); -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/api/notifications/list/route.ts b/tests/fixtures/next-prisma-repo/app/src/app/api/notifications/list/route.ts deleted file mode 100644 index 54d55d4..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/api/notifications/list/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET() { - return Response.json({ items: [] }); -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/api/profile/public/route.ts b/tests/fixtures/next-prisma-repo/app/src/app/api/profile/public/route.ts deleted file mode 100644 index d36ef0a..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/api/profile/public/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -export async function GET() { - return Response.json({ profile: null }); -} - -export async function PUT() { - return Response.json({ ok: true }); -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/api/reports/route.ts b/tests/fixtures/next-prisma-repo/app/src/app/api/reports/route.ts deleted file mode 100644 index 79e101f..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/api/reports/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function POST() { - return Response.json({ ok: true }); -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/api/users/[username]/public/route.ts b/tests/fixtures/next-prisma-repo/app/src/app/api/users/[username]/public/route.ts deleted file mode 100644 index bc3eb4f..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/api/users/[username]/public/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET() { - return Response.json({ user: null }); -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/components/vendor/VendorSidebar.tsx b/tests/fixtures/next-prisma-repo/app/src/app/components/vendor/VendorSidebar.tsx deleted file mode 100644 index 7bd8403..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/components/vendor/VendorSidebar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { vendorNavSections } from "./vendorNavConfig"; - -export default function VendorSidebar() { - return ( - - ); -} diff --git a/tests/fixtures/next-prisma-repo/app/src/app/components/vendor/vendorNavConfig.ts b/tests/fixtures/next-prisma-repo/app/src/app/components/vendor/vendorNavConfig.ts deleted file mode 100644 index 0520548..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/app/components/vendor/vendorNavConfig.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const vendorNavSections = [ - { - title: "Workspace", - items: [ - { href: "/dashboard/vendor", label: "Overview" }, - { href: "/dashboard/vendor/orders", label: "Orders" } - ] - }, - { - title: "Settings", - items: [{ href: "/dashboard/vendor/profile", label: "Profile" }] - } -]; - -export const vendorNavLinks = vendorNavSections.flatMap((section) => section.items); diff --git a/tests/fixtures/next-prisma-repo/app/src/components/ui/Button.tsx b/tests/fixtures/next-prisma-repo/app/src/components/ui/Button.tsx deleted file mode 100644 index a527d3d..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/components/ui/Button.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function Button() { - return ; -} diff --git a/tests/fixtures/next-prisma-repo/app/src/lib/auth/permissions.ts b/tests/fixtures/next-prisma-repo/app/src/lib/auth/permissions.ts deleted file mode 100644 index 3c0ac45..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/lib/auth/permissions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function canAccessVendorDashboard(user: { role: string } | null) { - return user?.role === "VENDOR" || user?.role === "ADMIN"; -} diff --git a/tests/fixtures/next-prisma-repo/app/src/lib/auth/session.ts b/tests/fixtures/next-prisma-repo/app/src/lib/auth/session.ts deleted file mode 100644 index 5af086d..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/lib/auth/session.ts +++ /dev/null @@ -1,6 +0,0 @@ -export async function getCurrentUser() { - return { - id: "user_1", - role: "VENDOR" - }; -} diff --git a/tests/fixtures/next-prisma-repo/app/src/services/publicProfileService.ts b/tests/fixtures/next-prisma-repo/app/src/services/publicProfileService.ts deleted file mode 100644 index 03028be..0000000 --- a/tests/fixtures/next-prisma-repo/app/src/services/publicProfileService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function getPublicProfile(username: string) { - return { username }; -} diff --git a/tests/fixtures/next-prisma-repo/docs/notifications/definition.md b/tests/fixtures/next-prisma-repo/docs/notifications/definition.md deleted file mode 100644 index 9730438..0000000 --- a/tests/fixtures/next-prisma-repo/docs/notifications/definition.md +++ /dev/null @@ -1,16 +0,0 @@ -# Notifications Definition - -**Status:** Draft - -## Actors - -### Customer - -### Vendor - -### Operator - -## Principles - -- Delivery channels are decoupled from business triggers. -- Notification dispatch must be asynchronous. diff --git a/tests/fixtures/next-prisma-repo/docs/vendor-dashboard/README.md b/tests/fixtures/next-prisma-repo/docs/vendor-dashboard/README.md deleted file mode 100644 index ef6af75..0000000 --- a/tests/fixtures/next-prisma-repo/docs/vendor-dashboard/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Vendor Dashboard - -This module lets approved vendors configure and publish their profile without technical support. - -## Objectives - -- Guide activation from setup to published listing. -- Avoid inconsistent data between branches and services. - -## States - -- Active: profile complete and at least one published service. -- Inactive: required profile data is missing. diff --git a/tests/fixtures/next-prisma-repo/docs/vendor-dashboard/decisions.md b/tests/fixtures/next-prisma-repo/docs/vendor-dashboard/decisions.md deleted file mode 100644 index 96243fa..0000000 --- a/tests/fixtures/next-prisma-repo/docs/vendor-dashboard/decisions.md +++ /dev/null @@ -1,9 +0,0 @@ -# Vendor Dashboard Decisions - -## Why branches are optional? - -- Small vendors often operate from a single location. - -## Why shared service scope is the default? - -- Reduce friction during activation. diff --git a/tests/fixtures/next-prisma-repo/package.json b/tests/fixtures/next-prisma-repo/package.json deleted file mode 100644 index c065f46..0000000 --- a/tests/fixtures/next-prisma-repo/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "next-prisma-repo", - "private": true, - "dependencies": { - "next": "15.0.0", - "prisma": "6.0.0", - "react": "19.0.0", - "react-dom": "19.0.0", - "zod": "4.0.0" - } -} diff --git a/tests/fixtures/sample-repo/.github/workflows/test.yml b/tests/fixtures/sample-repo/.github/workflows/test.yml deleted file mode 100644 index 5a4e60f..0000000 --- a/tests/fixtures/sample-repo/.github/workflows/test.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: fixture-ci -on: - push: -jobs: - test: - runs-on: ubuntu-latest - steps: - - run: echo "fixture" diff --git a/tests/fixtures/sample-repo/Dockerfile b/tests/fixtures/sample-repo/Dockerfile deleted file mode 100644 index d3df7f6..0000000 --- a/tests/fixtures/sample-repo/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM node:22-alpine -WORKDIR /app -COPY package.json package.json -COPY src src -CMD ["node", "src/index.ts"] diff --git a/tests/fixtures/sample-repo/README.md b/tests/fixtures/sample-repo/README.md deleted file mode 100644 index 5adc9d6..0000000 --- a/tests/fixtures/sample-repo/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# sample-repo - -Fixture repository used to validate discovery, orchestration, governance, and CLI smoke flows. diff --git a/tests/fixtures/sample-repo/openapi.yaml b/tests/fixtures/sample-repo/openapi.yaml deleted file mode 100644 index d9db3b5..0000000 --- a/tests/fixtures/sample-repo/openapi.yaml +++ /dev/null @@ -1,10 +0,0 @@ -openapi: 3.0.3 -info: - title: Sample Repo API - version: 1.0.0 -paths: - /health: - get: - responses: - "200": - description: ok diff --git a/tests/fixtures/sample-repo/package.json b/tests/fixtures/sample-repo/package.json deleted file mode 100644 index 35d8b0c..0000000 --- a/tests/fixtures/sample-repo/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "sample-repo", - "version": "1.0.0", - "private": true, - "dependencies": { - "express": "^4.21.0", - "graphql": "^16.9.0", - "pino": "^9.5.0", - "prom-client": "^15.1.0" - }, - "devDependencies": { - "vitest": "^4.0.18" - } -} diff --git a/tests/fixtures/sample-repo/schema.graphql b/tests/fixtures/sample-repo/schema.graphql deleted file mode 100644 index 6cf08a8..0000000 --- a/tests/fixtures/sample-repo/schema.graphql +++ /dev/null @@ -1,3 +0,0 @@ -type Query { - health: String! -} diff --git a/tests/fixtures/sample-repo/src/index.ts b/tests/fixtures/sample-repo/src/index.ts deleted file mode 100644 index 777a92f..0000000 --- a/tests/fixtures/sample-repo/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function boot(): string { - return "ok"; -} diff --git a/tests/fixtures/sample-repo/tests/app.test.ts b/tests/fixtures/sample-repo/tests/app.test.ts deleted file mode 100644 index 4637c8d..0000000 --- a/tests/fixtures/sample-repo/tests/app.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { boot } from "../src/index"; - -describe("fixture", () => { - it("boots", () => { - expect(boot()).toBe("ok"); - }); -}); diff --git a/tests/helpers.ts b/tests/helpers.ts deleted file mode 100644 index b6a7e40..0000000 --- a/tests/helpers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { mkdtemp, rm } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import os from "node:os"; -import path from "node:path"; - -const currentDir = path.dirname(fileURLToPath(import.meta.url)); - -export const fixtureRepoPath = path.resolve(currentDir, "fixtures/sample-repo"); -export const devAgentFixtureRepoPath = path.resolve(currentDir, "fixtures/dev-agent-repo"); -export const nextPrismaFixtureRepoPath = path.resolve(currentDir, "fixtures/next-prisma-repo"); -export const workspaceFixturePath = path.resolve(currentDir, "fixtures/multi-repo-workspace"); - -export async function createTempOutputDir(prefix: string): Promise { - return mkdtemp(path.join(os.tmpdir(), `${prefix}-`)); -} - -export async function cleanupDir(dirPath: string): Promise { - await rm(dirPath, { recursive: true, force: true }); -} diff --git a/tests/integration/ai-agent-reporting.test.ts b/tests/integration/ai-agent-reporting.test.ts deleted file mode 100644 index 0ca6654..0000000 --- a/tests/integration/ai-agent-reporting.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readFile } from "node:fs/promises"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { QAAgent } from "../../agents/qa_agent"; -import { ContextBuilder } from "../../core/context_builder"; -import { DiscoveryEngine } from "../../core/discovery_engine"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath } from "../helpers"; - -describe("AI-enabled agent reporting", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("merges AI insights into the report while preserving deterministic findings", async () => { - const outputDir = await createTempOutputDir("project-brain-ai-report"); - cleanupTargets.push(outputDir); - - const discovery = await new DiscoveryEngine().analyze(fixtureRepoPath); - const context = await new ContextBuilder().build(discovery, outputDir); - const agent = new QAAgent() as QAAgent & { - aiRouter: { - ask: (input: { task?: string; prompt: string; context?: string }) => Promise; - }; - }; - - agent.aiRouter = { - async ask(input) { - expect(input.task).toBe("qa-analysis"); - return JSON.stringify({ - issues: [ - { - severity: "medium", - description: "Release validation depends too heavily on manual verification steps." - } - ], - proposed_improvements: [ - { - type: "testing", - proposal: "Introduce a small regression suite for the highest-risk flows before weekly releases." - } - ] - }); - } - }; - - const report = await agent.run(context); - const content = await readFile(report.outputPath, "utf8"); - - expect(content).toContain("## Human Deterministic Findings"); - expect(content).toContain("## AI Insights"); - expect(content).toContain("## Combined Recommendations"); - expect(content).toContain("Release validation depends too heavily on manual verification steps."); - expect(content).toContain("Introduce a small regression suite for the highest-risk flows before weekly releases."); - expect(report.recommendations.some((recommendation) => recommendation.includes("regression suite"))).toBe(true); - }); -}); diff --git a/tests/integration/architecture-plan.test.ts b/tests/integration/architecture-plan.test.ts deleted file mode 100644 index 560734c..0000000 --- a/tests/integration/architecture-plan.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { access, readFile } from "node:fs/promises"; - -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath } from "../helpers"; - -describe("Architecture plan integration", () => { - const cleanupTargets: string[] = []; - const originalOllamaTimeout = process.env.OLLAMA_TIMEOUT_MS; - - beforeEach(() => { - process.env.OLLAMA_TIMEOUT_MS = "1"; - }); - - afterEach(async () => { - if (originalOllamaTimeout === undefined) { - delete process.env.OLLAMA_TIMEOUT_MS; - } else { - process.env.OLLAMA_TIMEOUT_MS = originalOllamaTimeout; - } - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("writes architecture planning artifacts and context", async () => { - const outputDir = await createTempOutputDir("project-brain-architecture-plan"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - - const result = await orchestrator.architecturePlan(fixtureRepoPath, outputDir); - - await access(result.planDir); - await access(result.blueprintPath); - await access(result.statePath); - await access(result.claudeContextPath); - await access(result.memoryPath); - - const blueprint = await readFile(result.blueprintPath, "utf8"); - const state = await readFile(result.statePath, "utf8"); - const claude = await readFile(result.claudeContextPath, "utf8"); - const memory = await readFile(result.memoryPath, "utf8"); - const memoryPayload = JSON.parse(memory) as { repoName: string }; - - expect(blueprint).toContain("Architecture Blueprint"); - expect(state).toContain("Architecture Evolution State"); - expect(claude).toContain("CLAUDE Working Context"); - expect(memoryPayload.repoName.length).toBeGreaterThan(0); - expect(state).toContain(result.context.repoName); - expect(blueprint).toContain(result.context.discovery.infrastructure.join(", ") || "Not explicitly detected"); - }); -}); diff --git a/tests/integration/ask-intent-routing.test.ts b/tests/integration/ask-intent-routing.test.ts deleted file mode 100644 index 24f3a39..0000000 --- a/tests/integration/ask-intent-routing.test.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { access, mkdir, readFile, writeFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { fileExists } from "../../shared/fs-utils"; -import type { ImprovementPlanResult, ResumeResult } from "../../shared/types"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath, nextPrismaFixtureRepoPath } from "../helpers"; - -describe("Ask intent routing", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("routes discovery-style prompts into repository mapping", async () => { - const outputDir = await createTempOutputDir("project-brain-ask-discover"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - - const result = await orchestrator.ask(fixtureRepoPath, outputDir, "identifica este proyecto"); - - expect(result.workflow).toBe("discover-project"); - expect(result.artifacts.some((artifact) => artifact.label === "Codebase map summary")).toBe(true); - await access(result.briefPath); - - const brief = await readFile(result.briefPath, "utf8"); - expect(brief).toContain("discover-project"); - expect(brief).toContain("Codebase map summary"); - }); - - it("routes policy prompts into firewall inspection", async () => { - const outputDir = await createTempOutputDir("project-brain-ask-firewall"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - - const result = await orchestrator.ask(fixtureRepoPath, outputDir, "inspecciona el firewall y aprobaciones"); - - expect(result.workflow).toBe("inspect-firewall"); - expect(result.artifacts.some((artifact) => artifact.label === "Firewall report")).toBe(true); - await access(result.briefPath); - - const brief = await readFile(result.briefPath, "utf8"); - expect(brief).toContain("inspect-firewall"); - expect(brief).toContain("Firewall report"); - }); - - it("routes security prompts into the dedicated security audit workflow", async () => { - const outputDir = await createTempOutputDir("project-brain-ask-security-audit"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - process.env.OLLAMA_TIMEOUT_MS = "1"; - - const result = await orchestrator.ask(nextPrismaFixtureRepoPath, outputDir, "haz una auditoria de seguridad del repositorio"); - - expect(result.workflow).toBe("security-audit"); - expect(result.artifacts.some((artifact) => artifact.label === "Security audit report")).toBe(true); - await access(result.briefPath); - - const brief = await readFile(result.briefPath, "utf8"); - expect(brief).toContain("security-audit"); - expect(brief).toContain("Security audit report"); - }); - - it("can enrich strategic ask flows with the planner model", async () => { - const outputDir = await createTempOutputDir("project-brain-ask-ai"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator({ - aiRouter: { - async selectModel() { - return { - preferredRoute: "cloud", - selectedRoute: "cloud", - provider: "ollama", - model: "kimi-k2.5:cloud", - profile: "planner", - residency: "remote", - reason: "Strategic ask uses planner model.", - offlineCapable: false - }; - }, - async ask() { - return JSON.stringify({ - headline: "Interpreted the request as a strategic project definition flow.", - summary: ["The request should branch into discovery first and then critical gap analysis."], - follow_ups: ['project-brain ask "dime que le falta criticamente"'], - suggested_workflow: "discover-project" - }); - } - } - }); - - const result = await orchestrator.ask(fixtureRepoPath, outputDir, "quiero definir bien el stack y el alcance de este proyecto"); - - expect(result.workflow).toBe("discover-project"); - expect(result.preflightFacts?.recommendedNextAction).toBeTruthy(); - expect(result.aiAssistance?.model).toBe("kimi-k2.5:cloud"); - expect(result.aiAssistance?.profile).toBe("planner"); - - const brief = await readFile(result.briefPath, "utf8"); - expect(brief).toContain("kimi-k2.5:cloud"); - expect(brief).toContain("AI Assist"); - expect(brief).toContain("Preflight Facts"); - expect(await fileExists(path.join(outputDir, "reports", "fact_query.md"))).toBe(false); - }); - - it("routes continuation prompts into resume-aware recovery", async () => { - const outputDir = await createTempOutputDir("project-brain-ask-resume"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - const context = await orchestrator.initTarget(fixtureRepoPath, outputDir); - const resumeReportPath = path.join(context.reportsDir, "resume.md"); - const swarmPath = path.join(context.memoryDir, "swarm", "swarm_run.json"); - const planSummaryPath = path.join(context.docsDir, "improvement_plan", "SUMMARY.md"); - const roadmapPath = path.join(context.docsDir, "improvement_plan", "ROADMAP.md"); - - await mkdir(path.dirname(swarmPath), { recursive: true }); - await mkdir(path.dirname(planSummaryPath), { recursive: true }); - await writeFile(resumeReportPath, "# Resume\n", "utf8"); - await writeFile(swarmPath, '{"ok":true}\n', "utf8"); - await writeFile(planSummaryPath, "# Improvement Plan Summary\n", "utf8"); - await writeFile(roadmapPath, "# Roadmap\n", "utf8"); - - orchestrator.resume = async () => - ({ - context, - reportPath: resumeReportPath, - memoryPath: path.join(context.memoryDir, "resume", "resume.json"), - git: { - isGitRepo: true, - branch: "main" - }, - summary: { - headline: "Resume from Swarm: The swarm already found concrete next steps.", - stage: "swarm", - artifactCount: 1, - latestArtifactLabel: "Swarm", - latestArtifactUpdatedAt: "2026-03-18T10:05:00.000Z" - }, - latestArtifact: { - label: "Swarm", - path: swarmPath, - exists: true, - updatedAt: "2026-03-18T10:05:00.000Z" - }, - memoryReadiness: { - status: "ready", - memoryBriefPath: path.join(context.memoryDir, "MEMORY_BRIEF.md"), - memoryBriefJsonPath: path.join(context.runtimeMemoryDir, "memory_brief", "memory_brief.json"), - generatedAt: "2026-03-18T10:00:00.000Z", - ageHours: 0, - maxAgeHours: 72, - factsCount: 1, - evidenceCount: 1, - tokenGuidanceCount: 1, - reason: "test" - }, - artifacts: [ - { - label: "Swarm", - path: swarmPath, - exists: true, - updatedAt: "2026-03-18T10:05:00.000Z" - } - ], - notes: [ - "Latest artifact: Swarm at 2026-03-18T10:05:00.000Z.", - "The swarm already found concrete next steps." - ], - suggestions: [ - { - label: "Continue With Improvement Plan", - command: `project-brain plan-improvements . --output "${outputDir}"`, - rationale: "Convert the swarm findings into a persistent roadmap.", - priority: "high" - } - ] - }) satisfies ResumeResult; - - orchestrator.planImprovements = async () => - ({ - context, - planDir: path.join(context.docsDir, "improvement_plan"), - summaryPath: planSummaryPath, - statePath: path.join(context.docsDir, "improvement_plan", "STATE.md"), - risksPath: path.join(context.docsDir, "improvement_plan", "KNOWN_RISKS.md"), - roadmapPath, - tracksPath: path.join(context.docsDir, "improvement_plan", "TRACKS.md") - }) satisfies ImprovementPlanResult; - - const result = await orchestrator.ask(fixtureRepoPath, outputDir, "continua con el proyecto"); - - expect(result.workflow).toBe("resume-project"); - expect(result.artifacts.some((artifact) => artifact.label === "Resume report")).toBe(true); - expect(result.artifacts.some((artifact) => artifact.label === "Improvement plan summary")).toBe(true); - expect(result.summary.some((line) => line.includes("Recovered stage: swarm"))).toBe(true); - expect(result.guidedExecution?.label).toBe("Improvement Plan"); - expect(result.guidedExecution?.command).toContain("plan-improvements"); - expect(result.followUps.some((step) => step.includes("review-delta"))).toBe(true); - - const brief = await readFile(result.briefPath, "utf8"); - expect(brief).toContain("resume-project"); - expect(brief).toContain("Resume report"); - expect(brief).toContain("Recovered stage: swarm"); - expect(brief).toContain("Guided continuation"); - expect(brief).toContain("Improvement Plan"); - }); - - it("continues from an improvement plan into review-delta when the user asks to continue", async () => { - const outputDir = await createTempOutputDir("project-brain-ask-resume-plan"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - const context = await orchestrator.initTarget(fixtureRepoPath, outputDir); - const resumeReportPath = path.join(context.reportsDir, "resume.md"); - const planSummaryPath = path.join(context.docsDir, "improvement_plan", "SUMMARY.md"); - const impactReportPath = path.join(context.reportsDir, "impact_radius.md"); - const graphPath = path.join(context.runtimeMemoryDir, "code_graph", "import_graph.json"); - - await mkdir(path.dirname(planSummaryPath), { recursive: true }); - await mkdir(path.dirname(graphPath), { recursive: true }); - await writeFile(resumeReportPath, "# Resume\n", "utf8"); - await writeFile(planSummaryPath, "# Improvement Plan Summary\n", "utf8"); - await writeFile(impactReportPath, "# Impact Radius\n", "utf8"); - await writeFile(graphPath, '{"ok":true}\n', "utf8"); - - orchestrator.resume = async () => - ({ - context, - reportPath: resumeReportPath, - memoryPath: path.join(context.memoryDir, "resume", "resume.json"), - git: { - isGitRepo: true, - branch: "main" - }, - summary: { - headline: "Resume from Improvement Plan: a persistent roadmap already exists for this output path.", - stage: "plan-improvements", - artifactCount: 1, - latestArtifactLabel: "Improvement Plan", - latestArtifactUpdatedAt: "2026-03-18T10:10:00.000Z" - }, - latestArtifact: { - label: "Improvement Plan", - path: planSummaryPath, - exists: true, - updatedAt: "2026-03-18T10:10:00.000Z" - }, - memoryReadiness: { - status: "ready", - memoryBriefPath: path.join(context.memoryDir, "MEMORY_BRIEF.md"), - memoryBriefJsonPath: path.join(context.runtimeMemoryDir, "memory_brief", "memory_brief.json"), - generatedAt: "2026-03-18T10:00:00.000Z", - ageHours: 0, - maxAgeHours: 72, - factsCount: 1, - evidenceCount: 1, - tokenGuidanceCount: 1, - reason: "test" - }, - artifacts: [ - { - label: "Improvement Plan", - path: planSummaryPath, - exists: true, - updatedAt: "2026-03-18T10:10:00.000Z" - } - ], - notes: [ - "Latest artifact: Improvement Plan at 2026-03-18T10:10:00.000Z.", - "A persistent roadmap already exists for this output path." - ], - suggestions: [ - { - label: "Review Latest Changes", - command: `project-brain review-delta . --output "${outputDir}"`, - rationale: "The next useful checkpoint is a bounded review of recent changes.", - priority: "medium" - } - ] - }) satisfies ResumeResult; - - orchestrator.reviewDelta = async () => ({ - targetPath: fixtureRepoPath, - outputPath: outputDir, - changedFiles: ["core/orchestrator/main.ts"], - directDependents: ["core/resume/index.ts"], - transitiveDependents: ["cli/project-brain.ts"], - reviewFiles: ["core/orchestrator/main.ts", "core/resume/index.ts"], - impactedTests: ["tests/integration/ask-intent-routing.test.ts"], - unresolvedImports: [], - graphPath, - reportPath: impactReportPath, - graphStats: { - nodes: 12, - edges: 18, - files: 4, - symbols: 7, - buildMode: "incremental", - updatedFiles: 1 - } - }); - - const result = await orchestrator.ask(fixtureRepoPath, outputDir, "retoma donde nos quedamos"); - - expect(result.workflow).toBe("resume-project"); - expect(result.guidedExecution?.label).toBe("Review Delta"); - expect(result.guidedExecution?.command).toContain("review-delta"); - expect(result.artifacts.some((artifact) => artifact.label === "Impact report")).toBe(true); - expect(result.followUps.some((step) => step.includes("status"))).toBe(true); - - const brief = await readFile(result.briefPath, "utf8"); - expect(brief).toContain("Continued from Improvement Plan into Review Delta."); - expect(brief).toContain("Impact report"); - }); -}); diff --git a/tests/integration/code-graph-v2.test.ts b/tests/integration/code-graph-v2.test.ts deleted file mode 100644 index 881a29d..0000000 --- a/tests/integration/code-graph-v2.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { access, readFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { writeFileEnsured } from "../../shared/fs-utils"; -import { cleanupDir, createTempOutputDir } from "../helpers"; - -async function seedGraphRepo(repoDir: string): Promise { - await writeFileEnsured( - path.join(repoDir, "package.json"), - JSON.stringify( - { - name: "code-graph-v2-fixture", - private: true, - type: "module" - }, - null, - 2 - ) - ); - await writeFileEnsured(path.join(repoDir, "src", "shared.ts"), "export const shared = 'base';\n"); - await writeFileEnsured( - path.join(repoDir, "src", "service.ts"), - "import { shared } from './shared';\nexport function service() { return shared; }\n" - ); - await writeFileEnsured( - path.join(repoDir, "src", "app.ts"), - "import { service } from './service';\nexport function app() { return service(); }\n" - ); -} - -describe("Code graph v2 integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("builds a persistent symbol-aware graph and updates incrementally", async () => { - const repoDir = await createTempOutputDir("project-brain-graph-v2-repo"); - const outputDir = await createTempOutputDir("project-brain-graph-v2-output"); - cleanupTargets.push(repoDir, outputDir); - - await seedGraphRepo(repoDir); - - const orchestrator = new ProjectBrainOrchestrator(); - const firstBuild = await orchestrator.buildCodeGraph(repoDir, outputDir); - - await access(firstBuild.graphPath); - - expect(firstBuild.graph.build.mode).toBe("full"); - expect(firstBuild.graph.stats.files).toBe(3); - expect(firstBuild.graph.stats.symbols).toBeGreaterThanOrEqual(3); - expect(firstBuild.graph.edges.some((edge) => edge.kind === "imports" && edge.from === "src/app.ts" && edge.to === "src/service.ts")).toBe(true); - expect(firstBuild.graph.symbols.some((symbol) => symbol.id === "src/service.ts#service" && symbol.kind === "function")).toBe(true); - - await writeFileEnsured( - path.join(repoDir, "src", "service.ts"), - "import { shared } from './shared';\nexport function service() { return `${shared}:updated`; }\n" - ); - - const secondBuild = await orchestrator.buildCodeGraph(repoDir, outputDir); - const persisted = JSON.parse(await readFile(secondBuild.graphPath, "utf8")) as { - build: { mode: string; updatedFiles: string[] }; - stats: { files: number; symbols: number }; - }; - - expect(secondBuild.graph.build.mode).toBe("incremental"); - expect(secondBuild.graph.build.updatedFiles).toEqual(["src/service.ts"]); - expect(persisted.build.mode).toBe("incremental"); - expect(persisted.build.updatedFiles).toEqual(["src/service.ts"]); - expect(persisted.stats.files).toBe(3); - expect(persisted.stats.symbols).toBeGreaterThanOrEqual(3); - }); -}); diff --git a/tests/integration/codebase-map.test.ts b/tests/integration/codebase-map.test.ts deleted file mode 100644 index 34250af..0000000 --- a/tests/integration/codebase-map.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { readFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath } from "../helpers"; - -describe("Codebase map integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("writes structured codebase map artifacts for a repository", async () => { - const outputDir = await createTempOutputDir("project-brain-codebase-map"); - cleanupTargets.push(outputDir); - - const orchestrator = new ProjectBrainOrchestrator(); - const result = await orchestrator.mapScope(fixtureRepoPath, outputDir); - - expect("context" in result).toBe(true); - if (!("context" in result)) { - throw new Error("Expected a single-repository codebase map result."); - } - - const expectedFiles = [ - "SUMMARY.md", - "STACK.md", - "INTEGRATIONS.md", - "ARCHITECTURE.md", - "STRUCTURE.md", - "CONVENTIONS.md", - "TESTING.md", - "CONCERNS.md" - ]; - - expect(result.codebaseMapDir).toBe(path.join(outputDir, "docs", "codebase_map")); - expect(result.files).toHaveLength(expectedFiles.length); - - for (const fileName of expectedFiles) { - const content = await readFile(path.join(result.codebaseMapDir, fileName), "utf8"); - expect(content.trim().length).toBeGreaterThan(20); - } - - const summary = await readFile(path.join(result.codebaseMapDir, "SUMMARY.md"), "utf8"); - expect(summary).toContain("sample-repo"); - expect(summary).toContain("Codebase Map Summary"); - expect(summary).toContain("project-brain analyze"); - }); -}); diff --git a/tests/integration/context-annotations.test.ts b/tests/integration/context-annotations.test.ts deleted file mode 100644 index c8a2bc3..0000000 --- a/tests/integration/context-annotations.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { access, readFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath } from "../helpers"; - -describe("Context annotations integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("persists annotations and surfaces them in generated context artifacts", async () => { - const outputDir = await createTempOutputDir("project-brain-annotations"); - cleanupTargets.push(outputDir); - - const orchestrator = new ProjectBrainOrchestrator(); - const annotation = await orchestrator.annotateTarget(fixtureRepoPath, outputDir, { - scope: "repo", - note: "Legacy billing paths are fragile; avoid broad refactors without a safety net." - }); - const mapResult = await orchestrator.mapTarget(fixtureRepoPath, outputDir); - - expect(annotation.scope).toBe("repo"); - - const annotations = await orchestrator.listAnnotations(fixtureRepoPath, outputDir); - expect(annotations).toHaveLength(1); - expect(annotations[0]?.note).toContain("Legacy billing paths are fragile"); - - await access(path.join(outputDir, "memory", "annotations", "index.json")); - - const annotationsArtifact = await readFile(path.join(outputDir, "AI_CONTEXT", "ANNOTATIONS.md"), "utf8"); - const summary = await readFile(mapResult.summaryPath, "utf8"); - - expect(annotationsArtifact).toContain("Legacy billing paths are fragile"); - expect(summary).toContain("Local Notes"); - expect(summary).toContain("Legacy billing paths are fragile"); - }); -}); diff --git a/tests/integration/context-lite.test.ts b/tests/integration/context-lite.test.ts deleted file mode 100644 index ce1924a..0000000 --- a/tests/integration/context-lite.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath, nextPrismaFixtureRepoPath } from "../helpers"; - -describe("Context-lite integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("writes a lightweight AI_CONTEXT pack for smaller apps", async () => { - const outputDir = await createTempOutputDir("project-brain-context-lite"); - cleanupTargets.push(outputDir); - - const orchestrator = new ProjectBrainOrchestrator(); - const result = await orchestrator.contextLite(fixtureRepoPath, outputDir); - - const expectedFiles = [ - "system_overview.md", - "domain_inventory.md", - "modules_map.md", - "frontend_architecture.md", - "backend_flows_and_contracts.md", - "ui_rules.md", - "MASTER_CONTEXT_PROMPT.md", - "DECISIONS.md", - "LEARNINGS.md", - "TASKS.md" - ]; - - expect(result.artifactPaths).toHaveLength(expectedFiles.length); - expect(result.reportPath).toBe(path.join(outputDir, "reports", "context_lite.md")); - expect(result.summary.length).toBeGreaterThan(5); - expect(result.openQuestions.length).toBeGreaterThan(0); - - for (const fileName of expectedFiles) { - const content = await readFile(path.join(outputDir, "AI_CONTEXT", fileName), "utf8"); - expect(content.trim().length).toBeGreaterThan(20); - } - - const systemOverview = await readFile(path.join(outputDir, "AI_CONTEXT", "system_overview.md"), "utf8"); - expect(systemOverview).toContain("sample-repo"); - expect(systemOverview).toContain("Express"); - - const backendFlows = await readFile(path.join(outputDir, "AI_CONTEXT", "backend_flows_and_contracts.md"), "utf8"); - expect(backendFlows).toContain("openapi.yaml"); - expect(backendFlows).toContain("schema.graphql"); - - const tasks = await readFile(path.join(outputDir, "AI_CONTEXT", "TASKS.md"), "utf8"); - expect(tasks).toContain("context-lite:generated:start"); - expect(tasks).toContain("Confirmar la fuente de verdad de datos"); - - const masterPrompt = await readFile(path.join(outputDir, "AI_CONTEXT", "MASTER_CONTEXT_PROMPT.md"), "utf8"); - expect(masterPrompt).toContain("Actua como analista tecnico del repositorio actual."); - expect(masterPrompt).toContain("AI_CONTEXT/"); - expect(masterPrompt).toContain(fixtureRepoPath); - expect(masterPrompt).toContain("Contexto confirmado en esta corrida"); - expect(masterPrompt).toContain("Forma detectada: Backend/API service"); - expect(masterPrompt).toContain("Fuentes que debes priorizar"); - expect(masterPrompt).toContain("Dominios detectados en esta corrida"); - expect(masterPrompt).toContain("Superficies activas ya confirmadas"); - expect(masterPrompt).toContain("Huecos o ambigüedades que siguen abiertas"); - }); - - it("prioritizes prisma and full-stack next signals when present", async () => { - const outputDir = await createTempOutputDir("project-brain-context-lite-next-prisma"); - cleanupTargets.push(outputDir); - - const orchestrator = new ProjectBrainOrchestrator(); - await orchestrator.contextLite(nextPrismaFixtureRepoPath, outputDir); - - const systemOverview = await readFile(path.join(outputDir, "AI_CONTEXT", "system_overview.md"), "utf8"); - expect(systemOverview).toContain("Full-stack web application"); - expect(systemOverview).toContain("Prisma schema define la fuente de verdad relacional"); - expect(systemOverview).toContain("Usuarios públicos o tráfico anónimo"); - expect(systemOverview).toContain("Fuentes canónicas declaradas por la documentación"); - expect(systemOverview).toContain("app/src/app/api/*"); - expect(systemOverview).toContain("app/prisma/schema.prisma"); - expect(systemOverview).toContain("Documentos operativos de referencia"); - expect(systemOverview).toContain("app/docs/README.md"); - expect(systemOverview).toContain("Documentación estructurada detectada"); - expect(systemOverview).not.toContain("Admin override"); - expect(systemOverview).not.toContain("Actor documentado: GET"); - expect(systemOverview).not.toContain("AI_CONTEXT/ARCHITECTURE.md"); - expect(systemOverview).toContain("contiene marcadores de conflicto"); - - const frontendArchitecture = await readFile( - path.join(outputDir, "AI_CONTEXT", "frontend_architecture.md"), - "utf8" - ); - expect(frontendArchitecture).toContain("app/src/components/"); - expect(frontendArchitecture).toContain("VendorSidebar"); - expect(frontendArchitecture).toContain("Overview"); - - const domainInventory = await readFile(path.join(outputDir, "AI_CONTEXT", "domain_inventory.md"), "utf8"); - expect(domainInventory).toContain("access-control"); - expect(domainInventory).toContain("app/src/lib/auth/permissions.ts"); - expect(domainInventory).toContain("vendor-dashboard"); - expect(domainInventory).toContain("notifications"); - expect(domainInventory).toContain("profiles"); - expect(domainInventory).toContain("app/src/app/api/profile/public/route.ts"); - expect(domainInventory).toContain("app/src/services/publicProfileService.ts"); - expect(domainInventory).toContain("reports"); - expect(domainInventory).toContain("app/src/app/api/reports/route.ts"); - expect(domainInventory).not.toContain("app/backups/vendor_snapshot.json"); - expect(domainInventory).not.toContain("AI_CONTEXT/vendor_notes.md"); - expect(domainInventory).not.toContain("El dominio `reports` está documentado, pero no se asociaron superficies"); - - const backendFlows = await readFile(path.join(outputDir, "AI_CONTEXT", "backend_flows_and_contracts.md"), "utf8"); - expect(backendFlows).toContain("app/API.md"); - expect(backendFlows).toContain("app/BUSINESS_RULES.md"); - expect(backendFlows).toContain("contiene marcadores de conflicto"); - - const uiRules = await readFile(path.join(outputDir, "AI_CONTEXT", "ui_rules.md"), "utf8"); - expect(uiRules).toContain("menús por actor"); - expect(uiRules).toContain("permission-check"); - - const masterPrompt = await readFile(path.join(outputDir, "AI_CONTEXT", "MASTER_CONTEXT_PROMPT.md"), "utf8"); - expect(masterPrompt).toContain("Usa este prompt cuando necesites crear o refrescar el `AI_CONTEXT/`"); - expect(masterPrompt).toContain(nextPrismaFixtureRepoPath); - expect(masterPrompt).toContain("Forma detectada: Full-stack web application"); - expect(masterPrompt).toContain("Fuentes canónicas declaradas por esta corrida"); - expect(masterPrompt).toContain("`app/src/app/api/*`"); - expect(masterPrompt).toContain("`app/prisma/schema.prisma`"); - expect(masterPrompt).toContain("Documentos operativos de referencia"); - expect(masterPrompt).toContain("`app/API.md`"); - expect(masterPrompt).toContain("`app/docs/README.md`"); - expect(masterPrompt).toContain("Dominios detectados en esta corrida"); - expect(masterPrompt).toContain("`profiles`"); - expect(masterPrompt).toContain("`reports`"); - expect(masterPrompt).toContain("Superficies activas ya confirmadas"); - expect(masterPrompt).toContain("Huecos o ambigüedades que siguen abiertas"); - expect(masterPrompt).toContain("contiene marcadores de conflicto"); - - const decisions = await readFile(path.join(outputDir, "AI_CONTEXT", "DECISIONS.md"), "utf8"); - expect(decisions).toContain("Reduce friction during activation"); - expect(decisions).toContain("declara como canon"); - - const tasks = await readFile(path.join(outputDir, "AI_CONTEXT", "TASKS.md"), "utf8"); - expect(tasks).toContain("Validar documentación marcada como pendiente o draft"); - expect(tasks).toContain("Resolver ambigüedad en fuentes de verdad"); - expect(tasks).not.toContain("Add a CI workflow to run validation on every change."); - }); - - it("extracts PHP MVC business domains from controllers, models, views, and SQL", async () => { - const repoDir = await createTempOutputDir("project-brain-context-lite-php-mvc-repo"); - const outputDir = await createTempOutputDir("project-brain-context-lite-php-mvc-output"); - cleanupTargets.push(repoDir, outputDir); - - await mkdir(path.join(repoDir, "default", "app", "controllers"), { recursive: true }); - await mkdir(path.join(repoDir, "default", "app", "models"), { recursive: true }); - await mkdir(path.join(repoDir, "default", "app", "views", "empleado"), { recursive: true }); - await mkdir(path.join(repoDir, "default", "app", "config"), { recursive: true }); - await writeFile( - path.join(repoDir, "README.md"), - `![KumbiaPHP logo](https://example.test/logo.svg) - -## Sistema para el control de la información del personal - -Registro, administración y generación de gafetes de identificación para el personal activo. -` - ); - await writeFile(path.join(repoDir, "default", "app", "config", "config.php"), "Empleados\n"); - await writeFile(path.join(repoDir, "gafete.sql"), "CREATE TABLE `empleado` (`id` int);\n"); - - const orchestrator = new ProjectBrainOrchestrator(); - await orchestrator.contextLite(repoDir, outputDir); - - const systemOverview = await readFile(path.join(outputDir, "AI_CONTEXT", "system_overview.md"), "utf8"); - expect(systemOverview).toContain("Sistema para el control de la información del personal"); - expect(systemOverview).toContain("KumbiaPHP"); - expect(systemOverview).toContain("Dump o esquema SQL versionado"); - - const domainInventory = await readFile(path.join(outputDir, "AI_CONTEXT", "domain_inventory.md"), "utf8"); - expect(domainInventory).toContain("Dominio `empleado`"); - expect(domainInventory).toContain("default/app/controllers/empleado_controller.php"); - expect(domainInventory).toContain("default/app/models/empleado.php"); - expect(domainInventory).toContain("Dominio `cuenta`"); - - const modulesMap = await readFile(path.join(outputDir, "AI_CONTEXT", "modules_map.md"), "utf8"); - expect(modulesMap).toContain("Módulo empleado"); - expect(modulesMap).toContain("Módulo MVC PHP/Kumbia"); - - const backendFlows = await readFile(path.join(outputDir, "AI_CONTEXT", "backend_flows_and_contracts.md"), "utf8"); - expect(backendFlows).toContain("Kumbia action /empleado/listar"); - expect(backendFlows).toContain("gafete.sql"); - }); -}); diff --git a/tests/integration/context-registry.test.ts b/tests/integration/context-registry.test.ts deleted file mode 100644 index e5e320f..0000000 --- a/tests/integration/context-registry.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { access, readFile } from "node:fs/promises"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath } from "../helpers"; - -describe("Context registry integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("searches the local context registry and writes a report", async () => { - const outputDir = await createTempOutputDir("project-brain-context-search"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - - const result = await orchestrator.contextSearch(fixtureRepoPath, outputDir, "express observability", "official"); - - expect(result.hits.length).toBeGreaterThan(0); - expect(result.hits[0]?.entry.id).toBe("node-express-api"); - - await access(result.reportPath); - await access(result.cachePath); - - const report = await readFile(result.reportPath, "utf8"); - expect(report).toContain("Context Search"); - expect(report).toContain("Node + Express API Baseline"); - }); - - it("materializes a context entry into AI_CONTEXT external context", async () => { - const outputDir = await createTempOutputDir("project-brain-context-get"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - - const result = await orchestrator.contextGet(fixtureRepoPath, outputDir, "node-express-api"); - - await access(result.artifactPath); - await access(result.cachePath); - - const artifact = await readFile(result.artifactPath, "utf8"); - expect(artifact).toContain("Node + Express API Baseline"); - expect(artifact).toContain("Guidance"); - }); - - it("lists available context sources with trust levels", async () => { - const outputDir = await createTempOutputDir("project-brain-context-sources"); - cleanupTargets.push(outputDir); - const orchestrator = new ProjectBrainOrchestrator(); - - const result = await orchestrator.contextSources(fixtureRepoPath, outputDir); - - expect(result.sources.length).toBeGreaterThan(0); - await access(result.reportPath); - - const report = await readFile(result.reportPath, "utf8"); - expect(report).toContain("Context Sources"); - expect(report).toContain("project-brain curated"); - }); -}); diff --git a/tests/integration/dev-agent-analysis.test.ts b/tests/integration/dev-agent-analysis.test.ts deleted file mode 100644 index 0e24d1a..0000000 --- a/tests/integration/dev-agent-analysis.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { readFile } from "node:fs/promises"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { DevAgent } from "../../agents/dev_agent"; -import { ContextBuilder } from "../../core/context_builder"; -import { DiscoveryEngine } from "../../core/discovery_engine"; -import { cleanupDir, createTempOutputDir, devAgentFixtureRepoPath } from "../helpers"; - -const DEV_AGENT_INTEGRATION_TIMEOUT_MS = 15000; - -describe("DevAgent integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("produces actionable architectural findings for a repository fixture", async () => { - const outputDir = await createTempOutputDir("project-brain-dev-agent"); - cleanupTargets.push(outputDir); - - const discovery = await new DiscoveryEngine().analyze(devAgentFixtureRepoPath); - const context = await new ContextBuilder().build(discovery, outputDir); - const report = await new DevAgent().run(context); - const content = await readFile(report.outputPath, "utf8"); - - expect(report.outputPath.endsWith("dev_architecture_analysis.md")).toBe(true); - expect(report.recommendations.length).toBeGreaterThanOrEqual(3); - expect(content).toContain("Top 10 Architecture Risks"); - expect(content).toContain("Architectural Observations"); - expect(content).toContain("Break circular module dependencies"); - }, DEV_AGENT_INTEGRATION_TIMEOUT_MS); -}); diff --git a/tests/integration/dev-agent-patch-proposals.test.ts b/tests/integration/dev-agent-patch-proposals.test.ts deleted file mode 100644 index 38f8a77..0000000 --- a/tests/integration/dev-agent-patch-proposals.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { existsSync } from "node:fs"; -import { readdir, readFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { DevAgent } from "../../agents/dev_agent"; -import { ContextBuilder } from "../../core/context_builder"; -import { DiscoveryEngine } from "../../core/discovery_engine"; -import { writeFileEnsured } from "../../shared/fs-utils"; -import { cleanupDir, createTempOutputDir } from "../helpers"; - -describe("DevAgent patch proposal workflow", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("reads UX implementation tasks and generates non-applied patch proposals", async () => { - const repoDir = await createTempOutputDir("project-brain-dev-patches-repo"); - const outputDir = await createTempOutputDir("project-brain-dev-patches-output"); - cleanupTargets.push(repoDir, outputDir); - - await writeFileEnsured( - path.join(repoDir, "package.json"), - JSON.stringify( - { - name: "workflow-frontend", - private: true, - dependencies: { - react: "^19.0.0" - } - }, - null, - 2 - ) - ); - await writeFileEnsured( - path.join(repoDir, "tsconfig.json"), - JSON.stringify( - { - compilerOptions: { - jsx: "react-jsx", - target: "ES2022", - module: "ESNext" - }, - include: ["src/**/*"] - }, - null, - 2 - ) - ); - await writeFileEnsured(path.join(repoDir, "src", "components", "Sidebar.tsx"), "export function Sidebar() { return null; }\n"); - await writeFileEnsured(path.join(repoDir, "src", "components", "Dashboard.tsx"), "export function Dashboard() { return null; }\n"); - await writeFileEnsured(path.join(repoDir, "src", "components", "OrderForm.tsx"), "export function OrderForm() { return null; }\n"); - - const discovery = await new DiscoveryEngine().analyze(repoDir); - const context = await new ContextBuilder().build(discovery, outputDir); - - await writeFileEnsured( - path.join(outputDir, "UX_IMPLEMENTATION_TASKS.md"), - `# UX Implementation Tasks - -### Task -Component: Sidebar -File: src/components/Sidebar.tsx -Problem: Navigation relies on a crowded sidebar that hides the most common actions. -User impact: Users lose orientation and need extra clicks to reach core workflows. -Proposed change: Reduce sidebar depth and group navigation items around the main operator workflows. -Risk: medium -Effort: Medium - -### Task -Component: Dashboard -File: src/components/Dashboard.tsx -Problem: The dashboard hierarchy makes it difficult to identify the main operational KPI at a glance. -User impact: Users cannot understand system status or priorities quickly. -Proposed change: Simplify dashboard layout and prioritize the primary KPI cards in the first viewport. -Risk: medium -Effort: Medium - -### Task -Component: Forms -File: src/components/OrderForm.tsx -Problem: Several forms use technical terminology and unclear labels that increase data entry errors. -User impact: Users take longer to complete tasks and are more likely to submit incorrect data. -Proposed change: Clarify field labels, replace technical terms, and add inline helper text for complex fields. -Risk: high -Effort: Medium - -### Task -Component: Sidebar -File: src/server/auth.ts -Problem: Authentication flow should be rewritten as part of the navigation cleanup. -User impact: None -Proposed change: Change server-side auth logic. -Risk: high -Effort: High -` - ); - - const report = await new DevAgent().run(context); - const patchFiles = (await readdir(context.patchProposalDir)) - .filter((entry) => entry.endsWith(".diff")) - .sort((left, right) => left.localeCompare(right)); - const firstPatch = await readFile(path.join(context.patchProposalDir, patchFiles[0] ?? ""), "utf8"); - const reportContent = await readFile(report.outputPath, "utf8"); - - expect(report.outputPath.endsWith("dev_architecture_analysis.md")).toBe(true); - expect(patchFiles).toEqual([ - "patch_001_form_labels.diff", - "patch_002_sidebar_navigation.diff", - "patch_003_dashboard_layout.diff" - ]); - expect(firstPatch).toContain("# Stage: PROPOSE_PATCHES"); - expect(firstPatch).toContain("# Human approval required: yes"); - expect(firstPatch).toContain("diff --git a/src/components/OrderForm.tsx b/src/components/OrderForm.tsx"); - expect(reportContent).toContain("## PROPOSE_PATCHES"); - expect(reportContent).toContain("patch_001 -> src/components/OrderForm.tsx"); - expect(existsSync(path.join(repoDir, "patch_proposals"))).toBe(false); - }); -}); diff --git a/tests/integration/discovery-engine.test.ts b/tests/integration/discovery-engine.test.ts deleted file mode 100644 index 11a7a12..0000000 --- a/tests/integration/discovery-engine.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { DiscoveryEngine } from "../../core/discovery_engine"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath } from "../helpers"; - -describe("DiscoveryEngine integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("analyzes a simple repository fixture end-to-end", async () => { - const engine = new DiscoveryEngine(); - const result = await engine.analyze(fixtureRepoPath); - - expect(result.repoName).toBe("sample-repo"); - expect(result.languages).toContain("TypeScript"); - expect(result.frameworks).toContain("Express"); - expect(result.apis).toContain("REST"); - expect(result.apis).toContain("OpenAPI"); - expect(result.infrastructure).toContain("Dockerfile"); - expect(result.testing).toContain("Vitest"); - }); - - it("excludes generated output directories when requested", async () => { - const repoDir = await createTempOutputDir("project-brain-discovery"); - cleanupTargets.push(repoDir); - - await mkdir(path.join(repoDir, "src"), { recursive: true }); - await mkdir(path.join(repoDir, "sample-output", "reports"), { recursive: true }); - await writeFile(path.join(repoDir, "package.json"), JSON.stringify({ name: "temp-repo", dependencies: { express: "1.0.0" } })); - await writeFile(path.join(repoDir, "src", "index.ts"), "export const value = 1;\n"); - await writeFile(path.join(repoDir, "sample-output", "reports", "noise.ts"), "export const noise = 1;\n"); - - const engine = new DiscoveryEngine(); - const result = await engine.analyze(repoDir, { excludePaths: ["sample-output"] }); - - expect(result.files).toContain("src/index.ts"); - expect(result.files).not.toContain("sample-output/reports/noise.ts"); - }); - - it("ignores project-brain root artifacts without hiding source memory modules", async () => { - const repoDir = await createTempOutputDir("project-brain-generated-artifacts"); - cleanupTargets.push(repoDir); - - await mkdir(path.join(repoDir, "src"), { recursive: true }); - await mkdir(path.join(repoDir, "AI_CONTEXT"), { recursive: true }); - await mkdir(path.join(repoDir, "reports"), { recursive: true }); - await mkdir(path.join(repoDir, "tasks", "packets"), { recursive: true }); - await mkdir(path.join(repoDir, "memory", "memory_brief"), { recursive: true }); - await writeFile(path.join(repoDir, "package.json"), JSON.stringify({ name: "temp-repo" })); - await writeFile(path.join(repoDir, "src", "index.ts"), "export const value = 1;\n"); - await writeFile(path.join(repoDir, "AI_CONTEXT", "noise.ts"), "export const noise = 1;\n"); - await writeFile(path.join(repoDir, "reports", "noise.ts"), "export const noise = 1;\n"); - await writeFile(path.join(repoDir, "tasks", "packets", "noise.ts"), "export const noise = 1;\n"); - await writeFile(path.join(repoDir, "memory", "memory_brief", "memory_brief.json"), "{}\n"); - - const result = await new DiscoveryEngine().analyze(repoDir); - - expect(result.files).toContain("src/index.ts"); - expect(result.files).not.toContain("AI_CONTEXT/noise.ts"); - expect(result.files).not.toContain("reports/noise.ts"); - expect(result.files).not.toContain("tasks/packets/noise.ts"); - expect(result.structure.topLevelDirectories).not.toContain("memory"); - }); - - it("keeps a real source memory directory when it is not project-brain runtime output", async () => { - const repoDir = await createTempOutputDir("project-brain-source-memory"); - cleanupTargets.push(repoDir); - - await mkdir(path.join(repoDir, "memory", "context_store"), { recursive: true }); - await writeFile(path.join(repoDir, "package.json"), JSON.stringify({ name: "temp-repo" })); - await writeFile(path.join(repoDir, "memory", "context_store", "index.ts"), "export const source = true;\n"); - - const result = await new DiscoveryEngine().analyze(repoDir); - - expect(result.files).toContain("memory/context_store/index.ts"); - expect(result.structure.topLevelDirectories).toContain("memory"); - }); -}); diff --git a/tests/integration/discovery-fixture-filtering.test.ts b/tests/integration/discovery-fixture-filtering.test.ts deleted file mode 100644 index 5f758e6..0000000 --- a/tests/integration/discovery-fixture-filtering.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { DiscoveryEngine } from "../../core/discovery_engine"; -import { cleanupDir, createTempOutputDir } from "../helpers"; - -describe("DiscoveryEngine fixture filtering", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("ignores nested test fixtures when inferring repository signals", async () => { - const repoDir = await createTempOutputDir("project-brain-fixture-filter"); - cleanupTargets.push(repoDir); - - await mkdir(path.join(repoDir, "src"), { recursive: true }); - await mkdir(path.join(repoDir, "core", "vendor", "library"), { recursive: true }); - await mkdir(path.join(repoDir, "tests", "fixtures", "nested-app"), { recursive: true }); - await writeFile( - path.join(repoDir, "package.json"), - JSON.stringify({ - name: "real-repo", - dependencies: { - commander: "^14.0.0" - }, - devDependencies: { - vitest: "^4.0.0" - } - }) - ); - await writeFile(path.join(repoDir, "src", "index.ts"), "export const ready = true;\n"); - await writeFile(path.join(repoDir, "core", "vendor", "library", "noise.ts"), "export const vendored = true;\n"); - await writeFile( - path.join(repoDir, "tests", "fixtures", "nested-app", "package.json"), - JSON.stringify({ - name: "nested-fixture", - dependencies: { - react: "^19.0.0", - express: "^5.0.0" - } - }) - ); - await writeFile( - path.join(repoDir, "tests", "fixtures", "nested-app", "openapi.yaml"), - "openapi: 3.0.0\ninfo:\n title: Nested fixture\n version: 1.0.0\n" - ); - - const engine = new DiscoveryEngine(); - const result = await engine.analyze(repoDir); - - expect(result.frameworks).not.toContain("React"); - expect(result.frameworks).not.toContain("Express"); - expect(result.apis).not.toContain("OpenAPI"); - expect(result.files.some((file) => file.includes("tests/fixtures"))).toBe(false); - expect(result.files.some((file) => file.includes("/vendor/"))).toBe(false); - }); -}); diff --git a/tests/integration/ecosystem-radar.test.ts b/tests/integration/ecosystem-radar.test.ts deleted file mode 100644 index d7948cb..0000000 --- a/tests/integration/ecosystem-radar.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { access, readFile } from "node:fs/promises"; - -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { cleanupDir, createTempOutputDir, fixtureRepoPath } from "../helpers"; - -function jsonResponse(payload: unknown): Response { - return new Response(JSON.stringify(payload), { - status: 200, - headers: { - "content-type": "application/json" - } - }); -} - -function readmeResponse(markdown: string): Response { - return jsonResponse({ - encoding: "base64", - content: Buffer.from(markdown, "utf8").toString("base64") - }); -} - -describe("Ecosystem radar integration", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - vi.unstubAllGlobals(); - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("discovers ecosystem candidates and feeds them into the local context registry", async () => { - const outputDir = await createTempOutputDir("project-brain-ecosystem-radar"); - cleanupTargets.push(outputDir); - - const fakeRepos = new Map([ - [ - "langchain-ai/langmem", - { - name: "langmem", - full_name: "langchain-ai/langmem", - html_url: "https://github.com/langchain-ai/langmem", - description: "Memory toolkit for agents.", - stargazers_count: 1300, - forks_count: 120, - language: "Python", - topics: ["memory", "agents", "langchain"], - pushed_at: "2026-03-10T00:00:00Z", - owner: { login: "langchain-ai" }, - license: { spdx_id: "MIT" } - } - ], - [ - "example/agent-memory-lab", - { - name: "agent-memory-lab", - full_name: "example/agent-memory-lab", - html_url: "https://github.com/example/agent-memory-lab", - description: "Agent memory experiments for developer tooling.", - stargazers_count: 890, - forks_count: 44, - language: "TypeScript", - topics: ["memory", "agents", "tooling"], - pushed_at: "2026-03-12T00:00:00Z", - owner: { login: "example" }, - license: { spdx_id: "Apache-2.0" } - } - ] - ]); - - vi.stubGlobal( - "fetch", - vi.fn(async (input: string | URL | Request) => { - const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url); - - if (url.pathname === "/search/repositories") { - return jsonResponse({ - items: [fakeRepos.get("example/agent-memory-lab")] - }); - } - - const segments = url.pathname.split("/").filter(Boolean); - if (segments[0] === "repos" && segments[1] && segments[2]) { - const fullName = `${segments[1]}/${segments[2]}`; - const repo = - fakeRepos.get(fullName) ?? - { - name: segments[2], - full_name: fullName, - html_url: `https://github.com/${fullName}`, - description: `${fullName} benchmark repository.`, - stargazers_count: 500, - forks_count: 50, - language: "TypeScript", - topics: ["agents", "context"], - pushed_at: "2026-03-01T00:00:00Z", - owner: { login: segments[1] }, - license: { spdx_id: "MIT" } - }; - - if (segments[3] === "readme") { - return readmeResponse(`# ${repo.name}\n\n${repo.description} README with agent memory and context details.\n`); - } - - return jsonResponse(repo); - } - - return new Response("not found", { status: 404 }); - }) - ); - - const orchestrator = new ProjectBrainOrchestrator(); - const result = await orchestrator.ecosystemRadar(fixtureRepoPath, outputDir, { - limit: 1, - bucketId: "memory" - }); - - expect(result.candidates.length).toBeGreaterThan(1); - expect(result.candidates.some((candidate) => candidate.entry.id === "langmem-agent-memory")).toBe(true); - expect(result.candidates.some((candidate) => candidate.repoFullName === "example/agent-memory-lab")).toBe(true); - expect(result.candidates.some((candidate) => candidate.entry.id === "ast-grep-structural-search")).toBe(false); - - await access(result.reportPath); - await access(result.cachePath); - - const report = await readFile(result.reportPath, "utf8"); - expect(report).toContain("Ecosystem Radar"); - expect(report).toContain("## Curated Seeds"); - expect(report).toContain("## Discovered Candidates"); - expect(report).toContain("langmem"); - expect(report).toContain("example/agent-memory-lab"); - expect(report).not.toContain("ast-grep/ast-grep"); - - const contextSearch = await orchestrator.contextSearch(fixtureRepoPath, outputDir, "langmem memory", "maintainer"); - expect(contextSearch.hits.some((hit) => hit.entry.id === "langmem-agent-memory")).toBe(true); - - const contextGet = await orchestrator.contextGet(fixtureRepoPath, outputDir, "langmem-agent-memory"); - const artifact = await readFile(contextGet.artifactPath, "utf8"); - expect(artifact).toContain("LangMem Agent Memory"); - - const sources = await orchestrator.contextSources(fixtureRepoPath, outputDir); - expect(sources.sources.some((source) => source.source === "github-radar curated")).toBe(true); - }); -}); diff --git a/tests/integration/fact-query.test.ts b/tests/integration/fact-query.test.ts deleted file mode 100644 index c500863..0000000 --- a/tests/integration/fact-query.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { access, readFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { ProjectBrainOrchestrator } from "../../core/orchestrator/main"; -import { writeScopeMemoryFromSwarmResult } from "../../memory/scope_store"; -import { appendFileEnsured, writeFileEnsured } from "../../shared/fs-utils"; -import type { ProjectContext, SwarmRunResult } from "../../shared/types"; -import { cleanupDir, createTempOutputDir } from "../helpers"; - -async function seedRepo(repoDir: string): Promise { - await writeFileEnsured( - path.join(repoDir, "package.json"), - JSON.stringify( - { - name: "fact-query-fixture", - private: true, - dependencies: { - commander: "^14.0.1" - } - }, - null, - 2 - ) - ); - await writeFileEnsured(path.join(repoDir, "src", "swarm_runtime.ts"), "export function askWithSwarmCache() { return 'cached'; }\n"); -} - -function staleScopeSwarmResult(context: ProjectContext): SwarmRunResult { - return { - engine: "bounded", - context, - intent: "inspect beta contract", - reportPath: path.join(context.reportsDir, "swarm_run.md"), - memoryPath: path.join(context.memoryDir, "swarm", "swarm_run.json"), - resilience: { - runTimeoutMs: 1, - plannerTimeoutMs: 1, - synthesisTimeoutMs: 1, - taskTimeoutMs: 1, - maxRetries: 0, - queueBudget: 1, - plannerTimedOut: false, - synthesisTimedOut: false, - runTimedOut: false, - timedOutTasks: 0, - retriedTasks: 0, - splitTasks: 0, - failedTasks: 0, - droppedTasks: 0, - localBudgetMode: true, - adaptiveQueueBudget: false - }, - chunking: { - selectedChunkSize: 1, - scopeUnits: 1, - scopeChunks: 1, - queuedTasks: 1, - queueStrategy: "round-robin", - scopeBias: "balanced", - scopeHints: [] - }, - parallelism: { - selected: 1, - cpuCount: 1, - loadAverage1m: 0, - freeMemoryMb: 1, - totalMemoryMb: 1, - pressure: "low" - }, - planner: { - provider: "test", - model: "test", - residency: "local", - overview: "test" - }, - tasks: [], - workerResults: [ - { - taskId: "beta", - parentTaskId: "beta", - chunkId: "scope-1", - attempt: 1, - status: "completed", - title: "Inspect beta", - profile: "worker", - scopePaths: ["src"], - provider: "test", - model: "test", - residency: "local", - summary: "done", - findings: [], - recommendations: [], - verifiedFacts: ["beta-contract is enabled"], - unknowns: [], - evidenceRefs: ["src/feature.ts"] - } - ], - synthesis: { - provider: "test", - model: "test", - residency: "local", - headline: "done", - summary: "done", - priorities: [], - nextSteps: [], - verifiedFacts: ["beta-contract is enabled"], - unknowns: [], - evidenceRefs: ["src/feature.ts"] - } - }; -} - -describe("fact query", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("queries memory brief and repository fact graph without model calls", async () => { - const repoDir = await createTempOutputDir("project-brain-fact-query-repo"); - const outputDir = await createTempOutputDir("project-brain-fact-query-output"); - cleanupTargets.push(repoDir, outputDir); - - await seedRepo(repoDir); - - const orchestrator = new ProjectBrainOrchestrator(); - await orchestrator.buildCodeGraph(repoDir, outputDir); - const context = await orchestrator.initTarget(repoDir, outputDir); - await appendFileEnsured( - path.join(context.memoryDir, "DECISIONS.md"), - "\n- Use swarm cache policy before model execution because cache keys must reflect prompt policy.\n" - ); - await orchestrator.status(repoDir, outputDir); - - const result = await orchestrator.factQuery(repoDir, outputDir, "swarm cache policy"); - - await access(result.reportPath); - await access(result.memoryPath); - expect(result.answer).toContain("FOUND"); - expect(result.memoryMatches.some((match) => /swarm cache policy/i.test(match.text))).toBe(true); - expect(result.nodeMatches.some((match) => match.label.includes("swarm_runtime"))).toBe(true); - expect(result.evidenceRefs.some((ref) => ref.includes("src/swarm_runtime.ts"))).toBe(true); - - const persisted = JSON.parse(await readFile(result.memoryPath, "utf8")) as { answer?: string }; - expect(persisted.answer).toBe(result.answer); - }); - - it("does not answer from stale scope memory and reports a stale warning", async () => { - const repoDir = await createTempOutputDir("project-brain-fact-query-stale-repo"); - const outputDir = await createTempOutputDir("project-brain-fact-query-stale-output"); - cleanupTargets.push(repoDir, outputDir); - - await writeFileEnsured(path.join(repoDir, "package.json"), JSON.stringify({ name: "stale-fact-query", private: true }, null, 2)); - await writeFileEnsured(path.join(repoDir, "src", "feature.ts"), "export const feature = true;\n"); - - const orchestrator = new ProjectBrainOrchestrator(); - const context = await orchestrator.initTarget(repoDir, outputDir); - await writeScopeMemoryFromSwarmResult(context, staleScopeSwarmResult(context)); - await writeFileEnsured(path.join(repoDir, "src", "feature.ts"), "export const feature = false;\n"); - - const result = await orchestrator.factQuery(repoDir, outputDir, "beta-contract"); - - expect(result.scopeMemoryMatches ?? []).toHaveLength(0); - expect(result.answer).not.toContain("scope-memory="); - expect(result.unknowns.some((unknown) => /Scope memory for src is stale/.test(unknown))).toBe(true); - }); -}); diff --git a/tests/integration/frontend-ux-targeting.test.ts b/tests/integration/frontend-ux-targeting.test.ts deleted file mode 100644 index a597bce..0000000 --- a/tests/integration/frontend-ux-targeting.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { readdir, readFile } from "node:fs/promises"; -import path from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { DevAgent } from "../../agents/dev_agent"; -import { UXImprovementAgent } from "../../agents/ux_improvement_agent"; -import { ContextBuilder } from "../../core/context_builder"; -import { DiscoveryEngine } from "../../core/discovery_engine"; -import { writeFileEnsured } from "../../shared/fs-utils"; -import { cleanupDir, createTempOutputDir } from "../helpers"; - -const originalTimeout = process.env.OLLAMA_TIMEOUT_MS; - -describe("frontend UX targeting", () => { - const cleanupTargets: string[] = []; - - afterEach(async () => { - if (originalTimeout === undefined) { - delete process.env.OLLAMA_TIMEOUT_MS; - } else { - process.env.OLLAMA_TIMEOUT_MS = originalTimeout; - } - - await Promise.all(cleanupTargets.splice(0).map((target) => cleanupDir(target))); - }); - - it("maps UX tasks and review-only patches to real frontend surfaces", async () => { - const repoDir = await createTempOutputDir("workflow-frontend-architecture"); - const outputDir = await createTempOutputDir("project-brain-frontend-ux"); - cleanupTargets.push(repoDir, outputDir); - - await writeFileEnsured( - path.join(repoDir, "package.json"), - JSON.stringify( - { - name: "workflow-frontend", - dependencies: { - next: "^15.0.0", - react: "^19.0.0" - } - }, - null, - 2 - ) - ); - - await writeFileEnsured( - path.join(repoDir, "src", "shared", "ui", "layout", "Sidebar.tsx"), - `const NAV_LIFECYCLE_V2_ENABLED = process.env.NEXT_PUBLIC_NAV_LIFECYCLE_V2 !== 'false'; -export function Sidebar() { - return null; -} -` - ); - await writeFileEnsured( - path.join(repoDir, "src", "domains", "admin-console", "components", "AdminConsoleNav.tsx"), - `const navItems = [ - { href: '/admin/catalogos', label: 'Catálogos' }, - { href: '/admin/configuracion', label: 'Configuración' }, - { href: '/admin/checklists', label: 'Checklists' }, - { href: '/admin/observabilidad', label: 'Observabilidad' }, - { href: '/admin/importaciones', label: 'Importaciones' }, -]; -export function AdminConsoleNav() { return null; } -` - ); - await writeFileEnsured( - path.join(repoDir, "src", "domains", "procedimiento-wizard", "components", "NextStepCard.tsx"), - `export function NextStepCard() { - return ( -
-

Siguiente paso recomendado

-

{nextStep.reason}

- -
- ); -} -` - ); - await writeFileEnsured( - path.join(repoDir, "src", "domains", "necesidades", "components", "NecesidadForm.tsx"), - `export function NecesidadForm() { - return ( -
- - - - - - - -