Skip to content

Add FTS5/BM25 search to item tool for reverse PR→item lookup - #193

Merged
getappz merged 4 commits into
masterfrom
task/14
Jul 15, 2026
Merged

Add FTS5/BM25 search to item tool for reverse PR→item lookup#193
getappz merged 4 commits into
masterfrom
task/14

Conversation

@getappz

@getappz getappz commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Auto-opened on item done for 019f5bf6-4288-7962-a34e-0b259842d97d.

Summary by CodeRabbit

  • New Features

    • Added full-text search for items across names, descriptions, and metadata.
    • Added a new item search action with query and result-limit support.
    • Search results are ranked by relevance and scoped to the active project.
    • Existing and newly added items are automatically included in the search index.
  • Bug Fixes

    • Excluded deleted items from search results.
    • Added validation for missing or blank search queries.

getappz added 3 commits July 15, 2026 12:26
- Sanitize search query via flare-search-kit::fts_query to quote tokens
  so user input like 'PR-123' isn't parsed as column:value reference
- Add flare-search-kit dependency to agentflare-backend
- Use bm25() with name-weighted (3.0) scoring for better relevance ordering
- Fix search function using raw query instead of sanitized safe variant
- Make ranking test tolerant of BM25 ties
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The backend adds an FTS5 index and project-scoped ranked item search. The MCP item tool gains a search action with query validation, dispatch, result serialization, and updated request documentation.

Changes

Item search

Layer / File(s) Summary
FTS index and backend search
crates/agentflare-backend/Cargo.toml, crates/agentflare-backend/src/db.rs, crates/agentflare-backend/src/migrations/0005_items_fts.sql, crates/agentflare-backend/src/item.rs
The backend registers and maintains an FTS5 index, sanitizes and ranks project-scoped queries, excludes deleted items, clamps limits, and tests search behavior.
MCP item search wiring
src/mcp_server.rs, src/mcp_server/item.rs
The MCP schema and router support search; the handler validates queries, calls backend search, maps errors, and returns JSON.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant ItemRouter
  participant ItemSearchHandler
  participant BackendItemSearch
  participant SQLiteFTS5
  MCPClient->>ItemRouter: search action with query
  ItemRouter->>ItemSearchHandler: dispatch request
  ItemSearchHandler->>BackendItemSearch: project id, query, limit
  BackendItemSearch->>SQLiteFTS5: sanitized MATCH query
  SQLiteFTS5-->>BackendItemSearch: ranked matching items
  BackendItemSearch-->>ItemSearchHandler: project-scoped items
  ItemSearchHandler-->>MCPClient: JSON results
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is missing the required Summary, Test plan, and Notes for reviewers sections from the template. Add the template sections with a concise summary, the commands/run results for testing, and notes on risk areas and backwards compatibility.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding FTS5/BM25 search to the item tool for reverse PR-to-item lookup.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/14

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/compact.rs (1)

51-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider wrapping per-line inserts in an explicit transaction.

Each stmt.execute(...) implicitly commits individually; for large transcripts, wrapping the loop in an explicit transaction (conn.transaction()/BEGIN/COMMIT) reduces overhead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compact.rs` around lines 51 - 59, Wrap the per-line insert loop in the
compact transcript insertion flow with an explicit transaction using the
existing conn handle, execute the prepared statement through the transaction,
and commit once after all lines are inserted. Preserve the current insert SQL,
parameters, and error handling while replacing per-line commits with a single
transaction commit.
src/memory/mcp.rs (1)

310-322: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

O(n²) score lookup via find() inside map().

For each of entries.len() lines, scored.iter().find(...) scans up to scored.len(). A HashMap<usize, f64> built once from scored would make this O(n).

♻️ Proposed refactor
+    let score_by_index: std::collections::HashMap<usize, f64> =
+        scored.iter().map(|s| (s.index, s.score)).collect();
     let output: Vec<serde_json::Value> = entries
         .iter()
         .enumerate()
         .map(|(i, entry)| {
-            let score = scored.iter().find(|s| s.index == i).map(|s| s.score);
+            let score = score_by_index.get(&i).copied();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/memory/mcp.rs` around lines 310 - 322, Replace the per-entry
scored.iter().find lookup in the output construction with a HashMap<usize, f64>
built once from scored before the entries iteration. Use the map to retrieve
each score by index while preserving the existing JSON fields and None behavior
for missing scores.
src/mcp_server.rs (1)

601-603: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

limit doc doesn't mention its new search usage.

