diff --git a/AGENTS.md.template b/AGENTS.md.template deleted file mode 100644 index a7dfb1b..0000000 --- a/AGENTS.md.template +++ /dev/null @@ -1,430 +0,0 @@ -# Attune Project Rules - -## Project Overview -Attune is an **event-driven automation and orchestration platform** built in Rust, similar to StackStorm. It enables building complex workflows triggered by events with multi-tenancy, RBAC, and human-in-the-loop capabilities. - -## Development Status: Pre-Production - -**This project is under active development with no users, deployments, or stable releases.** - -### Breaking Changes Policy -- **Breaking changes are explicitly allowed and encouraged** when they improve the architecture, API design, or developer experience -- **No backward compatibility required** - there are no existing versions to support -- **Database migrations can be modified or consolidated** - no production data exists -- **API contracts can change freely** - no external integrations depend on them, only internal interfaces with other services and the web UI must be maintained. -- **Configuration formats can be redesigned** - no existing config files need migration -- **Service interfaces can be refactored** - no live deployments to worry about - -When this project reaches v1.0 or gets its first production deployment, this section should be removed and replaced with appropriate stability guarantees and versioning policies. - -## Languages & Core Technologies -- **Primary Language**: Rust 2021 edition -- **Database**: PostgreSQL 14+ (primary data store + LISTEN/NOTIFY pub/sub) -- **Message Queue**: RabbitMQ 3.12+ (via lapin) -- **Cache**: Redis 7.0+ (optional) -- **Web UI**: TypeScript + React 19 + Vite -- **Async Runtime**: Tokio -- **Web Framework**: Axum 0.8 -- **ORM**: SQLx (compile-time query checking) - -## Project Structure (Cargo Workspace) - -``` -attune/ -├── Cargo.toml # Workspace root -├── config.{development,test}.yaml # Environment configs -├── Makefile # Common dev tasks -├── crates/ # Rust services -│ ├── common/ # Shared library (models, db, repos, mq, config, error) -│ ├── api/ # REST API service (8080) -│ ├── executor/ # Execution orchestration service -│ ├── worker/ # Action execution service (multi-runtime) -│ ├── sensor/ # Event monitoring service -│ ├── notifier/ # Real-time notification service -│ └── cli/ # Command-line interface -├── migrations/ # SQLx database migrations (18 tables) -├── web/ # React web UI (Vite + TypeScript) -├── packs/ # Pack bundles -│ └── core/ # Core pack (timers, HTTP, etc.) -├── docs/ # Technical documentation -├── scripts/ # Helper scripts (DB setup, testing) -└── tests/ # Integration tests -``` - -## Service Architecture (Distributed Microservices) - -1. **attune-api**: REST API gateway, JWT auth, all client interactions -2. **attune-executor**: Manages execution lifecycle, scheduling, policy enforcement -3. **attune-worker**: Executes actions in multiple runtimes (Python/Node.js/containers) -4. **attune-sensor**: Monitors triggers, generates events -5. **attune-notifier**: Real-time notifications via PostgreSQL LISTEN/NOTIFY + WebSocket - -**Communication**: Services communicate via RabbitMQ for async operations - -## Docker Compose Orchestration - -**All Attune services run via Docker Compose.** - -- **Compose file**: `docker-compose.yaml` (root directory) -- **Configuration**: `config.docker.yaml` (Docker-specific settings) -- **Default user**: `test@attune.local` / `TestPass123!` (auto-created) - -**Services**: -- **Infrastructure**: postgres, rabbitmq, redis -- **Init** (run-once): migrations, init-user, init-packs -- **Application**: api (8080), executor, worker-{shell,python,node,full}, sensor, notifier (8081), web (3000) - -**Commands**: -```bash -docker compose up -d # Start all services -docker compose down # Stop all services -docker compose logs -f # View logs -``` - -**Key environment overrides**: `JWT_SECRET`, `ENCRYPTION_KEY` (required for production) - -## Domain Model & Event Flow - -**Critical Event Flow**: -``` -Sensor → Trigger fires → Event created → Rule evaluates → -Enforcement created → Execution scheduled → Worker executes Action -``` - -**Key Entities** (all in `public` schema, IDs are `i64`): -- **Pack**: Bundle of automation components (actions, sensors, rules, triggers) -- **Trigger**: Event type definition (e.g., "webhook_received") -- **Sensor**: Monitors for trigger conditions, creates events -- **Event**: Instance of a trigger firing with payload -- **Action**: Executable task with parameters -- **Rule**: Links triggers to actions with conditional logic -- **Enforcement**: Represents a rule activation -- **Execution**: Single action run; supports parent-child relationships for workflows - - **Workflow Tasks**: Workflow-specific metadata stored in `execution.workflow_task` JSONB field -- **Inquiry**: Human-in-the-loop async interaction (approvals, inputs) -- **Identity**: User/service account with RBAC permissions -- **Key**: Encrypted secrets storage - -## Key Tools & Libraries - -### Shared Dependencies (workspace-level) -- **Async**: tokio, async-trait, futures -- **Web**: axum, tower, tower-http -- **Database**: sqlx (with postgres, json, chrono, uuid features) -- **Serialization**: serde, serde_json, serde_yaml_ng -- **Logging**: tracing, tracing-subscriber -- **Error Handling**: anyhow, thiserror -- **Config**: config crate (YAML + env vars) -- **Validation**: validator -- **Auth**: jsonwebtoken, argon2 -- **CLI**: clap -- **OpenAPI**: utoipa, utoipa-swagger-ui -- **Message Queue**: lapin (RabbitMQ) -- **HTTP Client**: reqwest -- **Testing**: mockall, tempfile, serial_test - -### Web UI Dependencies -- **Framework**: React 19 + react-router-dom -- **State**: Zustand, @tanstack/react-query -- **HTTP**: axios (with generated OpenAPI client) -- **Styling**: Tailwind CSS -- **Icons**: lucide-react -- **Build**: Vite, TypeScript - -## Configuration System -- **Primary**: YAML config files (`config.yaml`, `config.{env}.yaml`) -- **Overrides**: Environment variables with prefix `ATTUNE__` and separator `__` - - Example: `ATTUNE__DATABASE__URL`, `ATTUNE__SERVER__PORT` -- **Loading Priority**: Base config → env-specific config → env vars -- **Required for Production**: `JWT_SECRET`, `ENCRYPTION_KEY` (32+ chars) -- **Location**: Root directory or `ATTUNE_CONFIG` env var path - -## Authentication & Security -- **Auth Type**: JWT (access tokens: 1h, refresh tokens: 7d) -- **Password Hashing**: Argon2id -- **Protected Routes**: Use `RequireAuth(user)` extractor in Axum -- **Secrets Storage**: AES-GCM encrypted in `key` table with scoped ownership -- **User Info**: Stored in `identity` table - -## Code Conventions & Patterns - -### General -- **Error Handling**: Use `attune_common::error::Error` and `Result` type alias -- **Async Everywhere**: All I/O operations use async/await with Tokio -- **Module Structure**: Public API exposed via `mod.rs` with `pub use` re-exports - -### Database Layer -- **Schema**: All tables use unqualified names; schema determined by PostgreSQL `search_path` -- **Production**: Always uses `public` schema (configured explicitly in `config.production.yaml`) -- **Tests**: Each test uses isolated schema (e.g., `test_a1b2c3d4`) for true parallel execution -- **Schema Resolution**: PostgreSQL `search_path` mechanism, NO hardcoded schema prefixes in queries -- **Models**: Defined in `common/src/models.rs` with `#[derive(FromRow)]` for SQLx -- **Repositories**: One per entity in `common/src/repositories/`, provides CRUD + specialized queries -- **Pattern**: Services MUST interact with DB only through repository layer (no direct queries) -- **Transactions**: Use SQLx transactions for multi-table operations -- **IDs**: All IDs are `i64` (BIGSERIAL in PostgreSQL) -- **Timestamps**: `created`/`updated` columns auto-managed by DB triggers -- **JSON Fields**: Use `serde_json::Value` for flexible attributes/parameters, including `execution.workflow_task` JSONB -- **Enums**: PostgreSQL enum types mapped with `#[sqlx(type_name = "...")]` -- **Workflow Tasks**: Stored as JSONB in `execution.workflow_task` (consolidated from separate table 2026-01-27) -**Table Count**: 17 tables total in the schema - -### Pack File Loading -- **Pack Base Directory**: Configured via `packs_base_dir` in config (defaults to `/opt/attune/packs`, development uses `./packs`) -- **Action Script Resolution**: Worker constructs file paths as `{packs_base_dir}/{pack_ref}/actions/{entrypoint}` -- **Runtime Selection**: Determined by action's runtime field (e.g., "Shell", "Python") - compared case-insensitively -- **Parameter Passing**: Shell actions receive parameters as environment variables with `ATTUNE_ACTION_` prefix - -### API Service (`crates/api`) -- **Structure**: `routes/` (endpoints) + `dto/` (request/response) + `auth/` + `middleware/` -- **Responses**: Standardized `ApiResponse` wrapper with `data` field -- **Protected Routes**: Apply `RequireAuth` middleware -- **OpenAPI**: Documented with `utoipa` attributes (`#[utoipa::path]`) -- **Error Handling**: Custom `ApiError` type with proper HTTP status codes -- **Available at**: `http://localhost:8080` (dev), `/api-spec/openapi.json` for spec - -### Common Library (`crates/common`) -- **Modules**: `models`, `repositories`, `db`, `config`, `error`, `mq`, `crypto`, `utils`, `workflow`, `pack_registry` -- **Exports**: Commonly used types re-exported from `lib.rs` -- **Repository Layer**: All DB access goes through repositories in `repositories/` -- **Message Queue**: Abstractions in `mq/` for RabbitMQ communication - -### Web UI (`web/`) -- **Generated Client**: OpenAPI client auto-generated from API spec - - Run: `npm run generate:api` (requires API running on :8080) - - Location: `src/api/` -- **State Management**: Zustand for global state, TanStack Query for server state -- **Styling**: Tailwind utility classes -- **Dev Server**: `npm run dev` (typically :3000 or :5173) -- **Build**: `npm run build` - -## Development Workflow - -### Common Commands (Makefile) -```bash -make build # Build all services -make build-release # Release build -make test # Run all tests -make test-integration # Run integration tests -make fmt # Format code -make clippy # Run linter -make lint # fmt + clippy - -make run-api # Run API service -make run-executor # Run executor service -make run-worker # Run worker service -make run-sensor # Run sensor service -make run-notifier # Run notifier service - -make db-create # Create database -make db-migrate # Run migrations -make db-reset # Drop & recreate DB -``` - -### Database Operations -- **Migrations**: Located in `migrations/`, applied via `sqlx migrate run` -- **Test DB**: Separate `attune_test` database, setup with `make db-test-setup` -- **Schema**: All tables in `public` schema with auto-updating timestamps -- **Core Pack**: Load with `./scripts/load-core-pack.sh` after DB setup - -### Testing -- **Architecture**: Schema-per-test isolation (each test gets unique `test_` schema) -- **Parallel Execution**: Tests run concurrently without `#[serial]` constraints (4-8x faster) -- **Unit Tests**: In module files alongside code -- **Integration Tests**: In `tests/` directory -- **Test DB Required**: Use `make db-test-setup` before integration tests -- **Run**: `cargo test` or `make test` (parallel by default) -- **Verbose**: `cargo test -- --nocapture --test-threads=1` -- **Cleanup**: Schemas auto-dropped on test completion; orphaned schemas cleaned via `./scripts/cleanup-test-schemas.sh` -- **SQLx Offline Mode**: Enabled for compile-time query checking without live DB; regenerate with `cargo sqlx prepare` - -### CLI Tool -```bash -cargo install --path crates/cli # Install CLI -attune auth login # Login -attune pack list # List packs -attune action execute --param key=value -attune execution list # Monitor executions -``` - -## Test Failure Protocol - -**Proactively investigate and fix test failures when discovered, even if unrelated to the current task.** - -### Guidelines: -- **ALWAYS report test failures** to the user with relevant error output -- **ALWAYS run tests** after making changes: `make test` or `cargo test` -- **DO fix immediately** if the cause is obvious and fixable in 1-2 attempts -- **DO ask the user** if the failure is complex, requires architectural changes, or you're unsure of the cause -- **NEVER silently ignore** test failures or skip tests without approval -- **Gather context**: Run with `cargo test -- --nocapture --test-threads=1` for details - -### Priority: -- **Critical** (build/compile failures): Fix immediately -- **Related** (affects current work): Fix before proceeding -- **Unrelated**: Report and ask if you should fix now or defer - -When reporting, ask: "Should I fix this first or continue with [original task]?" - -## Code Quality: Zero Warnings Policy - -**Maintain zero compiler warnings across the workspace.** Clean builds ensure new issues are immediately visible. - -### Workflow -- **Check after changes:** `cargo check --all-targets --workspace` -- **Before completing work:** Fix or document any warnings introduced -- **End of session:** Verify zero warnings before finishing - -### Handling Warnings -- **Fix first:** Remove dead code, unused imports, unnecessary variables -- **Prefix `_`:** For intentionally unused variables that document intent -- **Use `#[allow(dead_code)]`:** For API methods intended for future use (add doc comment explaining why) -- **Never ignore blindly:** Every suppression needs a clear rationale - -### Conservative Approach -- Preserve methods that complete a logical API surface -- Keep test helpers that are part of shared infrastructure -- When uncertain about removal, ask the user - -### Red Flags -- ❌ Introducing new warnings -- ❌ Blanket `#[allow(warnings)]` without specific justification -- ❌ Accumulating warnings over time - -## File Naming & Location Conventions - -### When Adding Features: -- **New API Endpoint**: - - Route handler in `crates/api/src/routes/.rs` - - DTO in `crates/api/src/dto/.rs` - - Update `routes/mod.rs` and main router -- **New Domain Model**: - - Add to `crates/common/src/models.rs` - - Create migration in `migrations/YYYYMMDDHHMMSS_description.sql` - - Add repository in `crates/common/src/repositories/.rs` -- **New Service**: Add to `crates/` and update workspace `Cargo.toml` members -- **Configuration**: Update `crates/common/src/config.rs` with serde defaults -- **Documentation**: Add to `docs/` directory - -### Important Files -- `crates/common/src/models.rs` - All domain models -- `crates/common/src/error.rs` - Error types -- `crates/common/src/config.rs` - Configuration structure -- `crates/api/src/routes/mod.rs` - API routing -- `config.development.yaml` - Dev configuration -- `Cargo.toml` - Workspace dependencies -- `Makefile` - Development commands - -## Common Pitfalls to Avoid -1. **NEVER** bypass repositories - always use the repository layer for DB access -2. **NEVER** forget `RequireAuth` middleware on protected endpoints -3. **NEVER** hardcode service URLs - use configuration -4. **NEVER** commit secrets in config files (use env vars in production) -5. **NEVER** hardcode schema prefixes in SQL queries - rely on PostgreSQL `search_path` mechanism -6. **ALWAYS** use PostgreSQL enum type mappings for custom enums -7. **ALWAYS** use transactions for multi-table operations -8. **ALWAYS** start with `attune/` or correct crate name when specifying file paths -9. **ALWAYS** convert runtime names to lowercase for comparison (database may store capitalized) -10. **REMEMBER** IDs are `i64`, not `i32` or `uuid` -11. **REMEMBER** schema is determined by `search_path`, not hardcoded in queries (production uses `attune`, development uses `public`) -12. **REMEMBER** to regenerate SQLx metadata after schema-related changes: `cargo sqlx prepare` - -## Deployment -- **Target**: Distributed deployment with separate service instances -- **Docker**: Dockerfiles for each service (planned in `docker/` dir) -- **Config**: Use environment variables for secrets in production -- **Database**: PostgreSQL 14+ with connection pooling -- **Message Queue**: RabbitMQ required for service communication -- **Web UI**: Static files served separately or via API service - -## Current Development Status -- ✅ **Complete**: Database migrations (17 tables), API service (most endpoints), common library, message queue infrastructure, repository layer, JWT auth, CLI tool, Web UI (basic), Executor service (core functionality), Worker service (shell/Python execution) -- 🔄 **In Progress**: Sensor service, advanced workflow features, Python runtime dependency management -- 📋 **Planned**: Notifier service, execution policies, monitoring, pack registry system - -## Quick Reference - -### Start Development Environment -```bash -# Start PostgreSQL and RabbitMQ -# Load core pack: ./scripts/load-core-pack.sh -# Start API: make run-api -# Start Web UI: cd web && npm run dev -``` - -### File Path Examples -- Models: `attune/crates/common/src/models.rs` -- API routes: `attune/crates/api/src/routes/actions.rs` -- Repositories: `attune/crates/common/src/repositories/execution.rs` -- Migrations: `attune/migrations/*.sql` -- Web UI: `attune/web/src/` -- Config: `attune/config.development.yaml` - -### Documentation Locations -- API docs: `attune/docs/api-*.md` -- Configuration: `attune/docs/configuration.md` -- Architecture: `attune/docs/*-architecture.md`, `attune/docs/*-service.md` -- Testing: `attune/docs/testing-*.md`, `attune/docs/running-tests.md`, `attune/docs/schema-per-test.md` -- AI Agent Work Summaries: `attune/work-summary/*.md` -- Deployment: `attune/docs/production-deployment.md` -- DO NOT create additional documentation files in the root of the project. all new documentation describing how to use the system should be placed in the `attune/docs` directory, and documentation describing the work performed should be placed in the `attune/work-summary` directory. - -## Work Summary & Reporting - -**Avoid redundant summarization - summarize changes once at completion, not continuously.** - -### Guidelines: -- **Report progress** during work: brief status updates, blockers, questions -- **Summarize once** at completion: consolidated overview of all changes made -- **Work summaries**: Write to `attune/work-summary/*.md` only at task completion, not incrementally -- **Avoid duplication**: Don't re-explain the same changes multiple times in different formats -- **What changed, not how**: Focus on outcomes and impacts, not play-by-play narration - -### Good Pattern: -``` -[Making changes with tool calls and brief progress notes] -... -[At completion] -"I've completed the task. Here's a summary of changes: [single consolidated overview]" -``` - -### Bad Pattern: -``` -[Makes changes] -"So I changed X, Y, and Z..." -[More changes] -"To summarize, I modified X, Y, and Z..." -[Writes work summary] -"In this session I updated X, Y, and Z..." -``` - -## Maintaining the AGENTS.md file - -**IMPORTANT: Keep this file up-to-date as the project evolves.** - -After making changes to the project, you MUST update this `AGENTS.md` file if any of the following occur: - -- **New dependencies added or major dependencies removed** (check package.json, Cargo.toml, requirements.txt, etc.) -- **Project structure changes**: new directories/modules created, existing ones renamed or removed -- **Architecture changes**: new layers, patterns, or major refactoring that affects how components interact -- **New frameworks or tools adopted** (e.g., switching from REST to GraphQL, adding a new testing framework) -- **Deployment or infrastructure changes** (new CI/CD pipelines, different hosting, containerization added) -- **New major features** that introduce new subsystems or significantly change existing ones -- **Style guide or coding convention updates** - -### `AGENTS.md` Content inclusion policy -- DO NOT simply summarize changes in the `AGENTS.md` file. If there are existing sections that need updating due to changes in the application architecture or project structure, update them accordingly. -- When relevant, work summaries should instead be written to `attune/work-summary/*.md` - -### Update procedure: -1. After completing your changes, review if they affect any section of `AGENTS.md` -2. If yes, immediately update the relevant sections -3. Add a brief comment at the top of `AGENTS.md` with the date and what was updated (optional but helpful) - -### Update format: -When updating, be surgical - modify only the affected sections rather than rewriting the entire file. Maintain the existing structure and tone. - -**Treat `AGENTS.md` as living documentation.** An outdated `AGENTS.md` file is worse than no `AGENTS.md` file, as it will mislead future AI agents and waste time. - -## Project Documentation Index -{{DOCUMENTATION_INDEX}} diff --git a/Cargo.lock b/Cargo.lock index 2eaf256..ffd210a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,9 +74,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "amq-protocol" -version = "8.3.1" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355603365d2217f7fbc03f0be085ea1440498957890f04276402012cdde445f5" +checksum = "8032525e9bb1bb8aa556476de729106e972b9fb811e5db21ce462a4f0f057d03" dependencies = [ "amq-protocol-tcp", "amq-protocol-types", @@ -88,24 +88,22 @@ dependencies = [ [[package]] name = "amq-protocol-tcp" -version = "8.3.1" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7b97a85e08671697e724a6b7f1459ff81603613695e3151764a9529c6fec15" +checksum = "22f50ebc589843a42a1428b3e1b149164645bfe8c22a7ed0f128ad0af4aaad84" dependencies = [ "amq-protocol-uri", - "async-trait", + "async-rs", "cfg-if", - "executor-trait 2.1.2", - "reactor-trait 2.8.0", "tcp-stream", "tracing", ] [[package]] name = "amq-protocol-types" -version = "8.3.1" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2984a816dba991b5922503921d8f94650792bdeac47c27c83830710d2567f63" +checksum = "12ffea0c942eb17ea55262e4cc57b223d8d6f896269b1313153f9215784dc2b8" dependencies = [ "cookie-factory", "nom 8.0.0", @@ -115,9 +113,9 @@ dependencies = [ [[package]] name = "amq-protocol-uri" -version = "8.3.1" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31db8e69d1456ec8ecf6ee598707179cf1d95f34f7d30037b16ad43f0cddcff" +checksum = "baa9f65c896cb658503e5547e262132ac356c26bc477afedfd8d3f324f4c5006" dependencies = [ "amq-protocol-types", "percent-encoding", @@ -313,6 +311,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-executor" version = "1.14.0" @@ -335,52 +346,10 @@ checksum = "13f937e26114b93193065fd44f507aa2e9169ad0cdabbb996920b1fe1ddea7ba" dependencies = [ "async-channel", "async-executor", - "async-io", "async-lock", "blocking", "futures-lite", -] - -[[package]] -name = "async-global-executor-trait" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9af57045d58eeb1f7060e7025a1631cbc6399e0a1d10ad6735b3d0ea7f8346ce" -dependencies = [ - "async-global-executor", - "async-trait", - "executor-trait 2.1.2", -] - -[[package]] -name = "async-global-executor-trait" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3727b7da74b92d2d03403cf1142706b53423e5c050791af438f8f50edea057a" -dependencies = [ - "async-global-executor", - "async-global-executor-trait 2.2.0", - "async-trait", - "executor-trait 2.1.2", - "executor-trait 3.1.0", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", + "tokio", ] [[package]] @@ -394,18 +363,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-reactor-trait" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab52004af1f14a170088bd9e10a2d3b2f2307ce04320e58a6ce36ee531be625" -dependencies = [ - "async-io", - "async-trait", - "futures-core", - "reactor-trait 3.1.1", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -417,6 +374,23 @@ dependencies = [ "syn", ] +[[package]] +name = "async-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc222a468bdc5ebbe7ce4c595e16d433d531b3b8eda094b2ace18d9fd02fcaa3" +dependencies = [ + "async-compat", + "async-global-executor", + "async-trait", + "cfg-if", + "futures-core", + "futures-io", + "hickory-resolver", + "tokio", + "tokio-stream", +] + [[package]] name = "async-task" version = "4.7.1" @@ -1328,6 +1302,12 @@ dependencies = [ "itertools", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "cron" version = "0.15.0" @@ -1350,6 +1330,15 @@ dependencies = [ "strum", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1742,6 +1731,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1812,24 +1813,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "executor-trait" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c39dff9342e4e0e16ce96be751eb21a94e94a87bb2f6e63ad1961c2ce109bf" -dependencies = [ - "async-trait", -] - -[[package]] -name = "executor-trait" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d6a1fc6700fa12782770cb344a29172ae940ea41d5fd5049fdf236dd6eaa92" -dependencies = [ - "async-trait", -] - [[package]] name = "fancy-regex" version = "0.17.0" @@ -1912,6 +1895,17 @@ dependencies = [ "spin", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2251,6 +2245,52 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -2390,7 +2430,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.2", "system-configuration", "tokio", "tower-service", @@ -2558,6 +2598,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2707,20 +2759,19 @@ dependencies = [ [[package]] name = "lapin" -version = "3.7.2" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913a84142a99160ecef997a5c17c53639bcbac4424a0315a5ffe6c8be8e8db86" +checksum = "16eff4aa0d9ab12052d1f967ddec97cd03be4065eea8ed390e8f1dce1f03bf0a" dependencies = [ "amq-protocol", - "async-global-executor-trait 3.1.0", - "async-reactor-trait", + "async-rs", "async-trait", "backon", - "executor-trait 2.1.2", - "flume", + "cfg-if", + "flume 0.12.0", "futures-core", "futures-io", - "reactor-trait 2.8.0", + "tokio", "tracing", ] @@ -2937,6 +2988,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "moka" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "multer" version = "3.1.0" @@ -3142,6 +3210,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -3506,20 +3578,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "polyval" version = "0.6.2" @@ -3532,6 +3590,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -3640,7 +3704,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.6.2", "thiserror 2.0.18", "tokio", "tracing", @@ -3678,7 +3742,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.2", "tracing", "windows-sys 0.60.2", ] @@ -3809,30 +3873,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "reactor-trait" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffbbf16bc3e4db5fdcf4b77cebf1313610b54b339712aa90088d2d9b1acb1f1" -dependencies = [ - "async-trait", - "reactor-trait 3.1.1", -] - -[[package]] -name = "reactor-trait" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1c85237926dd82e8bc3634240ecf2236ea81e904b3d83cdb1df974af9af293" -dependencies = [ - "async-io", - "async-trait", - "executor-trait 2.1.2", - "flume", - "futures-core", - "futures-io", -] - [[package]] name = "redis" version = "1.0.4" @@ -3853,7 +3893,7 @@ dependencies = [ "pin-project-lite", "ryu", "sha1_smol", - "socket2", + "socket2 0.6.2", "tokio", "tokio-util", "url", @@ -4049,6 +4089,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "ring" version = "0.17.14" @@ -4186,9 +4232,9 @@ dependencies = [ [[package]] name = "rustls-connector" -version = "0.21.11" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10eb7ce243317e6b6a342ef6bff8c2e0d46d78120a9aeb2ee39693a569615c96" +checksum = "f510f2d983baf4a45354ae8ca5abf5a6cdb3c47244ea22f705499d6d9c09a912" dependencies = [ "futures-io", "futures-rustls", @@ -4211,15 +4257,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -4609,6 +4646,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -4818,7 +4865,7 @@ checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", - "flume", + "flume 0.11.1", "futures-channel", "futures-core", "futures-executor", @@ -4938,6 +4985,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tar" version = "0.4.44" @@ -4951,16 +5004,15 @@ dependencies = [ [[package]] name = "tcp-stream" -version = "0.30.9" +version = "0.34.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282ebecea8280bce8b7a0695b5dc93a19839dd445cbba70d3e07c9f6e12c4653" +checksum = "228ee8f41fd20e97f2af4afdd54901b1711aef9d49136d8d6c53f10f4416a4cb" dependencies = [ + "async-rs", "cfg-if", "futures-io", "p12-keystore", - "reactor-trait 2.8.0", "rustls-connector", - "rustls-pemfile", ] [[package]] @@ -5118,7 +5170,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -5912,6 +5964,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -6328,6 +6386,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wiremock" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index b2a7c0b..1996d6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ clap = { version = "4.5", features = ["derive"] } # Message queue / PubSub # RabbitMQ -lapin = "3.7" +lapin = "4.1" # Redis redis = { version = "1.0", features = ["tokio-comp", "connection-manager"] } diff --git a/crates/common/src/mq/connection.rs b/crates/common/src/mq/connection.rs index 039522d..fc207e4 100644 --- a/crates/common/src/mq/connection.rs +++ b/crates/common/src/mq/connection.rs @@ -161,7 +161,7 @@ impl Connection { pub async fn close(&self) -> MqResult<()> { let mut conn_guard = self.connection.write().await; if let Some(conn) = conn_guard.take() { - conn.close(200, "Normal shutdown") + conn.close(200, "Normal shutdown".into()) .await .map_err(|e| MqError::Connection(format!("Failed to close connection: {}", e)))?; info!("Connection closed"); @@ -187,7 +187,7 @@ impl Connection { channel .exchange_declare( - &config.name, + config.name.as_str().into(), kind, ExchangeDeclareOptions { durable: config.durable, @@ -216,7 +216,7 @@ impl Connection { channel .queue_declare( - &config.name, + config.name.as_str().into(), QueueDeclareOptions { durable: config.durable, exclusive: config.exclusive, @@ -248,9 +248,9 @@ impl Connection { channel .queue_bind( - queue, - exchange, - routing_key, + queue.into(), + exchange.into(), + routing_key.into(), QueueBindOptions::default(), FieldTable::default(), ) @@ -315,7 +315,7 @@ impl Connection { channel .queue_declare( - &config.name, + config.name.as_str().into(), QueueDeclareOptions { durable: config.durable, exclusive: config.exclusive, diff --git a/crates/common/src/mq/consumer.rs b/crates/common/src/mq/consumer.rs index ee3831a..9b3bed2 100644 --- a/crates/common/src/mq/consumer.rs +++ b/crates/common/src/mq/consumer.rs @@ -59,8 +59,8 @@ impl Consumer { let consumer = self .channel .basic_consume( - &self.config.queue, - &self.config.tag, + self.config.queue.as_str().into(), + self.config.tag.as_str().into(), BasicConsumeOptions { no_ack: self.config.auto_ack, exclusive: self.config.exclusive, diff --git a/crates/common/src/mq/publisher.rs b/crates/common/src/mq/publisher.rs index 46fdaa0..d21f5e7 100644 --- a/crates/common/src/mq/publisher.rs +++ b/crates/common/src/mq/publisher.rs @@ -88,8 +88,8 @@ impl Publisher { let confirmation = self .channel .basic_publish( - exchange, - routing_key, + exchange.into(), + routing_key.into(), BasicPublishOptions::default(), &payload, properties, @@ -129,8 +129,8 @@ impl Publisher { self.channel .basic_publish( - exchange, - routing_key, + exchange.into(), + routing_key.into(), BasicPublishOptions::default(), payload, properties, diff --git a/crates/core-timer-sensor/src/rule_listener.rs b/crates/core-timer-sensor/src/rule_listener.rs index 02c01b7..f4001ae 100644 --- a/crates/core-timer-sensor/src/rule_listener.rs +++ b/crates/core-timer-sensor/src/rule_listener.rs @@ -61,7 +61,7 @@ impl RuleLifecycleListener { // Declare exchange (idempotent) channel .exchange_declare( - &self.mq_exchange, + self.mq_exchange.as_str().into(), lapin::ExchangeKind::Topic, ExchangeDeclareOptions { durable: true, @@ -78,7 +78,7 @@ impl RuleLifecycleListener { let queue_name = format!("sensor.{}", self.sensor_ref); channel .queue_declare( - &queue_name, + queue_name.as_str().into(), QueueDeclareOptions { durable: true, ..Default::default() @@ -101,9 +101,9 @@ impl RuleLifecycleListener { for routing_key in &routing_keys { channel .queue_bind( - &queue_name, - &self.mq_exchange, - routing_key, + queue_name.as_str().into(), + self.mq_exchange.as_str().into(), + (*routing_key).into(), QueueBindOptions::default(), FieldTable::default(), ) @@ -147,8 +147,8 @@ impl RuleLifecycleListener { // Start consuming messages let consumer = channel .basic_consume( - &queue_name, - "sensor-timer-consumer", + queue_name.as_str().into(), + "sensor-timer-consumer".into(), BasicConsumeOptions { no_ack: false, ..Default::default() diff --git a/crates/sensor/src/rule_lifecycle_listener.rs b/crates/sensor/src/rule_lifecycle_listener.rs index c646a64..6c6f779 100644 --- a/crates/sensor/src/rule_lifecycle_listener.rs +++ b/crates/sensor/src/rule_lifecycle_listener.rs @@ -62,7 +62,7 @@ impl RuleLifecycleListener { consumer .channel() .queue_declare( - queue, + queue.into(), lapin::options::QueueDeclareOptions { durable: true, exclusive: false, @@ -78,9 +78,9 @@ impl RuleLifecycleListener { consumer .channel() .queue_bind( - queue, - exchange, - routing_key, + queue.into(), + exchange.into(), + (*routing_key).into(), lapin::options::QueueBindOptions::default(), lapin::types::FieldTable::default(), ) diff --git a/deny.toml b/deny.toml index e78aef1..28a897f 100644 --- a/deny.toml +++ b/deny.toml @@ -4,17 +4,7 @@ all-features = true [advisories] version = 2 yanked = "deny" -# Note: RUSTSEC-2023-0071 (rsa via sqlx-mysql) is in Cargo.lock but unreachable — -# sqlx-macros-core unconditionally resolves sqlx-mysql; we only use postgres. -# cargo deny's graph analysis correctly identifies it as unreachable, so no -# ignore entry is needed here. If cargo audit is ever re-added, it will need -# --ignore RUSTSEC-2023-0071 since it scans the lockfile without graph analysis. -ignore = [ - # rustls-pemfile v2.x - unmaintained - # Transitive dependency via lapin → amq-protocol-tcp → tcp-stream. - # No alternative available until lapin updates its TLS stack. - { id = "RUSTSEC-2025-0134", reason = "transitive via lapin TLS stack; no alternative" }, -] +ignore = [] [licenses] version = 2 diff --git a/scripts/generate_agents_md_index.py b/scripts/generate_agents_md_index.py deleted file mode 100755 index dcc3b34..0000000 --- a/scripts/generate_agents_md_index.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate AGENTS.md index file in minified format. - -This script scans the docs, scripts, and work-summary directories -and generates a minified index file that helps AI agents quickly -understand the project structure and available documentation. - -The script uses AGENTS.md.template as a base and injects the generated -index at the {{DOCUMENTATION_INDEX}} placeholder. - -Usage: - python scripts/generate_agents_md_index.py -""" - -import os -from collections import defaultdict -from pathlib import Path -from typing import Dict, List, Set - - -def get_project_root() -> Path: - """Get the project root directory (parent of scripts/).""" - script_dir = Path(__file__).parent - return script_dir.parent - - -def scan_directory( - base_path: Path, extensions: Set[str] = None -) -> Dict[str, List[str]]: - """ - Scan a directory and organize files by subdirectory. - - Args: - base_path: Directory to scan - extensions: Set of file extensions to include (e.g., {'.md', '.py'}). None means all files. - - Returns: - Dictionary mapping relative directory paths to lists of filenames - """ - if not base_path.exists(): - return {} - - structure = defaultdict(list) - - for item in sorted(base_path.rglob("*")): - if item.is_file(): - # Filter by extension if specified - if extensions and item.suffix not in extensions: - continue - - # Get relative path from base_path - rel_path = item.relative_to(base_path) - parent_dir = str(rel_path.parent) if rel_path.parent != Path(".") else "" - - structure[parent_dir].append(item.name) - - return structure - - -def format_directory_entry( - dir_path: str, files: List[str], max_files: int = None -) -> str: - """ - Format a directory entry in minified format. - - Args: - dir_path: Directory path (empty string for root) - files: List of filenames in the directory - max_files: Maximum number of files to list before truncating - - Returns: - Formatted string like "path:{file1,file2,...}" - """ - if not files: - return "" - - # Sort files for consistency - sorted_files = sorted(files) - - # Truncate if needed - if max_files and len(sorted_files) > max_files: - file_list = sorted_files[:max_files] + ["..."] - else: - file_list = sorted_files - - files_str = ",".join(file_list) - - if dir_path: - return f"{dir_path}:{{{files_str}}}" - else: - return f"root:{{{files_str}}}" - - -def generate_index_content(root_dirs: Dict[str, Dict[str, any]]) -> str: - """ - Generate the documentation index content. - - Args: - root_dirs: Dictionary mapping directory names to their scan configs - - Returns: - Formatted index content as a string - """ - lines = [] - - lines.append("[Attune Project Documentation Index]") - lines.append("|root: ./") - lines.append( - "|IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning" - ) - lines.append( - "|IMPORTANT: This index provides a quick overview - use grep/read_file for details" - ) - lines.append("|") - lines.append("| Format: path/to/dir:{file1,file2,...}") - lines.append( - "| '...' indicates truncated file list - use grep/list_directory for full contents" - ) - lines.append("|") - lines.append("| To regenerate this index: make generate-agents-index") - lines.append("|") - - # Process each root directory - for dir_name, config in root_dirs.items(): - base_path = config["path"] - extensions = config.get("extensions") - max_files = config.get("max_files", 10) - - structure = scan_directory(base_path, extensions) - - if not structure: - lines.append(f"|{dir_name}: (empty)") - continue - - # Sort directories for consistent output - sorted_dirs = sorted(structure.keys()) - - for dir_path in sorted_dirs: - files = structure[dir_path] - - # Build the full path relative to project root - if dir_path: - full_path = f"{dir_name}/{dir_path}" - else: - full_path = dir_name - - entry = format_directory_entry(full_path, files, max_files) - if entry: - lines.append(f"|{entry}") - - return "\n".join(lines) - - -def generate_agents_md( - template_path: Path, output_path: Path, root_dirs: Dict[str, Dict[str, any]] -) -> None: - """ - Generate the AGENTS.md file using template. - - Args: - template_path: Path to AGENTS.md.template file - output_path: Path where AGENTS.md should be written - root_dirs: Dictionary mapping directory names to their scan configs - """ - # Generate the index content - index_content = generate_index_content(root_dirs) - - # Read the template - if not template_path.exists(): - print(f"⚠️ Template not found at {template_path}") - print(f" Creating AGENTS.md without template...") - content = index_content + "\n" - else: - template = template_path.read_text() - # Inject the index into the template - content = template.replace("{{DOCUMENTATION_INDEX}}", index_content) - - # Write to file - output_path.write_text(content) - print(f"✓ Generated {output_path}") - index_lines = index_content.count("\n") + 1 - total_lines = content.count("\n") + 1 - print(f" Index lines: {index_lines}") - print(f" Total lines: {total_lines}") - - -def main(): - """Main entry point.""" - project_root = get_project_root() - - # Configuration for directories to scan - root_dirs = { - "docs": { - "path": project_root / "docs", - "extensions": {".md", ".txt", ".yaml", ".yml", ".json", ".sh"}, - "max_files": 15, - }, - "scripts": { - "path": project_root / "scripts", - "extensions": {".sh", ".py", ".sql", ".js", ".html"}, - "max_files": 20, - }, - "work-summary": { - "path": project_root / "work-summary", - "extensions": {".md", ".txt"}, - "max_files": 20, - }, - } - - template_path = project_root / "AGENTS.md.template" - output_path = project_root / "AGENTS.md" - - print("Generating AGENTS.md index...") - print(f"Project root: {project_root}") - print(f"Template: {template_path}") - print() - - # Generate the index - generate_agents_md(template_path, output_path, root_dirs) - - print() - print("Index generation complete!") - print(f"Review the generated file at: {output_path}") - - -if __name__ == "__main__": - main()