Compare commits
5 Commits
06229ce98b
...
d63d6f0b9c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63d6f0b9c | ||
|
|
3a1307dcdc | ||
|
|
6ea3108a20 | ||
|
|
81647545e7 | ||
|
|
39a832688d |
742
AGENTS.md.backup
Normal file
742
AGENTS.md.backup
Normal file
@@ -0,0 +1,742 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## RULE 0 - THE FUNDAMENTAL OVERRIDE PEROGATIVE
|
||||||
|
|
||||||
|
If I tell you to do something, even if it goes against what follows below, YOU MUST LISTEN TO ME. I AM IN CHARGE, NOT YOU.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RULE NUMBER 1: NO FILE DELETION
|
||||||
|
|
||||||
|
**YOU ARE NEVER ALLOWED TO DELETE A FILE WITHOUT EXPRESS PERMISSION.** Even a new file that you yourself created, such as a test code file. You have a horrible track record of deleting critically important files or otherwise throwing away tons of expensive work. As a result, you have permanently lost any and all rights to determine that a file or folder should be deleted.
|
||||||
|
|
||||||
|
**YOU MUST ALWAYS ASK AND RECEIVE CLEAR, WRITTEN PERMISSION BEFORE EVER DELETING A FILE OR FOLDER OF ANY KIND.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Irreversible Git & Filesystem Actions — DO NOT EVER BREAK GLASS
|
||||||
|
|
||||||
|
> **Note:** Treat destructive commands as break-glass. If there's any doubt, stop and ask.
|
||||||
|
|
||||||
|
1. **Absolutely forbidden commands:** `git reset --hard`, `git clean -fd`, `rm -rf`, or any command that can delete or overwrite code/data must never be run unless the user explicitly provides the exact command and states, in the same message, that they understand and want the irreversible consequences.
|
||||||
|
2. **No guessing:** If there is any uncertainty about what a command might delete or overwrite, stop immediately and ask the user for specific approval. "I think it's safe" is never acceptable.
|
||||||
|
3. **Safer alternatives first:** When cleanup or rollbacks are needed, request permission to use non-destructive options (`git status`, `git diff`, `git stash`, copying to backups) before ever considering a destructive command.
|
||||||
|
4. **Mandatory explicit plan:** Even after explicit user authorization, restate the command verbatim, list exactly what will be affected, and wait for a confirmation that your understanding is correct. Only then may you execute it—if anything remains ambiguous, refuse and escalate.
|
||||||
|
5. **Document the confirmation:** When running any approved destructive command, record (in the session notes / final response) the exact user text that authorized it, the command actually run, and the execution time. If that record is absent, the operation did not happen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Toolchain: Rust & Cargo
|
||||||
|
|
||||||
|
We only use **Cargo** in this project, NEVER any other package manager.
|
||||||
|
|
||||||
|
- **Edition/toolchain:** Follow `rust-toolchain.toml` (if present). Do not assume stable vs nightly.
|
||||||
|
- **Dependencies:** Explicit versions for stability; keep the set minimal.
|
||||||
|
- **Configuration:** Cargo.toml only
|
||||||
|
- **Unsafe code:** Forbidden (`#![forbid(unsafe_code)]`)
|
||||||
|
|
||||||
|
When writing Rust code, reference RUST_CLI_TOOLS_BEST_PRACTICES.md
|
||||||
|
|
||||||
|
### Release Profile
|
||||||
|
|
||||||
|
Use the release profile defined in `Cargo.toml`. If you need to change it, justify the
|
||||||
|
performance/size tradeoff and how it impacts determinism and cancellation behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Editing Discipline
|
||||||
|
|
||||||
|
### No Script-Based Changes
|
||||||
|
|
||||||
|
**NEVER** run a script that processes/changes code files in this repo. Brittle regex-based transformations create far more problems than they solve.
|
||||||
|
|
||||||
|
- **Always make code changes manually**, even when there are many instances
|
||||||
|
- For many simple changes: use parallel subagents
|
||||||
|
- For subtle/complex changes: do them methodically yourself
|
||||||
|
|
||||||
|
### No File Proliferation
|
||||||
|
|
||||||
|
If you want to change something or add a feature, **revise existing code files in place**.
|
||||||
|
|
||||||
|
**NEVER** create variations like:
|
||||||
|
- `mainV2.rs`
|
||||||
|
- `main_improved.rs`
|
||||||
|
- `main_enhanced.rs`
|
||||||
|
|
||||||
|
New files are reserved for **genuinely new functionality** that makes zero sense to include in any existing file. The bar for creating new files is **incredibly high**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backwards Compatibility
|
||||||
|
|
||||||
|
We do not care about backwards compatibility—we're in early development with no users. We want to do things the **RIGHT** way with **NO TECH DEBT**.
|
||||||
|
|
||||||
|
- Never create "compatibility shims"
|
||||||
|
- Never create wrapper functions for deprecated APIs
|
||||||
|
- Just fix the code directly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compiler Checks (CRITICAL)
|
||||||
|
|
||||||
|
**After any substantive code changes, you MUST verify no errors were introduced:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for compiler errors and warnings
|
||||||
|
cargo check --all-targets
|
||||||
|
|
||||||
|
# Check for clippy lints (pedantic + nursery are enabled)
|
||||||
|
cargo clippy --all-targets -- -D warnings
|
||||||
|
|
||||||
|
# Verify formatting
|
||||||
|
cargo fmt --check
|
||||||
|
```
|
||||||
|
|
||||||
|
If you see errors, **carefully understand and resolve each issue**. Read sufficient context to fix them the RIGHT way.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit & Property Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Run with output
|
||||||
|
cargo test -- --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
When adding or changing primitives, add tests that assert the core invariants:
|
||||||
|
|
||||||
|
- no task leaks
|
||||||
|
- no obligation leaks
|
||||||
|
- losers are drained after races
|
||||||
|
- region close implies quiescence
|
||||||
|
|
||||||
|
Prefer deterministic lab-runtime tests for concurrency-sensitive behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Agent Mail — Multi-Agent Coordination
|
||||||
|
|
||||||
|
A mail-like layer that lets coding agents coordinate asynchronously via MCP tools and resources. Provides identities, inbox/outbox, searchable threads, and advisory file reservations with human-auditable artifacts in Git.
|
||||||
|
|
||||||
|
### Why It's Useful
|
||||||
|
|
||||||
|
- **Prevents conflicts:** Explicit file reservations (leases) for files/globs
|
||||||
|
- **Token-efficient:** Messages stored in per-project archive, not in context
|
||||||
|
- **Quick reads:** `resource://inbox/...`, `resource://thread/...`
|
||||||
|
|
||||||
|
### Same Repository Workflow
|
||||||
|
|
||||||
|
1. **Register identity:**
|
||||||
|
```
|
||||||
|
ensure_project(project_key=<abs-path>)
|
||||||
|
register_agent(project_key, program, model)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Reserve files before editing:**
|
||||||
|
```
|
||||||
|
file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Communicate with threads:**
|
||||||
|
```
|
||||||
|
send_message(..., thread_id="FEAT-123")
|
||||||
|
fetch_inbox(project_key, agent_name)
|
||||||
|
acknowledge_message(project_key, agent_name, message_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Quick reads:**
|
||||||
|
```
|
||||||
|
resource://inbox/{Agent}?project=<abs-path>&limit=20
|
||||||
|
resource://thread/{id}?project=<abs-path>&include_bodies=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Macros vs Granular Tools
|
||||||
|
|
||||||
|
- **Prefer macros for speed:** `macro_start_session`, `macro_prepare_thread`, `macro_file_reservation_cycle`, `macro_contact_handshake`
|
||||||
|
- **Use granular tools for control:** `register_agent`, `file_reservation_paths`, `send_message`, `fetch_inbox`, `acknowledge_message`
|
||||||
|
|
||||||
|
### Common Pitfalls
|
||||||
|
|
||||||
|
- `"from_agent not registered"`: Always `register_agent` in the correct `project_key` first
|
||||||
|
- `"FILE_RESERVATION_CONFLICT"`: Adjust patterns, wait for expiry, or use non-exclusive reservation
|
||||||
|
- **Auth errors:** If JWT+JWKS enabled, include bearer token with matching `kid`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beads (br) — Dependency-Aware Issue Tracking
|
||||||
|
|
||||||
|
Beads provides a lightweight, dependency-aware issue database and CLI (`br` / beads_rust) for selecting "ready work," setting priorities, and tracking status. It complements MCP Agent Mail's messaging and file reservations.
|
||||||
|
|
||||||
|
**Note:** `br` is non-invasive—it never executes git commands directly. You must run git commands manually after `br sync --flush-only`.
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
|
||||||
|
- **Single source of truth:** Beads for task status/priority/dependencies; Agent Mail for conversation and audit
|
||||||
|
- **Shared identifiers:** Use Beads issue ID (e.g., `br-123`) as Mail `thread_id` and prefix subjects with `[br-123]`
|
||||||
|
- **Reservations:** When starting a task, call `file_reservation_paths()` with the issue ID in `reason`
|
||||||
|
|
||||||
|
### Typical Agent Flow
|
||||||
|
|
||||||
|
1. **Pick ready work (Beads):**
|
||||||
|
```bash
|
||||||
|
br ready --json # Choose highest priority, no blockers
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Reserve edit surface (Mail):**
|
||||||
|
```
|
||||||
|
file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true, reason="br-123")
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Announce start (Mail):**
|
||||||
|
```
|
||||||
|
send_message(..., thread_id="br-123", subject="[br-123] Start: <title>", ack_required=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Work and update:** Reply in-thread with progress
|
||||||
|
|
||||||
|
5. **Complete and release:**
|
||||||
|
```bash
|
||||||
|
br close br-123 --reason "Completed"
|
||||||
|
```
|
||||||
|
```
|
||||||
|
release_file_reservations(project_key, agent_name, paths=["src/**"])
|
||||||
|
```
|
||||||
|
Final Mail reply: `[br-123] Completed` with summary
|
||||||
|
|
||||||
|
### Mapping Cheat Sheet
|
||||||
|
|
||||||
|
| Concept | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Mail `thread_id` | `br-###` |
|
||||||
|
| Mail subject | `[br-###] ...` |
|
||||||
|
| File reservation `reason` | `br-###` |
|
||||||
|
| Commit messages | Include `br-###` for traceability |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## bv — Graph-Aware Triage Engine
|
||||||
|
|
||||||
|
bv is a graph-aware triage engine for Beads projects (`.beads/beads.jsonl`). It computes PageRank, betweenness, critical path, cycles, HITS, eigenvector, and k-core metrics deterministically.
|
||||||
|
|
||||||
|
**Scope boundary:** bv handles *what to work on* (triage, priority, planning). For agent-to-agent coordination (messaging, work claiming, file reservations), use MCP Agent Mail.
|
||||||
|
|
||||||
|
**CRITICAL: Use ONLY `--robot-*` flags. Bare `bv` launches an interactive TUI that blocks your session.**
|
||||||
|
|
||||||
|
### The Workflow: Start With Triage
|
||||||
|
|
||||||
|
**`bv --robot-triage` is your single entry point.** It returns:
|
||||||
|
- `quick_ref`: at-a-glance counts + top 3 picks
|
||||||
|
- `recommendations`: ranked actionable items with scores, reasons, unblock info
|
||||||
|
- `quick_wins`: low-effort high-impact items
|
||||||
|
- `blockers_to_clear`: items that unblock the most downstream work
|
||||||
|
- `project_health`: status/type/priority distributions, graph metrics
|
||||||
|
- `commands`: copy-paste shell commands for next steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bv --robot-triage # THE MEGA-COMMAND: start here
|
||||||
|
bv --robot-next # Minimal: just the single top pick + claim command
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Reference
|
||||||
|
|
||||||
|
**Planning:**
|
||||||
|
| Command | Returns |
|
||||||
|
|---------|---------|
|
||||||
|
| `--robot-plan` | Parallel execution tracks with `unblocks` lists |
|
||||||
|
| `--robot-priority` | Priority misalignment detection with confidence |
|
||||||
|
|
||||||
|
**Graph Analysis:**
|
||||||
|
| Command | Returns |
|
||||||
|
|---------|---------|
|
||||||
|
| `--robot-insights` | Full metrics: PageRank, betweenness, HITS, eigenvector, critical path, cycles, k-core, articulation points, slack |
|
||||||
|
| `--robot-label-health` | Per-label health: `health_level`, `velocity_score`, `staleness`, `blocked_count` |
|
||||||
|
| `--robot-label-flow` | Cross-label dependency: `flow_matrix`, `dependencies`, `bottleneck_labels` |
|
||||||
|
| `--robot-label-attention [--attention-limit=N]` | Attention-ranked labels |
|
||||||
|
|
||||||
|
**History & Change Tracking:**
|
||||||
|
| Command | Returns |
|
||||||
|
|---------|---------|
|
||||||
|
| `--robot-history` | Bead-to-commit correlations |
|
||||||
|
| `--robot-diff --diff-since <ref>` | Changes since ref: new/closed/modified issues, cycles |
|
||||||
|
|
||||||
|
**Other:**
|
||||||
|
| Command | Returns |
|
||||||
|
|---------|---------|
|
||||||
|
| `--robot-burndown <sprint>` | Sprint burndown, scope changes, at-risk items |
|
||||||
|
| `--robot-forecast <id\|all>` | ETA predictions with dependency-aware scheduling |
|
||||||
|
| `--robot-alerts` | Stale issues, blocking cascades, priority mismatches |
|
||||||
|
| `--robot-suggest` | Hygiene: duplicates, missing deps, label suggestions |
|
||||||
|
| `--robot-graph [--graph-format=json\|dot\|mermaid]` | Dependency graph export |
|
||||||
|
| `--export-graph <file.html>` | Interactive HTML visualization |
|
||||||
|
|
||||||
|
### Scoping & Filtering
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bv --robot-plan --label backend # Scope to label's subgraph
|
||||||
|
bv --robot-insights --as-of HEAD~30 # Historical point-in-time
|
||||||
|
bv --recipe actionable --robot-plan # Pre-filter: ready to work
|
||||||
|
bv --recipe high-impact --robot-triage # Pre-filter: top PageRank
|
||||||
|
bv --robot-triage --robot-triage-by-track # Group by parallel work streams
|
||||||
|
bv --robot-triage --robot-triage-by-label # Group by domain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Understanding Robot Output
|
||||||
|
|
||||||
|
**All robot JSON includes:**
|
||||||
|
- `data_hash` — Fingerprint of source beads.jsonl
|
||||||
|
- `status` — Per-metric state: `computed|approx|timeout|skipped` + elapsed ms
|
||||||
|
- `as_of` / `as_of_commit` — Present when using `--as-of`
|
||||||
|
|
||||||
|
**Two-phase analysis:**
|
||||||
|
- **Phase 1 (instant):** degree, topo sort, density
|
||||||
|
- **Phase 2 (async, 500ms timeout):** PageRank, betweenness, HITS, eigenvector, cycles
|
||||||
|
|
||||||
|
### jq Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bv --robot-triage | jq '.quick_ref' # At-a-glance summary
|
||||||
|
bv --robot-triage | jq '.recommendations[0]' # Top recommendation
|
||||||
|
bv --robot-plan | jq '.plan.summary.highest_impact' # Best unblock target
|
||||||
|
bv --robot-insights | jq '.status' # Check metric readiness
|
||||||
|
bv --robot-insights | jq '.Cycles' # Circular deps (must fix!)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UBS — Ultimate Bug Scanner
|
||||||
|
|
||||||
|
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubs file.rs file2.rs # Specific files (< 1s) — USE THIS
|
||||||
|
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||||
|
ubs --only=rust,toml src/ # Language filter (3-5x faster)
|
||||||
|
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||||
|
ubs . # Whole project (ignores target/, Cargo.lock)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Format
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ Category (N errors)
|
||||||
|
file.rs:42:5 – Issue description
|
||||||
|
💡 Suggested fix
|
||||||
|
Exit code: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||||
|
|
||||||
|
### Fix Workflow
|
||||||
|
|
||||||
|
1. Read finding → category + fix suggestion
|
||||||
|
2. Navigate `file:line:col` → view context
|
||||||
|
3. Verify real issue (not false positive)
|
||||||
|
4. Fix root cause (not symptom)
|
||||||
|
5. Re-run `ubs <file>` → exit 0
|
||||||
|
6. Commit
|
||||||
|
|
||||||
|
### Bug Severity
|
||||||
|
|
||||||
|
- **Critical (always fix):** Memory safety, use-after-free, data races, SQL injection
|
||||||
|
- **Important (production):** Unwrap panics, resource leaks, overflow checks
|
||||||
|
- **Contextual (judgment):** TODO/FIXME, println! debugging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ast-grep vs ripgrep
|
||||||
|
|
||||||
|
**Use `ast-grep` when structure matters.** It parses code and matches AST nodes, ignoring comments/strings, and can **safely rewrite** code.
|
||||||
|
|
||||||
|
- Refactors/codemods: rename APIs, change import forms
|
||||||
|
- Policy checks: enforce patterns across a repo
|
||||||
|
- Editor/automation: LSP mode, `--json` output
|
||||||
|
|
||||||
|
**Use `ripgrep` when text is enough.** Fastest way to grep literals/regex.
|
||||||
|
|
||||||
|
- Recon: find strings, TODOs, log lines, config values
|
||||||
|
- Pre-filter: narrow candidate files before ast-grep
|
||||||
|
|
||||||
|
### Rule of Thumb
|
||||||
|
|
||||||
|
- Need correctness or **applying changes** → `ast-grep`
|
||||||
|
- Need raw speed or **hunting text** → `rg`
|
||||||
|
- Often combine: `rg` to shortlist files, then `ast-grep` to match/modify
|
||||||
|
|
||||||
|
### Rust Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find structured code (ignores comments)
|
||||||
|
ast-grep run -l Rust -p 'fn $NAME($$$ARGS) -> $RET { $$$BODY }'
|
||||||
|
|
||||||
|
# Find all unwrap() calls
|
||||||
|
ast-grep run -l Rust -p '$EXPR.unwrap()'
|
||||||
|
|
||||||
|
# Quick textual hunt
|
||||||
|
rg -n 'println!' -t rust
|
||||||
|
|
||||||
|
# Combine speed + precision
|
||||||
|
rg -l -t rust 'unwrap\(' | xargs ast-grep run -l Rust -p '$X.unwrap()' --json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Morph Warp Grep — AI-Powered Code Search
|
||||||
|
|
||||||
|
**Use `mcp__morph-mcp__warp_grep` for exploratory "how does X work?" questions.** An AI agent expands your query, greps the codebase, reads relevant files, and returns precise line ranges with full context.
|
||||||
|
|
||||||
|
**Use `ripgrep` for targeted searches.** When you know exactly what you're looking for.
|
||||||
|
|
||||||
|
**Use `ast-grep` for structural patterns.** When you need AST precision for matching/rewriting.
|
||||||
|
|
||||||
|
### When to Use What
|
||||||
|
|
||||||
|
| Scenario | Tool | Why |
|
||||||
|
|----------|------|-----|
|
||||||
|
| "How is pattern matching implemented?" | `warp_grep` | Exploratory; don't know where to start |
|
||||||
|
| "Where is the quick reject filter?" | `warp_grep` | Need to understand architecture |
|
||||||
|
| "Find all uses of `Regex::new`" | `ripgrep` | Targeted literal search |
|
||||||
|
| "Find files with `println!`" | `ripgrep` | Simple pattern |
|
||||||
|
| "Replace all `unwrap()` with `expect()`" | `ast-grep` | Structural refactor |
|
||||||
|
|
||||||
|
### warp_grep Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp__morph-mcp__warp_grep(
|
||||||
|
repoPath: "/path/to/dcg",
|
||||||
|
query: "How does the safe pattern whitelist work?"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns structured results with file paths, line ranges, and extracted code snippets.
|
||||||
|
|
||||||
|
### Anti-Patterns
|
||||||
|
|
||||||
|
- **Don't** use `warp_grep` to find a specific function name → use `ripgrep`
|
||||||
|
- **Don't** use `ripgrep` to understand "how does X work" → wastes time with manual reads
|
||||||
|
- **Don't** use `ripgrep` for codemods → risks collateral edits
|
||||||
|
|
||||||
|
<!-- bv-agent-instructions-v1 -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beads Workflow Integration
|
||||||
|
|
||||||
|
This project uses [beads_viewer](https://github.com/Dicklesworthstone/beads_viewer) for issue tracking. Issues are stored in `.beads/` and tracked in git.
|
||||||
|
|
||||||
|
**Note:** `br` is non-invasive—it never executes git commands directly. You must run git commands manually after `br sync --flush-only`.
|
||||||
|
|
||||||
|
### Essential Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View issues (launches TUI - avoid in automated sessions)
|
||||||
|
bv
|
||||||
|
|
||||||
|
# CLI commands for agents (use these instead)
|
||||||
|
br ready # Show issues ready to work (no blockers)
|
||||||
|
br list --status=open # All open issues
|
||||||
|
br show <id> # Full issue details with dependencies
|
||||||
|
br create --title="..." --type=task --priority=2
|
||||||
|
br update <id> --status=in_progress
|
||||||
|
br close <id> --reason="Completed"
|
||||||
|
br close <id1> <id2> # Close multiple issues at once
|
||||||
|
br sync --flush-only # Export to JSONL (then manually: git add .beads/ && git commit)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow Pattern
|
||||||
|
|
||||||
|
1. **Start**: Run `br ready` to find actionable work
|
||||||
|
2. **Claim**: Use `br update <id> --status=in_progress`
|
||||||
|
3. **Work**: Implement the task
|
||||||
|
4. **Complete**: Use `br close <id>`
|
||||||
|
5. **Sync**: Run `br sync --flush-only`, then `git add .beads/ && git commit -m "Update beads"`
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
|
||||||
|
- **Dependencies**: Issues can block other issues. `br ready` shows only unblocked work.
|
||||||
|
- **Priority**: P0=critical, P1=high, P2=medium, P3=low, P4=backlog (use numbers, not words)
|
||||||
|
- **Types**: task, bug, feature, epic, question, docs
|
||||||
|
- **Blocking**: `br dep add <issue> <depends-on>` to add dependencies
|
||||||
|
|
||||||
|
### Session Protocol
|
||||||
|
|
||||||
|
**Before ending any session, run this checklist:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status # Check what changed
|
||||||
|
git add <files> # Stage code changes
|
||||||
|
br sync --flush-only # Export beads to JSONL
|
||||||
|
git add .beads/ # Stage beads changes
|
||||||
|
git commit -m "..." # Commit code and beads
|
||||||
|
git push # Push to remote
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
- Check `br ready` at session start to find available work
|
||||||
|
- Update status as you work (in_progress → closed)
|
||||||
|
- Create new issues with `br create` when you discover tasks
|
||||||
|
- Use descriptive titles and set appropriate priority/type
|
||||||
|
- Always run `br sync --flush-only` then commit .beads/ before ending session
|
||||||
|
|
||||||
|
<!-- end-bv-agent-instructions -->
|
||||||
|
|
||||||
|
## Landing the Plane (Session Completion)
|
||||||
|
|
||||||
|
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
|
||||||
|
|
||||||
|
**MANDATORY WORKFLOW:**
|
||||||
|
|
||||||
|
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
||||||
|
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||||||
|
3. **Update issue status** - Close finished work, update in-progress items
|
||||||
|
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||||
|
```bash
|
||||||
|
git pull --rebase
|
||||||
|
br sync --flush-only
|
||||||
|
git add .beads/
|
||||||
|
git commit -m "Update beads"
|
||||||
|
git push
|
||||||
|
git status # MUST show "up to date with origin"
|
||||||
|
```
|
||||||
|
5. **Clean up** - Clear stashes, prune remote branches
|
||||||
|
6. **Verify** - All changes committed AND pushed
|
||||||
|
7. **Hand off** - Provide context for next session
|
||||||
|
|
||||||
|
**CRITICAL RULES:**
|
||||||
|
- Work is NOT complete until `git push` succeeds
|
||||||
|
- NEVER stop before pushing - that leaves work stranded locally
|
||||||
|
- NEVER say "ready to push when you are" - YOU must push
|
||||||
|
- If push fails, resolve and retry until it succeeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## cass — Cross-Agent Session Search
|
||||||
|
|
||||||
|
`cass` indexes prior agent conversations (Claude Code, Codex, Cursor, Gemini, ChatGPT, etc.) so we can reuse solved problems.
|
||||||
|
|
||||||
|
**Rules:** Never run bare `cass` (TUI). Always use `--robot` or `--json`.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cass health
|
||||||
|
cass search "async runtime" --robot --limit 5
|
||||||
|
cass view /path/to/session.jsonl -n 42 --json
|
||||||
|
cass expand /path/to/session.jsonl -n 42 -C 3 --json
|
||||||
|
cass capabilities --json
|
||||||
|
cass robot-docs guide
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
- Use `--fields minimal` for lean output
|
||||||
|
- Filter by agent with `--agent`
|
||||||
|
- Use `--days N` to limit to recent history
|
||||||
|
|
||||||
|
stdout is data-only, stderr is diagnostics; exit code 0 means success.
|
||||||
|
|
||||||
|
Treat cass as a way to avoid re-solving problems other agents already handled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note for Codex/GPT-5.2
|
||||||
|
|
||||||
|
You constantly bother me and stop working with concerned questions that look similar to this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Unexpected changes (need guidance)
|
||||||
|
|
||||||
|
- Working tree still shows edits I did not make in Cargo.toml, Cargo.lock, src/runtime.rs, src/scope.rs. Please advise whether to keep/commit/revert these before any further work. I did not touch them.
|
||||||
|
|
||||||
|
Next steps (pick one)
|
||||||
|
|
||||||
|
1. Decide how to handle the unrelated modified files above so we can resume cleanly.
|
||||||
|
```
|
||||||
|
|
||||||
|
NEVER EVER DO THAT AGAIN. The answer is literally ALWAYS the same: those are changes created by the potentially dozen of other agents working on the project at the same time. This is not only a common occurence, it happens multiple times PER MINUTE. The way to deal with it is simple: you NEVER, under ANY CIRCUMSTANCE, stash, revert, overwrite, or otherwise disturb in ANY way the work of other agents. Just treat those changes identically to changes that you yourself made. Just fool yourself into thinking YOU made the changes and simply don't recall it for some reason.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note on Built-in TODO Functionality
|
||||||
|
|
||||||
|
Also, if I ask you to explicitly use your built-in TODO functionality, don't complain about this and say you need to use beads. You can use built-in TODOs if I tell you specifically to do so. Always comply with such orders.
|
||||||
|
|
||||||
|
## TDD Requirements
|
||||||
|
|
||||||
|
Test-first development is mandatory:
|
||||||
|
1. **RED** - Write failing test first
|
||||||
|
2. **GREEN** - Minimal implementation to pass
|
||||||
|
3. **REFACTOR** - Clean up while green
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
Find the simplest solution that meets all acceptance criteria.
|
||||||
|
Use third party libraries whenever there's a well-maintained, active, and widely adopted solution (for example, date-fns for TS date math)
|
||||||
|
Build extensible pieces of logic that can easily be integrated with other pieces.
|
||||||
|
DRY principles should be loosely held.
|
||||||
|
Architecture MUST be clear and well thought-out. Ask the user for clarification whenever ambiguity is discovered around architecture, or you think a better approach than planned exists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Third-Party Library Usage
|
||||||
|
|
||||||
|
If you aren't 100% sure how to use a third-party library, **SEARCH ONLINE** to find the latest documentation and mid-2025 best practices.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gitlore Robot Mode
|
||||||
|
|
||||||
|
The `lore` CLI has a robot mode optimized for AI agent consumption with compact JSON output, structured errors with machine-actionable recovery steps, meaningful exit codes, response timing metadata, field selection for token efficiency, and TTY auto-detection.
|
||||||
|
|
||||||
|
### Activation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Explicit flag
|
||||||
|
lore --robot issues -n 10
|
||||||
|
|
||||||
|
# JSON shorthand (-J)
|
||||||
|
lore -J issues -n 10
|
||||||
|
|
||||||
|
# Auto-detection (when stdout is not a TTY)
|
||||||
|
lore issues | jq .
|
||||||
|
|
||||||
|
# Environment variable
|
||||||
|
LORE_ROBOT=1 lore issues
|
||||||
|
```
|
||||||
|
|
||||||
|
### Robot Mode Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List issues/MRs with JSON output
|
||||||
|
lore --robot issues -n 10
|
||||||
|
lore --robot mrs -s opened
|
||||||
|
|
||||||
|
# List with field selection (reduces token usage ~60%)
|
||||||
|
lore --robot issues --fields minimal
|
||||||
|
lore --robot mrs --fields iid,title,state,draft
|
||||||
|
|
||||||
|
# Show detailed entity info
|
||||||
|
lore --robot issues 123
|
||||||
|
lore --robot mrs 456 -p group/repo
|
||||||
|
|
||||||
|
# Count entities
|
||||||
|
lore --robot count issues
|
||||||
|
lore --robot count discussions --for mr
|
||||||
|
|
||||||
|
# Search indexed documents
|
||||||
|
lore --robot search "authentication bug"
|
||||||
|
|
||||||
|
# Check sync status
|
||||||
|
lore --robot status
|
||||||
|
|
||||||
|
# Run full sync pipeline
|
||||||
|
lore --robot sync
|
||||||
|
|
||||||
|
# Run sync without resource events
|
||||||
|
lore --robot sync --no-events
|
||||||
|
|
||||||
|
# Run ingestion only
|
||||||
|
lore --robot ingest issues
|
||||||
|
|
||||||
|
# Check environment health
|
||||||
|
lore --robot doctor
|
||||||
|
|
||||||
|
# Document and index statistics
|
||||||
|
lore --robot stats
|
||||||
|
|
||||||
|
# Quick health pre-flight check (exit 0 = healthy, 19 = unhealthy)
|
||||||
|
lore --robot health
|
||||||
|
|
||||||
|
# Generate searchable documents from ingested data
|
||||||
|
lore --robot generate-docs
|
||||||
|
|
||||||
|
# Generate vector embeddings via Ollama
|
||||||
|
lore --robot embed
|
||||||
|
|
||||||
|
# Agent self-discovery manifest (all commands, flags, exit codes, response schemas)
|
||||||
|
lore robot-docs
|
||||||
|
|
||||||
|
# Version information
|
||||||
|
lore --robot version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Format
|
||||||
|
|
||||||
|
All commands return compact JSON with a uniform envelope and timing metadata:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ok":true,"data":{...},"meta":{"elapsed_ms":42}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors return structured JSON to stderr with machine-actionable recovery steps:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"error":{"code":"CONFIG_NOT_FOUND","message":"...","suggestion":"Run 'lore init'","actions":["lore init"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `actions` array contains executable shell commands for automated recovery. It is omitted when empty.
|
||||||
|
|
||||||
|
### Field Selection
|
||||||
|
|
||||||
|
The `--fields` flag on `issues` and `mrs` list commands controls which fields appear in the JSON response:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lore -J issues --fields minimal # Preset: iid, title, state, updated_at_iso
|
||||||
|
lore -J mrs --fields iid,title,state,draft,labels # Custom field list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | Success |
|
||||||
|
| 1 | Internal error / not implemented |
|
||||||
|
| 2 | Usage error (invalid flags or arguments) |
|
||||||
|
| 3 | Config invalid |
|
||||||
|
| 4 | Token not set |
|
||||||
|
| 5 | GitLab auth failed |
|
||||||
|
| 6 | Resource not found |
|
||||||
|
| 7 | Rate limited |
|
||||||
|
| 8 | Network error |
|
||||||
|
| 9 | Database locked |
|
||||||
|
| 10 | Database error |
|
||||||
|
| 11 | Migration failed |
|
||||||
|
| 12 | I/O error |
|
||||||
|
| 13 | Transform error |
|
||||||
|
| 14 | Ollama unavailable |
|
||||||
|
| 15 | Ollama model not found |
|
||||||
|
| 16 | Embedding failed |
|
||||||
|
| 17 | Not found (entity does not exist) |
|
||||||
|
| 18 | Ambiguous match (use `-p` to specify project) |
|
||||||
|
| 19 | Health check failed |
|
||||||
|
| 20 | Config not found |
|
||||||
|
|
||||||
|
### Configuration Precedence
|
||||||
|
|
||||||
|
1. CLI flags (highest priority)
|
||||||
|
2. Environment variables (`LORE_ROBOT`, `GITLAB_TOKEN`, `LORE_CONFIG_PATH`)
|
||||||
|
3. Config file (`~/.config/lore/config.json`)
|
||||||
|
4. Built-in defaults (lowest priority)
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
- Use `lore --robot` or `lore -J` for all agent interactions
|
||||||
|
- Check exit codes for error handling
|
||||||
|
- Parse JSON errors from stderr; use `actions` array for automated recovery
|
||||||
|
- Use `--fields minimal` to reduce token usage (~60% fewer tokens)
|
||||||
|
- Use `-n` / `--limit` to control response size
|
||||||
|
- Use `-q` / `--quiet` to suppress progress bars and non-essential output
|
||||||
|
- Use `--color never` in non-TTY automation for ANSI-free output
|
||||||
|
- Use `-v` / `-vv` / `-vvv` for increasing verbosity (debug/trace logging)
|
||||||
|
- Use `--log-format json` for machine-readable log output to stderr
|
||||||
|
- TTY detection handles piped commands automatically
|
||||||
|
- Use `lore --robot health` as a fast pre-flight check before queries
|
||||||
|
- Use `lore robot-docs` for response schema discovery
|
||||||
|
- The `-p` flag supports fuzzy project matching (suffix and substring)
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1106,7 +1106,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lore"
|
name = "lore"
|
||||||
version = "0.5.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lore"
|
name = "lore"
|
||||||
version = "0.5.2"
|
version = "0.6.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Gitlore - Local GitLab data management with semantic search"
|
description = "Gitlore - Local GitLab data management with semantic search"
|
||||||
authors = ["Taylor Eernisse"]
|
authors = ["Taylor Eernisse"]
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ Configuration is stored in `~/.config/lore/config.json` (or `$XDG_CONFIG_HOME/lo
|
|||||||
{ "path": "group/project" },
|
{ "path": "group/project" },
|
||||||
{ "path": "other-group/other-project" }
|
{ "path": "other-group/other-project" }
|
||||||
],
|
],
|
||||||
|
"defaultProject": "group/project",
|
||||||
"sync": {
|
"sync": {
|
||||||
"backfillDays": 14,
|
"backfillDays": 14,
|
||||||
"staleLockMinutes": 10,
|
"staleLockMinutes": 10,
|
||||||
@@ -119,6 +120,7 @@ Configuration is stored in `~/.config/lore/config.json` (or `$XDG_CONFIG_HOME/lo
|
|||||||
| `gitlab` | `baseUrl` | -- | GitLab instance URL (required) |
|
| `gitlab` | `baseUrl` | -- | GitLab instance URL (required) |
|
||||||
| `gitlab` | `tokenEnvVar` | `GITLAB_TOKEN` | Environment variable containing API token |
|
| `gitlab` | `tokenEnvVar` | `GITLAB_TOKEN` | Environment variable containing API token |
|
||||||
| `projects` | `path` | -- | Project path (e.g., `group/project`) |
|
| `projects` | `path` | -- | Project path (e.g., `group/project`) |
|
||||||
|
| *(top-level)* | `defaultProject` | none | Fallback project path used when `-p` is omitted. Must match a configured project path (exact or suffix). CLI `-p` always overrides. |
|
||||||
| `sync` | `backfillDays` | `14` | Days to backfill on initial sync |
|
| `sync` | `backfillDays` | `14` | Days to backfill on initial sync |
|
||||||
| `sync` | `staleLockMinutes` | `10` | Minutes before sync lock considered stale |
|
| `sync` | `staleLockMinutes` | `10` | Minutes before sync lock considered stale |
|
||||||
| `sync` | `heartbeatIntervalSeconds` | `30` | Frequency of lock heartbeat updates |
|
| `sync` | `heartbeatIntervalSeconds` | `30` | Frequency of lock heartbeat updates |
|
||||||
@@ -204,7 +206,7 @@ When showing a single issue (e.g., `lore issues 123`), output includes: title, d
|
|||||||
|
|
||||||
#### Project Resolution
|
#### Project Resolution
|
||||||
|
|
||||||
The `-p` / `--project` flag uses cascading match logic across all commands:
|
When `-p` / `--project` is omitted, the `defaultProject` from config is used as a fallback. If neither is set, results span all configured projects. When a project is specified (via `-p` or config default), it uses cascading match logic across all commands:
|
||||||
|
|
||||||
1. **Exact match**: `group/project`
|
1. **Exact match**: `group/project`
|
||||||
2. **Case-insensitive**: `Group/Project`
|
2. **Case-insensitive**: `Group/Project`
|
||||||
@@ -508,12 +510,15 @@ lore init --force # Overwrite existing config
|
|||||||
lore init --non-interactive # Fail if prompts needed
|
lore init --non-interactive # Fail if prompts needed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When multiple projects are configured, `init` prompts whether to set a default project (used when `-p` is omitted). This can also be set via the `--default-project` flag.
|
||||||
|
|
||||||
In robot mode, `init` supports non-interactive setup via flags:
|
In robot mode, `init` supports non-interactive setup via flags:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lore -J init --gitlab-url https://gitlab.com \
|
lore -J init --gitlab-url https://gitlab.com \
|
||||||
--token-env-var GITLAB_TOKEN \
|
--token-env-var GITLAB_TOKEN \
|
||||||
--projects "group/project,other/project"
|
--projects "group/project,other/project" \
|
||||||
|
--default-project group/project
|
||||||
```
|
```
|
||||||
|
|
||||||
### `lore auth`
|
### `lore auth`
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
"--gitlab-url",
|
"--gitlab-url",
|
||||||
"--token-env-var",
|
"--token-env-var",
|
||||||
"--projects",
|
"--projects",
|
||||||
|
"--default-project",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
("generate-docs", &["--full", "--project"]),
|
("generate-docs", &["--full", "--project"]),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub struct InitInputs {
|
|||||||
pub gitlab_url: String,
|
pub gitlab_url: String,
|
||||||
pub token_env_var: String,
|
pub token_env_var: String,
|
||||||
pub project_paths: Vec<String>,
|
pub project_paths: Vec<String>,
|
||||||
|
pub default_project: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InitOptions {
|
pub struct InitOptions {
|
||||||
@@ -23,6 +24,7 @@ pub struct InitResult {
|
|||||||
pub data_dir: String,
|
pub data_dir: String,
|
||||||
pub user: UserInfo,
|
pub user: UserInfo,
|
||||||
pub projects: Vec<ProjectInfo>,
|
pub projects: Vec<ProjectInfo>,
|
||||||
|
pub default_project: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserInfo {
|
pub struct UserInfo {
|
||||||
@@ -104,6 +106,20 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitRe
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate default_project matches one of the configured project paths
|
||||||
|
if let Some(ref dp) = inputs.default_project {
|
||||||
|
let matched = inputs.project_paths.iter().any(|p| {
|
||||||
|
p.eq_ignore_ascii_case(dp)
|
||||||
|
|| p.to_ascii_lowercase()
|
||||||
|
.ends_with(&format!("/{}", dp.to_ascii_lowercase()))
|
||||||
|
});
|
||||||
|
if !matched {
|
||||||
|
return Err(LoreError::Other(format!(
|
||||||
|
"defaultProject '{dp}' does not match any configured project path"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(parent) = config_path.parent() {
|
if let Some(parent) = config_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
@@ -118,6 +134,7 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitRe
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|p| ProjectConfig { path: p.clone() })
|
.map(|p| ProjectConfig { path: p.clone() })
|
||||||
.collect(),
|
.collect(),
|
||||||
|
default_project: inputs.default_project.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let config_json = serde_json::to_string_pretty(&config)?;
|
let config_json = serde_json::to_string_pretty(&config)?;
|
||||||
@@ -152,5 +169,6 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitRe
|
|||||||
data_dir: data_dir.display().to_string(),
|
data_dir: data_dir.display().to_string(),
|
||||||
user,
|
user,
|
||||||
projects: validated_projects.into_iter().map(|(p, _)| p).collect(),
|
projects: validated_projects.into_iter().map(|(p, _)| p).collect(),
|
||||||
|
default_project: inputs.default_project,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ pub enum Commands {
|
|||||||
/// Comma-separated project paths (required in robot mode)
|
/// Comma-separated project paths (required in robot mode)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
projects: Option<String>,
|
projects: Option<String>,
|
||||||
|
|
||||||
|
/// Default project path (used when -p is omitted)
|
||||||
|
#[arg(long)]
|
||||||
|
default_project: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[command(hide = true)]
|
#[command(hide = true)]
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ pub struct Config {
|
|||||||
pub gitlab: GitLabConfig,
|
pub gitlab: GitLabConfig,
|
||||||
pub projects: Vec<ProjectConfig>,
|
pub projects: Vec<ProjectConfig>,
|
||||||
|
|
||||||
|
#[serde(rename = "defaultProject")]
|
||||||
|
pub default_project: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sync: SyncConfig,
|
pub sync: SyncConfig,
|
||||||
|
|
||||||
@@ -240,10 +243,32 @@ impl Config {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref dp) = config.default_project {
|
||||||
|
let matched = config.projects.iter().any(|p| {
|
||||||
|
p.path.eq_ignore_ascii_case(dp)
|
||||||
|
|| p.path
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.ends_with(&format!("/{}", dp.to_ascii_lowercase()))
|
||||||
|
});
|
||||||
|
if !matched {
|
||||||
|
return Err(LoreError::ConfigInvalid {
|
||||||
|
details: format!(
|
||||||
|
"defaultProject '{}' does not match any configured project path",
|
||||||
|
dp
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
validate_scoring(&config.scoring)?;
|
validate_scoring(&config.scoring)?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the effective project filter: CLI flag wins, then config default.
|
||||||
|
pub fn effective_project<'a>(&'a self, cli_project: Option<&'a str>) -> Option<&'a str> {
|
||||||
|
cli_project.or(self.default_project.as_deref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_scoring(scoring: &ScoringConfig) -> Result<()> {
|
fn validate_scoring(scoring: &ScoringConfig) -> Result<()> {
|
||||||
@@ -269,6 +294,8 @@ fn validate_scoring(scoring: &ScoringConfig) -> Result<()> {
|
|||||||
pub struct MinimalConfig {
|
pub struct MinimalConfig {
|
||||||
pub gitlab: MinimalGitLabConfig,
|
pub gitlab: MinimalGitLabConfig,
|
||||||
pub projects: Vec<ProjectConfig>,
|
pub projects: Vec<ProjectConfig>,
|
||||||
|
#[serde(rename = "defaultProject", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub default_project: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
#[derive(Debug, serde::Serialize)]
|
||||||
@@ -314,6 +341,31 @@ mod tests {
|
|||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_config_with_default_project(
|
||||||
|
dir: &TempDir,
|
||||||
|
default_project: Option<&str>,
|
||||||
|
) -> std::path::PathBuf {
|
||||||
|
let path = dir.path().join("config.json");
|
||||||
|
let dp_field = match default_project {
|
||||||
|
Some(dp) => format!(r#","defaultProject": "{dp}""#),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
let config = format!(
|
||||||
|
r#"{{
|
||||||
|
"gitlab": {{
|
||||||
|
"baseUrl": "https://gitlab.example.com",
|
||||||
|
"tokenEnvVar": "GITLAB_TOKEN"
|
||||||
|
}},
|
||||||
|
"projects": [
|
||||||
|
{{ "path": "group/project" }},
|
||||||
|
{{ "path": "other/repo" }}
|
||||||
|
]{dp_field}
|
||||||
|
}}"#
|
||||||
|
);
|
||||||
|
fs::write(&path, config).unwrap();
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_rejects_negative_author_weight() {
|
fn test_load_rejects_negative_author_weight() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
@@ -383,4 +435,130 @@ mod tests {
|
|||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(msg.contains("scoring.noteBonus"), "unexpected error: {msg}");
|
assert!(msg.contains("scoring.noteBonus"), "unexpected error: {msg}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_effective_project_cli_overrides_default() {
|
||||||
|
let config = Config {
|
||||||
|
gitlab: GitLabConfig {
|
||||||
|
base_url: "https://gitlab.example.com".to_string(),
|
||||||
|
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||||
|
},
|
||||||
|
projects: vec![ProjectConfig {
|
||||||
|
path: "group/project".to_string(),
|
||||||
|
}],
|
||||||
|
default_project: Some("group/project".to_string()),
|
||||||
|
sync: SyncConfig::default(),
|
||||||
|
storage: StorageConfig::default(),
|
||||||
|
embedding: EmbeddingConfig::default(),
|
||||||
|
logging: LoggingConfig::default(),
|
||||||
|
scoring: ScoringConfig::default(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
config.effective_project(Some("other/repo")),
|
||||||
|
Some("other/repo")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_effective_project_falls_back_to_default() {
|
||||||
|
let config = Config {
|
||||||
|
gitlab: GitLabConfig {
|
||||||
|
base_url: "https://gitlab.example.com".to_string(),
|
||||||
|
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||||
|
},
|
||||||
|
projects: vec![ProjectConfig {
|
||||||
|
path: "group/project".to_string(),
|
||||||
|
}],
|
||||||
|
default_project: Some("group/project".to_string()),
|
||||||
|
sync: SyncConfig::default(),
|
||||||
|
storage: StorageConfig::default(),
|
||||||
|
embedding: EmbeddingConfig::default(),
|
||||||
|
logging: LoggingConfig::default(),
|
||||||
|
scoring: ScoringConfig::default(),
|
||||||
|
};
|
||||||
|
assert_eq!(config.effective_project(None), Some("group/project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_effective_project_none_when_both_absent() {
|
||||||
|
let config = Config {
|
||||||
|
gitlab: GitLabConfig {
|
||||||
|
base_url: "https://gitlab.example.com".to_string(),
|
||||||
|
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||||
|
},
|
||||||
|
projects: vec![ProjectConfig {
|
||||||
|
path: "group/project".to_string(),
|
||||||
|
}],
|
||||||
|
default_project: None,
|
||||||
|
sync: SyncConfig::default(),
|
||||||
|
storage: StorageConfig::default(),
|
||||||
|
embedding: EmbeddingConfig::default(),
|
||||||
|
logging: LoggingConfig::default(),
|
||||||
|
scoring: ScoringConfig::default(),
|
||||||
|
};
|
||||||
|
assert_eq!(config.effective_project(None), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_with_valid_default_project() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = write_config_with_default_project(&dir, Some("group/project"));
|
||||||
|
let config = Config::load_from_path(&path).unwrap();
|
||||||
|
assert_eq!(config.default_project.as_deref(), Some("group/project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_rejects_invalid_default_project() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = write_config_with_default_project(&dir, Some("nonexistent/project"));
|
||||||
|
let err = Config::load_from_path(&path).unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(msg.contains("defaultProject"), "unexpected error: {msg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_default_project_suffix_match() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = write_config_with_default_project(&dir, Some("project"));
|
||||||
|
let config = Config::load_from_path(&path).unwrap();
|
||||||
|
assert_eq!(config.default_project.as_deref(), Some("project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_minimal_config_omits_null_default_project() {
|
||||||
|
let config = MinimalConfig {
|
||||||
|
gitlab: MinimalGitLabConfig {
|
||||||
|
base_url: "https://gitlab.example.com".to_string(),
|
||||||
|
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||||
|
},
|
||||||
|
projects: vec![ProjectConfig {
|
||||||
|
path: "group/project".to_string(),
|
||||||
|
}],
|
||||||
|
default_project: None,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
assert!(
|
||||||
|
!json.contains("defaultProject"),
|
||||||
|
"null default_project should be omitted: {json}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_minimal_config_includes_default_project_when_set() {
|
||||||
|
let config = MinimalConfig {
|
||||||
|
gitlab: MinimalGitLabConfig {
|
||||||
|
base_url: "https://gitlab.example.com".to_string(),
|
||||||
|
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||||
|
},
|
||||||
|
projects: vec![ProjectConfig {
|
||||||
|
path: "group/project".to_string(),
|
||||||
|
}],
|
||||||
|
default_project: Some("group/project".to_string()),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
assert!(
|
||||||
|
json.contains("defaultProject"),
|
||||||
|
"set default_project should be present: {json}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/main.rs
114
src/main.rs
@@ -205,6 +205,7 @@ async fn main() {
|
|||||||
gitlab_url,
|
gitlab_url,
|
||||||
token_env_var,
|
token_env_var,
|
||||||
projects,
|
projects,
|
||||||
|
default_project,
|
||||||
}) => {
|
}) => {
|
||||||
handle_init(
|
handle_init(
|
||||||
cli.config.as_deref(),
|
cli.config.as_deref(),
|
||||||
@@ -214,6 +215,7 @@ async fn main() {
|
|||||||
gitlab_url,
|
gitlab_url,
|
||||||
token_env_var,
|
token_env_var,
|
||||||
projects,
|
projects,
|
||||||
|
default_project,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -678,13 +680,14 @@ fn handle_issues(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
let project = config.effective_project(args.project.as_deref());
|
||||||
let asc = args.asc && !args.no_asc;
|
let asc = args.asc && !args.no_asc;
|
||||||
let has_due = args.has_due && !args.no_has_due;
|
let has_due = args.has_due && !args.no_has_due;
|
||||||
let open = args.open && !args.no_open;
|
let open = args.open && !args.no_open;
|
||||||
let order = if asc { "asc" } else { "desc" };
|
let order = if asc { "asc" } else { "desc" };
|
||||||
|
|
||||||
if let Some(iid) = args.iid {
|
if let Some(iid) = args.iid {
|
||||||
let result = run_show_issue(&config, iid, args.project.as_deref())?;
|
let result = run_show_issue(&config, iid, project)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_show_issue_json(&result, start.elapsed().as_millis() as u64);
|
print_show_issue_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
@@ -694,7 +697,7 @@ fn handle_issues(
|
|||||||
let state_normalized = args.state.as_deref().map(str::to_lowercase);
|
let state_normalized = args.state.as_deref().map(str::to_lowercase);
|
||||||
let filters = ListFilters {
|
let filters = ListFilters {
|
||||||
limit: args.limit,
|
limit: args.limit,
|
||||||
project: args.project.as_deref(),
|
project,
|
||||||
state: state_normalized.as_deref(),
|
state: state_normalized.as_deref(),
|
||||||
author: args.author.as_deref(),
|
author: args.author.as_deref(),
|
||||||
assignee: args.assignee.as_deref(),
|
assignee: args.assignee.as_deref(),
|
||||||
@@ -733,12 +736,13 @@ fn handle_mrs(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
let project = config.effective_project(args.project.as_deref());
|
||||||
let asc = args.asc && !args.no_asc;
|
let asc = args.asc && !args.no_asc;
|
||||||
let open = args.open && !args.no_open;
|
let open = args.open && !args.no_open;
|
||||||
let order = if asc { "asc" } else { "desc" };
|
let order = if asc { "asc" } else { "desc" };
|
||||||
|
|
||||||
if let Some(iid) = args.iid {
|
if let Some(iid) = args.iid {
|
||||||
let result = run_show_mr(&config, iid, args.project.as_deref())?;
|
let result = run_show_mr(&config, iid, project)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_show_mr_json(&result, start.elapsed().as_millis() as u64);
|
print_show_mr_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
@@ -748,7 +752,7 @@ fn handle_mrs(
|
|||||||
let state_normalized = args.state.as_deref().map(str::to_lowercase);
|
let state_normalized = args.state.as_deref().map(str::to_lowercase);
|
||||||
let filters = MrListFilters {
|
let filters = MrListFilters {
|
||||||
limit: args.limit,
|
limit: args.limit,
|
||||||
project: args.project.as_deref(),
|
project,
|
||||||
state: state_normalized.as_deref(),
|
state: state_normalized.as_deref(),
|
||||||
author: args.author.as_deref(),
|
author: args.author.as_deref(),
|
||||||
assignee: args.assignee.as_deref(),
|
assignee: args.assignee.as_deref(),
|
||||||
@@ -791,6 +795,7 @@ async fn handle_ingest(
|
|||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let dry_run = args.dry_run && !args.no_dry_run;
|
let dry_run = args.dry_run && !args.no_dry_run;
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
let project = config.effective_project(args.project.as_deref());
|
||||||
|
|
||||||
let force = args.force && !args.no_force;
|
let force = args.force && !args.no_force;
|
||||||
let full = args.full && !args.no_full;
|
let full = args.full && !args.no_full;
|
||||||
@@ -799,8 +804,7 @@ async fn handle_ingest(
|
|||||||
if dry_run {
|
if dry_run {
|
||||||
match args.entity.as_deref() {
|
match args.entity.as_deref() {
|
||||||
Some(resource_type) => {
|
Some(resource_type) => {
|
||||||
let preview =
|
let preview = run_ingest_dry_run(&config, resource_type, project, full)?;
|
||||||
run_ingest_dry_run(&config, resource_type, args.project.as_deref(), full)?;
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_dry_run_preview_json(&preview);
|
print_dry_run_preview_json(&preview);
|
||||||
} else {
|
} else {
|
||||||
@@ -808,10 +812,8 @@ async fn handle_ingest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let issues_preview =
|
let issues_preview = run_ingest_dry_run(&config, "issues", project, full)?;
|
||||||
run_ingest_dry_run(&config, "issues", args.project.as_deref(), full)?;
|
let mrs_preview = run_ingest_dry_run(&config, "mrs", project, full)?;
|
||||||
let mrs_preview =
|
|
||||||
run_ingest_dry_run(&config, "mrs", args.project.as_deref(), full)?;
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_combined_dry_run_json(&issues_preview, &mrs_preview);
|
print_combined_dry_run_json(&issues_preview, &mrs_preview);
|
||||||
} else {
|
} else {
|
||||||
@@ -854,7 +856,7 @@ async fn handle_ingest(
|
|||||||
let result = run_ingest(
|
let result = run_ingest(
|
||||||
&config,
|
&config,
|
||||||
resource_type,
|
resource_type,
|
||||||
args.project.as_deref(),
|
project,
|
||||||
force,
|
force,
|
||||||
full,
|
full,
|
||||||
false,
|
false,
|
||||||
@@ -880,28 +882,12 @@ async fn handle_ingest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let issues_result = run_ingest(
|
let issues_result = run_ingest(
|
||||||
&config,
|
&config, "issues", project, force, full, false, display, None, &signal,
|
||||||
"issues",
|
|
||||||
args.project.as_deref(),
|
|
||||||
force,
|
|
||||||
full,
|
|
||||||
false,
|
|
||||||
display,
|
|
||||||
None,
|
|
||||||
&signal,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mrs_result = run_ingest(
|
let mrs_result = run_ingest(
|
||||||
&config,
|
&config, "mrs", project, force, full, false, display, None, &signal,
|
||||||
"mrs",
|
|
||||||
args.project.as_deref(),
|
|
||||||
force,
|
|
||||||
full,
|
|
||||||
false,
|
|
||||||
display,
|
|
||||||
None,
|
|
||||||
&signal,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -1104,6 +1090,8 @@ struct InitOutputData {
|
|||||||
data_dir: String,
|
data_dir: String,
|
||||||
user: InitOutputUser,
|
user: InitOutputUser,
|
||||||
projects: Vec<InitOutputProject>,
|
projects: Vec<InitOutputProject>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
default_project: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -1136,6 +1124,7 @@ fn print_init_json(result: &InitResult) {
|
|||||||
name: p.name.clone(),
|
name: p.name.clone(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
default_project: result.default_project.clone(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
println!(
|
println!(
|
||||||
@@ -1146,6 +1135,7 @@ fn print_init_json(result: &InitResult) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn handle_init(
|
async fn handle_init(
|
||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
force: bool,
|
force: bool,
|
||||||
@@ -1154,6 +1144,7 @@ async fn handle_init(
|
|||||||
gitlab_url_flag: Option<String>,
|
gitlab_url_flag: Option<String>,
|
||||||
token_env_var_flag: Option<String>,
|
token_env_var_flag: Option<String>,
|
||||||
projects_flag: Option<String>,
|
projects_flag: Option<String>,
|
||||||
|
default_project_flag: Option<String>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
let missing: Vec<&str> = [
|
let missing: Vec<&str> = [
|
||||||
@@ -1191,6 +1182,7 @@ async fn handle_init(
|
|||||||
gitlab_url: gitlab_url_flag.unwrap(),
|
gitlab_url: gitlab_url_flag.unwrap(),
|
||||||
token_env_var: token_env_var_flag.unwrap(),
|
token_env_var: token_env_var_flag.unwrap(),
|
||||||
project_paths,
|
project_paths,
|
||||||
|
default_project: default_project_flag.clone(),
|
||||||
},
|
},
|
||||||
InitOptions {
|
InitOptions {
|
||||||
config_path: config_override.map(String::from),
|
config_path: config_override.map(String::from),
|
||||||
@@ -1285,6 +1277,29 @@ async fn handle_init(
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve default project: CLI flag, interactive prompt, or None
|
||||||
|
let default_project = if default_project_flag.is_some() {
|
||||||
|
default_project_flag
|
||||||
|
} else if project_paths.len() > 1 && !non_interactive {
|
||||||
|
let set_default = Confirm::new()
|
||||||
|
.with_prompt("Set a default project? (used when -p is omitted)")
|
||||||
|
.default(true)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
if set_default {
|
||||||
|
let selection = dialoguer::Select::new()
|
||||||
|
.with_prompt("Default project")
|
||||||
|
.items(&project_paths)
|
||||||
|
.default(0)
|
||||||
|
.interact()?;
|
||||||
|
Some(project_paths[selection].clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
println!("{}", style("\nValidating configuration...").blue());
|
println!("{}", style("\nValidating configuration...").blue());
|
||||||
|
|
||||||
let result = run_init(
|
let result = run_init(
|
||||||
@@ -1292,6 +1307,7 @@ async fn handle_init(
|
|||||||
gitlab_url,
|
gitlab_url,
|
||||||
token_env_var,
|
token_env_var,
|
||||||
project_paths,
|
project_paths,
|
||||||
|
default_project,
|
||||||
},
|
},
|
||||||
InitOptions {
|
InitOptions {
|
||||||
config_path: config_override.map(String::from),
|
config_path: config_override.map(String::from),
|
||||||
@@ -1317,6 +1333,10 @@ async fn handle_init(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref dp) = result.default_project {
|
||||||
|
println!("{}", style(format!("✓ Default project: {dp}")).green());
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
style(format!("\n✓ Config written to {}", result.config_path)).green()
|
style(format!("\n✓ Config written to {}", result.config_path)).green()
|
||||||
@@ -1680,7 +1700,9 @@ fn handle_timeline(
|
|||||||
|
|
||||||
let params = TimelineParams {
|
let params = TimelineParams {
|
||||||
query: args.query,
|
query: args.query,
|
||||||
project: args.project,
|
project: config
|
||||||
|
.effective_project(args.project.as_deref())
|
||||||
|
.map(String::from),
|
||||||
since: args.since,
|
since: args.since,
|
||||||
depth: args.depth,
|
depth: args.depth,
|
||||||
expand_mentions: args.expand_mentions,
|
expand_mentions: args.expand_mentions,
|
||||||
@@ -1722,7 +1744,9 @@ async fn handle_search(
|
|||||||
let cli_filters = SearchCliFilters {
|
let cli_filters = SearchCliFilters {
|
||||||
source_type: args.source_type,
|
source_type: args.source_type,
|
||||||
author: args.author,
|
author: args.author,
|
||||||
project: args.project,
|
project: config
|
||||||
|
.effective_project(args.project.as_deref())
|
||||||
|
.map(String::from),
|
||||||
labels: args.label,
|
labels: args.label,
|
||||||
path: args.path,
|
path: args.path,
|
||||||
since: args.since,
|
since: args.since,
|
||||||
@@ -1757,7 +1781,8 @@ async fn handle_generate_docs(
|
|||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
let result = run_generate_docs(&config, args.full, args.project.as_deref(), None)?;
|
let project = config.effective_project(args.project.as_deref());
|
||||||
|
let result = run_generate_docs(&config, args.full, project, None)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_generate_docs_json(&result, start.elapsed().as_millis() as u64);
|
print_generate_docs_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
@@ -2031,6 +2056,7 @@ struct RobotDocsData {
|
|||||||
clap_error_codes: serde_json::Value,
|
clap_error_codes: serde_json::Value,
|
||||||
error_format: String,
|
error_format: String,
|
||||||
workflows: serde_json::Value,
|
workflows: serde_json::Value,
|
||||||
|
config_notes: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -2046,12 +2072,12 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
|||||||
let commands = serde_json::json!({
|
let commands = serde_json::json!({
|
||||||
"init": {
|
"init": {
|
||||||
"description": "Initialize configuration and database",
|
"description": "Initialize configuration and database",
|
||||||
"flags": ["--force", "--non-interactive", "--gitlab-url <URL>", "--token-env-var <VAR>", "--projects <paths>"],
|
"flags": ["--force", "--non-interactive", "--gitlab-url <URL>", "--token-env-var <VAR>", "--projects <paths>", "--default-project <path>"],
|
||||||
"robot_flags": ["--gitlab-url", "--token-env-var", "--projects"],
|
"robot_flags": ["--gitlab-url", "--token-env-var", "--projects", "--default-project"],
|
||||||
"example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project",
|
"example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project,other/repo --default-project group/project",
|
||||||
"response_schema": {
|
"response_schema": {
|
||||||
"ok": "bool",
|
"ok": "bool",
|
||||||
"data": {"config_path": "string", "data_dir": "string", "user": {"username": "string", "name": "string"}, "projects": "[{path:string, name:string}]"},
|
"data": {"config_path": "string", "data_dir": "string", "user": {"username": "string", "name": "string"}, "projects": "[{path:string, name:string}]", "default_project": "string?"},
|
||||||
"meta": {"elapsed_ms": "int"}
|
"meta": {"elapsed_ms": "int"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2360,6 +2386,14 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
|||||||
"PARSE_ERROR": "General parse error"
|
"PARSE_ERROR": "General parse error"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let config_notes = serde_json::json!({
|
||||||
|
"defaultProject": {
|
||||||
|
"type": "string?",
|
||||||
|
"description": "Fallback project path used when -p/--project is omitted. Must match a configured project path (exact or suffix). CLI -p always overrides.",
|
||||||
|
"example": "group/project"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let output = RobotDocsOutput {
|
let output = RobotDocsOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: RobotDocsData {
|
data: RobotDocsData {
|
||||||
@@ -2377,6 +2411,7 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
|||||||
clap_error_codes,
|
clap_error_codes,
|
||||||
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\",\"actions\":[\"...\"]}}".to_string(),
|
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\",\"actions\":[\"...\"]}}".to_string(),
|
||||||
workflows,
|
workflows,
|
||||||
|
config_notes,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2391,11 +2426,14 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
|||||||
|
|
||||||
fn handle_who(
|
fn handle_who(
|
||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
args: WhoArgs,
|
mut args: WhoArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
if args.project.is_none() {
|
||||||
|
args.project = config.default_project.clone();
|
||||||
|
}
|
||||||
let run = run_who(&config, &args)?;
|
let run = run_who(&config, &args)?;
|
||||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
@@ -2433,6 +2471,7 @@ async fn handle_list_compat(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
let project_filter = config.effective_project(project_filter);
|
||||||
|
|
||||||
let state_normalized = state_filter.map(str::to_lowercase);
|
let state_normalized = state_filter.map(str::to_lowercase);
|
||||||
match entity {
|
match entity {
|
||||||
@@ -2511,6 +2550,7 @@ async fn handle_show_compat(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
let project_filter = config.effective_project(project_filter);
|
||||||
|
|
||||||
match entity {
|
match entity {
|
||||||
"issue" => {
|
"issue" => {
|
||||||
|
|||||||
Reference in New Issue
Block a user