This field's description is scoped to (list), but item_search now also consumes it (with a different default/behavior than list's "omit for no limit"). See consolidated comment for the paired validation gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp_server.rs` around lines 601 - 603, Update the schema description for
the limit field in the relevant request type to document both list and
item_search usage, including that search applies its own default or behavior
when the limit is omitted. Keep the existing serde default and field type
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/compact.rs`:
- Around line 34-83: Change score_lines to return a Result<Vec<ScoredLine>,
rusqlite::Error> and replace its SQLite-related expect calls, including
connection, table creation, statement preparation, inserts, and query execution,
with error propagation. Update handle_compact and any callers to propagate the
resulting error while preserving the existing empty and invalid-query early
returns.

In `@src/memory/mcp.rs`:
- Around line 273-275: Update the empty-input early return in the memory
response handler to include the same “query” field as the full response, while
preserving the existing empty lines, kept, and total values so both response
paths have a consistent shape.
- Around line 294-308: Update the deduplication of by_relevance in the
keep-quota filling logic so it preserves the original scored order from
score_lines while removing duplicate indices; do not sort the indices
numerically. Keep the existing filled-count and keep[idx] selection behavior
unchanged so highest-relevance entries are retained first.
- Around line 248-255: Update CompactInput and the handle_compact flow so the
scorer option has a real effect: select the requested scoring backend, including
the advertised fts5 and keyword choices, instead of always calling
crate::compact::score_lines. If backend selection is not supported, remove
scorer from the input and tool schema rather than leaving an ineffective field.

---

Nitpick comments:
In `@src/compact.rs`:
- Around line 51-59: Wrap the per-line insert loop in the compact transcript
insertion flow with an explicit transaction using the existing conn handle,
execute the prepared statement through the transaction, and commit once after
all lines are inserted. Preserve the current insert SQL, parameters, and error
handling while replacing per-line commits with a single transaction commit.

In `@src/mcp_server.rs`:
- Around line 601-603: Update the schema description for the limit field in the
relevant request type to document both list and item_search usage, including
that search applies its own default or behavior when the limit is omitted. Keep
the existing serde default and field type unchanged.

In `@src/memory/mcp.rs`:
- Around line 310-322: Replace the per-entry scored.iter().find lookup in the
output construction with a HashMap<usize, f64> built once from scored before the
entries iteration. Use the map to retrieve each score by index while preserving
the existing JSON fields and None behavior for missing scores.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 61fc90f0-8ff3-4495-bb3e-536fa31b1b45

📥 Commits

Reviewing files that changed from the base of the PR and between f65c582 and c5828e8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • crates/agentflare-backend/Cargo.toml
  • crates/agentflare-backend/src/db.rs
  • crates/agentflare-backend/src/item.rs
  • crates/agentflare-backend/src/migrations/0005_items_fts.sql
  • src/compact.rs
  • src/main.rs
  • src/mcp_server.rs
  • src/mcp_server/item.rs
  • src/memory/mcp.rs

Comment thread src/compact.rs
Comment on lines +34 to +83
pub fn score_lines(lines: &[LineEntry], query: &str) -> Vec<ScoredLine> {
if lines.is_empty() {
return vec![];
}

// OR mode for broader recall — compaction keeps all potentially relevant lines.
let Some(safe_query) = fts_query(query, MatchMode::Any) else {
return vec![];
};

let conn = Connection::open_in_memory().expect("in-memory SQLite connection");

conn.execute_batch(
"CREATE VIRTUAL TABLE transcript_fts USING fts5(\"ix\" UNINDEXED, text);",
)
.expect("create FTS5 table");

{
let mut stmt = conn
.prepare("INSERT INTO transcript_fts(\"ix\", text) VALUES(?1, ?2);")
.expect("prepare insert");
for line in lines {
stmt.execute(rusqlite::params![line.index as i64, &line.text])
.expect("insert line");
}
}

let weights = Bm25Weights::new(vec![]);
let sql = format!(
"SELECT \"ix\", text, bm25(transcript_fts{}) AS score \
FROM transcript_fts \
WHERE transcript_fts MATCH ?1 \
ORDER BY score",
weights.sql_args()
);

let mut stmt = conn.prepare(&sql).expect("prepare query");

let results = stmt
.query_map(rusqlite::params![safe_query], |row| {
Ok(ScoredLine {
index: row.get::<_, i64>("ix")? as usize,
text: row.get("text")?,
score: row.get("score")?,
})
})
.expect("query lines");

results.filter_map(|r| r.ok()).collect()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== compact.rs ==\n'
wc -l src/compact.rs
sed -n '1,140p' src/compact.rs

printf '\n== mcp caller ==\n'
rg -n "handle_compact|score_lines\\(" src -S

printf '\n== Cargo manifests and sqlite features ==\n'
rg -n "rusqlite|bundled|fts5|sqlite" Cargo.toml Cargo.lock . -S --glob '!target/**'

Repository: getappz/agentflare

Length of output: 45796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== handle_compact ==\n'
sed -n '240,310p' src/memory/mcp.rs

printf '\n== compact tests tail ==\n'
sed -n '140,220p' src/compact.rs

printf '\n== rusqlite/libsqlite3-sys feature clues ==\n'
rg -n "libsqlite3-sys|fts5|bundled" Cargo.lock Cargo.toml crates/*/Cargo.toml -S

Repository: getappz/agentflare

Length of output: 4641


🌐 Web query:

rusqlite bundled feature FTS5 enabled SQLite bundled libsqlite3-sys

💡 Result:

When using the rusqlite crate with the bundled feature, the bundled SQLite library is compiled from the latest source, which includes support for FTS5 [1][2]. FTS5 is a standard part of the modern SQLite amalgamation, and the bundled feature essentially links against this pre-compiled amalgamation [1][3]. You do not need to perform additional steps to "enable" FTS5 if you are using the bundled feature; it is included by default as part of the bundled SQLite source [4][1]. If you encounter issues where FTS5 is not recognized, ensure that: 1. You are using the bundled feature in your Cargo.toml: [dependencies] rusqlite = { version = "0.39.0", features = ["bundled"] } [1][2] 2. If you are linking against a system SQLite (not using bundled), you must ensure the system's version of SQLite was compiled with FTS5 support enabled [5]. If you require custom compilation flags for the bundled SQLite (e.g., to enable specific extensions or modify the build), you can pass configuration flags via the LIBSQLITE3_FLAGS environment variable during the build process, as libsqlite3-sys (which handles the compilation) respects this variable [4].

Citations:


Propagate SQLite errors from score_lines src/compact.rs:34-83
handle_compact already returns Result, so the .expect(...) calls here should bubble rusqlite::Error instead of panicking on the compact request path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compact.rs` around lines 34 - 83, Change score_lines to return a
Result<Vec<ScoredLine>, rusqlite::Error> and replace its SQLite-related expect
calls, including connection, table creation, statement preparation, inserts, and
query execution, with error propagation. Update handle_compact and any callers
to propagate the resulting error while preserving the existing empty and
invalid-query early returns.

Comment thread src/memory/mcp.rs
Comment thread src/memory/mcp.rs
Comment on lines +273 to +275
if entries.is_empty() {
return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0}).to_string());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Empty-lines response omits query, unlike the full response.

The full response (lines 324-330) includes "query", but this early return does not. Clients relying on a consistent response shape may break on the empty-input path.

💡 Proposed fix
     if entries.is_empty() {
-        return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0}).to_string());
+        return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0, "query": query}).to_string());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if entries.is_empty() {
return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0}).to_string());
}
if entries.is_empty() {
return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0, "query": query}).to_string());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/memory/mcp.rs` around lines 273 - 275, Update the empty-input early
return in the memory response handler to include the same “query” field as the
full response, while preserving the existing empty lines, kept, and total values
so both response paths have a consistent shape.

Comment thread src/memory/mcp.rs Outdated
- Backfill items_fts for items that already existed before this
  migration ran -- the AFTER INSERT/UPDATE/DELETE triggers only keep
  the index in sync going forward, so without a backfill every item
  created before this migration would be permanently invisible to
  item(search) until it happened to be touched again.
- Clamp item::search's limit via flare_search_kit::clamped_limit instead
  of a raw `limit as i64` cast -- usize::MAX (or any caller-supplied
  value that wraps negative on cast) turns into SQL "LIMIT -1", which
  SQLite treats as unlimited, defeating the cap. Same class of bug this
  codebase already guards against in gateway-registry/skill-registry's
  own search() via the same helper.
- Fix the limit field's schema description, which said "omit for no
  limit" -- true for item(list), but item(search) defaults to 20
  (clamped to 1000) when omitted, not unlimited.
- Resolve merge conflicts against master's already-fixed compact.rs/
  memory/mcp.rs (this branch predates #192/#195 and carried duplicate,
  pre-fix copies of that code) by taking master's versions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/mcp_server.rs (1)

2187-2200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dispatch, error message, and tool description are consistent for search.

The "search" => self.item_search(req) arm, the updated "unknown item action" error list, and the #[tool(...)] description all list search consistently with the action field doc at line 554.

One gap: I don't see an item_search-specific unit test in this file's test module (existing item action tests cover create/list/update_state/cancel/claim/done, etc., but not search). Tests for it may live in src/mcp_server/item.rs (not in this review's file set) — can you confirm coverage exists there for the empty-query rejection and a successful ranked-results case, similar to artifact_search_matches_name_description_and_content?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp_server.rs` around lines 2187 - 2200, Add or confirm focused tests for
item_search covering rejection of an empty query and successful ranked results
matching name, description, and content, following the pattern of
artifact_search_matches_name_description_and_content. Place the coverage in the
existing item_search test module or the relevant test location without changing
the dispatch or tool description.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/mcp_server.rs`:
- Around line 2187-2200: Add or confirm focused tests for item_search covering
rejection of an empty query and successful ranked results matching name,
description, and content, following the pattern of
artifact_search_matches_name_description_and_content. Place the coverage in the
existing item_search test module or the relevant test location without changing
the dispatch or tool description.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 37bdb7fd-fecc-43a0-b69f-797ea028ddf3

📥 Commits

Reviewing files that changed from the base of the PR and between c5828e8 and 2b0824c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • crates/agentflare-backend/src/item.rs
  • crates/agentflare-backend/src/migrations/0005_items_fts.sql
  • src/mcp_server.rs
  • src/mcp_server/item.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/agentflare-backend/src/item.rs
  • crates/agentflare-backend/src/migrations/0005_items_fts.sql
  • src/mcp_server/item.rs

@getappz
getappz merged commit f584c06 into master Jul 15, 2026
15 checks passed
@getappz
getappz deleted the task/14 branch July 15, 2026 10:38
getappz added a commit that referenced this pull request Jul 15, 2026
…uiring /handoff inbox (#194)

* feat: surface pending item queue at session start

SessionStart hook now queries backend DB for items assigned to the
current agent that are still open (excludes completed/cancelled via
state group check). Pending items listed as:

  Pending items assigned to you (claude-code, 3 open):
    #42 Review PR #193
    #47 MCP prompt argument completion

Adds agentflare-backend::item::list_by_assignee_agent(conn, project_id, agent)
with a JOIN on states to filter out completed/cancelled state groups.
Graceful no-op when backend.db doesn't exist yet (first run).

* Fix pending-item project lookup + address CodeRabbit findings

- SessionStart's pending-item lookup used
  "SELECT id FROM projects WHERE deleted_at IS NULL ORDER BY created_at
  LIMIT 1" -- backend.db is a single shared database across every repo
  agentflare has ever touched on this machine, so with more than one
  project this picked an arbitrary (oldest) one instead of the current
  repo's, surfacing the wrong agent's pending items. Now reuses
  AgentflareMcp::resolve_project (made pub(crate)), the same repo-linked
  resolution every other item/artifact/comment call already goes
  through.
- Cap the displayed pending-item list at 10 with a "+N more" note so an
  agent with a large open queue doesn't blow up the session-start
  message.
- Fix clippy (collapsible_if via let-chains) and fmt.
- Update the regression test to route project setup through the same
  resolve_project() call session_start_message uses internally, so it
  proves the actual fix instead of a lookup path that no longer matches
  production.
getappz pushed a commit that referenced this pull request Aug 27, 2026
…ntic warnings in an unrelated crate. Both crates compile, clippy is clean on the changed files, and all tests pass.

Implemented the dependency-cascade auto-dispatch mechanism per the spec:

1. **`crates/agentflare-backend/src/item/relations.rs`**: added `dependents_of(conn, item_id)` (reverse of `list_dependencies`) and `all_dependencies_completed(conn, item_id)` (true only when the item has ≥1 dependency and every one is in the `completed` state group; a soft-deleted dependency target counts as unsatisfied, fail-closed).
2. **`src/supervisor.rs`**: added `cascade_unblock_dependents(conn, item_id)` — for each dependent of `item_id` whose dependencies are now all completed, applies `READY_LABEL` via the existing `item::add_label` (idempotent `INSERT OR IGNORE`). Dependents with no `assignee_agent` are skipped with a loud `eprintln!` rather than silently no-op'd, per option (a) in the spec.
3. **`src/mcp_server/item.rs`**: `item_check_merge` now calls `cascade_unblock_dependents` right after `promoted` becomes true — this is the single hook point that catches both the automatic tick (`promote_merged_item`) and manual/reconciliation calls.

**Tests** (8 new, all passing):
- Backend: `dependents_of_finds_reverse_edges`, `all_dependencies_completed_requires_every_dependency_done`, `all_dependencies_completed_false_for_item_with_no_dependencies`.
- Supervisor: labels a dependent once its only dependency completes; leaves a dependent alone with a still-open sibling dependency; skips (doesn't label) an unassigned dependent; idempotent across repeated calls.

Ran `cargo test -p agentflare-backend` and `cargo test --bin agentflare -- supervisor:: item_check_merge` — all pass, no regressions in the 37 pre-existing supervisor tests. Clippy clean on both touched crates.

**Concerns / judgment calls:**
- Went with option (a) (require pre-existing `assignee_agent`, loud log) as recommended — unassigned dependents like #193/#194 won't auto-dispatch from this alone.
- Followed the spec literally on "completed" only (not "completed or cancelled") for the all-deps-done check, even though `groom`'s existing `blocked_by_map` treats cancelled dependencies as non-blocking too. This means a dependent stuck behind a *cancelled* dependency won't be auto-unblocked by this cascade — flagging this divergence in case it's not what's wanted.
- Didn't add a full `item_check_merge` end-to-end integration test (would require mocking a merged PR via git/worktree state) — coverage is at the `cascade_unblock_dependents` unit level instead, which matches the spec's test list.

Agentflare-Agent: claude-code_2-1-245_agent
Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking
Agentflare-Item: 195-auto-dispatch-dependents-when-a-blocking
getappz added a commit that referenced this pull request Aug 27, 2026
…cy-graph-driven dynamic workflows) (#621)

* Clean — no warnings from `agentflare-backend`, only pre-existing pedantic warnings in an unrelated crate. Both crates compile, clippy is clean on the changed files, and all tests pass.

Implemented the dependency-cascade auto-dispatch mechanism per the spec:

1. **`crates/agentflare-backend/src/item/relations.rs`**: added `dependents_of(conn, item_id)` (reverse of `list_dependencies`) and `all_dependencies_completed(conn, item_id)` (true only when the item has ≥1 dependency and every one is in the `completed` state group; a soft-deleted dependency target counts as unsatisfied, fail-closed).
2. **`src/supervisor.rs`**: added `cascade_unblock_dependents(conn, item_id)` — for each dependent of `item_id` whose dependencies are now all completed, applies `READY_LABEL` via the existing `item::add_label` (idempotent `INSERT OR IGNORE`). Dependents with no `assignee_agent` are skipped with a loud `eprintln!` rather than silently no-op'd, per option (a) in the spec.
3. **`src/mcp_server/item.rs`**: `item_check_merge` now calls `cascade_unblock_dependents` right after `promoted` becomes true — this is the single hook point that catches both the automatic tick (`promote_merged_item`) and manual/reconciliation calls.

**Tests** (8 new, all passing):
- Backend: `dependents_of_finds_reverse_edges`, `all_dependencies_completed_requires_every_dependency_done`, `all_dependencies_completed_false_for_item_with_no_dependencies`.
- Supervisor: labels a dependent once its only dependency completes; leaves a dependent alone with a still-open sibling dependency; skips (doesn't label) an unassigned dependent; idempotent across repeated calls.

Ran `cargo test -p agentflare-backend` and `cargo test --bin agentflare -- supervisor:: item_check_merge` — all pass, no regressions in the 37 pre-existing supervisor tests. Clippy clean on both touched crates.

**Concerns / judgment calls:**
- Went with option (a) (require pre-existing `assignee_agent`, loud log) as recommended — unassigned dependents like #193/#194 won't auto-dispatch from this alone.
- Followed the spec literally on "completed" only (not "completed or cancelled") for the all-deps-done check, even though `groom`'s existing `blocked_by_map` treats cancelled dependencies as non-blocking too. This means a dependent stuck behind a *cancelled* dependency won't be auto-unblocked by this cascade — flagging this divergence in case it's not what's wanted.
- Didn't add a full `item_check_merge` end-to-end integration test (would require mocking a merged PR via git/worktree state) — coverage is at the `cascade_unblock_dependents` unit level instead, which matches the spec's test list.

Agentflare-Agent: claude-code_2-1-245_agent
Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking
Agentflare-Item: 195-auto-dispatch-dependents-when-a-blocking

* cascade_unblock_dependents now inherits completed item's assignee (bare agent id) for unassigned dependents; added regression test; pre-existing unrelated test failure confirmed via baseline stash and flagged, not touched.

Agentflare-Agent: claude-code_2-1-245_agent
Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking
Agentflare-Item: 195-auto-dispatch-dependents-when-a-blocking

* style: cargo fmt cascade_unblock_dependents, allowlist supervisor_tests.rs LOC gate

Agentflare-Agent: claude-code
Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking
Agentflare-Item: 195
Agentflare-Session: 3eedea22-3a44-4342-963d-d211427f1fea

---------

Co-authored-by: shiva <shiva@gosysinfo.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant