Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .ai-team/agents/linus/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,18 @@
- v1.1 enables observability (skills usage, burndown metrics)
- Key implementation: DecisionService.search() and HealthCheck diagnostic command
- Roadmap session logged to .ai-team/log/2026-02-23-feature-roadmap.md

### DecisionSearchService (#69) (2026-02-24)
- Created DecisionSearchService in src/services/DecisionSearchService.ts - pure service, no file I/O, no VS Code deps
- Methods: search(), filterByDate(), filterByAuthor(), filter() (combined criteria)
- Search ranking: title match (weight 10) > author match (weight 5) > content match (weight 3)
- Multi-word queries split on whitespace; each term scored independently and summed
- filterByDate() uses string comparison on YYYY-MM-DD keys (avoids timezone issues)
- filterByAuthor() is case-insensitive substring match
- filter() chains: search first (preserves ranking order) then date range then author
- Open-ended date ranges: omit startDate or endDate in DecisionSearchCriteria
- Decisions without date excluded from date filters; decisions without author excluded from author filters
- Exported DecisionSearchCriteria and ScoredDecision interfaces for consumer use
- 37 tests in decisionSearchService.test.ts covering all methods and edge cases
- Service is decoupled from DecisionService - operates on DecisionEntry[], not files
- UI integration (tree view search box, filter controls) is Rusty's follow-up
11 changes: 11 additions & 0 deletions .ai-team/agents/rusty/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,14 @@ The SquadUI extension emerged from initial scaffolding through a rapid sequence
### Team Update: 2026-02-23 - Standup Report Enhancements & Fork-Aware Issues
**Team update (2026-02-23):** Two decisions merged: (1) Standup report issue linkification (#N in AI summaries become clickable GitHub links) with escape-then-linkify pipeline to prevent injection; (2) Velocity chart legend repositioning below canvas for better viewport + accessibility. Fork-aware issue fetching auto-detects upstream repos via GitHub API, with manual override via team.md. decided by @copilot + Rusty

### Active Status Redesign (2026-02-24, Issue #73)
- **Rich contextual status:** Replaced binary `'working' | 'idle'` with `MemberStatus` enum: `'working-on-issue' | 'reviewing-pr' | 'waiting-review' | 'working' | 'idle'`. Added `isActiveStatus()` helper and `ActivityContext` interface with `description`, `shortLabel`, `issueNumber?`, `prNumber?`.
- **OrchestrationLogService.getMemberActivity():** New method parses log entries to derive per-member activity context. Detects issue work vs PR review vs waiting status from log content. Falls back to generic `'working'` when no specific context is available. Original `getMemberStates()` preserved for backward compat.
- **SquadDataProvider:** Uses `getMemberActivity()` to populate `activityContext` on each `SquadMember`. GitHub-aware status now sets `'working-on-issue'` (not generic `'working'`).
- **Tree view:** Working members get `sync~spin` icon with green color. Description shows `role • ⚙️ Issue #42` style. Tooltip shows full context description.
- **Dashboard:** Member cards show status badge below name. "Working" summary card restored. Uses `isActiveStatus()` for counting.
- **Status bar:** Shows `Squad: 3/5 working` when members are active, falls back to `Squad: N members` when none are working.
- **Work details webview:** Shows member's activity context shortLabel in assigned-to card.
- **Key files:** `src/models/index.ts` (MemberStatus, ActivityContext, isActiveStatus), `src/services/OrchestrationLogService.ts` (getMemberActivity), `src/views/SquadTreeProvider.ts` (rich status display), `src/views/dashboard/htmlTemplate.ts` (status badges).
- **Tests:** Updated 8 test files to accept rich status values. 1093 tests passing (up from 1056).

33 changes: 33 additions & 0 deletions .ai-team/decisions/inbox/linus-decision-search-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Decision: DecisionSearchService API Design

**Date:** 2026-02-24
**Author:** Linus
**Issue:** #69

## Context

Issue #69 requires search and filtering for decisions. The existing `DecisionService` handles parsing; we need a separate service for query operations.

## Decision

Created `DecisionSearchService` as a pure, stateless service operating on `DecisionEntry[]`:

- **`search(decisions, query)`** — full-text search with relevance ranking (title 10× > author 5× > content 3×)
- **`filterByDate(decisions, startDate, endDate)`** — inclusive date range using YYYY-MM-DD string comparison
- **`filterByAuthor(decisions, author)`** — case-insensitive substring match
- **`filter(decisions, criteria)`** — chains all three: search first (preserves ranking), then date, then author

Exported types: `DecisionSearchCriteria`, `ScoredDecision`.

## Rationale

- Separation from `DecisionService` keeps parsing and querying decoupled — each can evolve independently
- Pure functions on arrays = trivially testable, no file I/O or VS Code deps
- Search-first chaining preserves relevance ordering through subsequent filters
- String comparison on YYYY-MM-DD avoids timezone issues that plague Date object comparisons

## Impact

- Rusty can consume `DecisionSearchService` from tree view code to wire up the search UI
- The `filter()` method accepts a `DecisionSearchCriteria` object — Rusty should bind UI inputs to this interface
- No changes to `DecisionEntry` model or `DecisionService` were needed
28 changes: 28 additions & 0 deletions .ai-team/decisions/inbox/rusty-rich-status-redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Rich Status Redesign

**Author:** Rusty
**Date:** 2026-02-24
**Issue:** #73 — Active Status Redesign

## Decision

Replaced binary `'working' | 'idle'` MemberStatus with rich contextual statuses:

- `'working-on-issue'` — agent is working on a GitHub issue (shows `⚙️ Issue #N`)
- `'reviewing-pr'` — agent is reviewing a pull request (shows `🔍 PR #N`)
- `'waiting-review'` — agent is waiting for a review (shows `⏳ Awaiting review`)
- `'working'` — generic active state when no specific context available (shows `⚡ Working`)
- `'idle'` — no recent activity (shows `—`)

Added `isActiveStatus()` helper — use this instead of `=== 'working'` to check if a member is active.

Added `ActivityContext` interface: `{ description, shortLabel, issueNumber?, prNumber? }` on `SquadMember` and `TeamMemberOverview`.

New `OrchestrationLogService.getMemberActivity()` method derives rich context from log entries.

## Impact

- **All code checking member status** should use `isActiveStatus(member.status)` instead of `member.status === 'working'`.
- **Tree view** now shows spinning icons for active members and contextual text in descriptions.
- **Dashboard** member cards show status badge. "Working" summary card restored.
- **Status bar** shows working/total count when members are active.
35 changes: 33 additions & 2 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,34 @@

/**
* Status of a squad member in the current session.
* - 'working': Currently executing a task
* - 'working-on-issue': Actively working on a GitHub issue
* - 'reviewing-pr': Reviewing a pull request
* - 'waiting-review': Waiting for a review from another member
* - 'working': Currently executing a task (generic)
* - 'idle': Available, no active task
*/
export type MemberStatus = 'working' | 'idle';
export type MemberStatus = 'working-on-issue' | 'reviewing-pr' | 'waiting-review' | 'working' | 'idle';

/**
* Returns true if the status represents an active (non-idle) state.
*/
export function isActiveStatus(status: MemberStatus): boolean {
return status !== 'idle';
}

/**
* Rich context about what a squad member is currently doing.
*/
export interface ActivityContext {
/** Human-readable summary of the current activity */
description: string;
/** Short label for tree view (e.g., "⚙️ Issue #42") */
shortLabel: string;
/** Related issue number, if any */
issueNumber?: number;
/** Related PR number, if any */
prNumber?: number;
}

/**
* Status of a task in the workflow.
Expand Down Expand Up @@ -57,6 +81,9 @@ export interface SquadMember {
/** Current working status */
status: MemberStatus;

/** Rich context about what the member is currently doing */
activityContext?: ActivityContext;

/** The task currently being worked on, if any */
currentTask?: Task;
}
Expand Down Expand Up @@ -374,6 +401,10 @@ export interface TeamMemberOverview {
role: string;
/** Current status */
status: MemberStatus;
/** Short status label for display (e.g., "⚙️ Issue #42") */
statusLabel?: string;
/** Rich activity context */
activityContext?: { description: string; shortLabel: string };
/** Special icon type (scribe, ralph, copilot, or undefined for regular members) */
iconType?: 'scribe' | 'ralph' | 'copilot';
/** Number of open issues assigned */
Expand Down
165 changes: 165 additions & 0 deletions src/services/DecisionSearchService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* Service for searching and filtering parsed decisions.
*
* Pure service layer — operates on DecisionEntry[] arrays, no file I/O.
* Designed to be consumed by tree providers and webview controllers.
*
* Ranking strategy: title match > body match > metadata match.
*/

import { DecisionEntry } from '../models';

/**
* Criteria bundle for combined search + filter operations.
* All fields are optional — omitted fields are not applied.
*/
export interface DecisionSearchCriteria {
/** Free-text query matched against title, content, and author */
query?: string;
/** Inclusive start date (YYYY-MM-DD) */
startDate?: string;
/** Inclusive end date (YYYY-MM-DD) */
endDate?: string;
/** Author name (case-insensitive substring match) */
author?: string;
}

/**
* A decision paired with its relevance score from a search operation.
*/
export interface ScoredDecision {
decision: DecisionEntry;
score: number;
}

// Relevance weights — title matches are worth more than body/metadata
const TITLE_WEIGHT = 10;
const CONTENT_WEIGHT = 3;
const AUTHOR_WEIGHT = 5;

export class DecisionSearchService {

/**
* Full-text search across decisions with relevance ranking.
* Matches against title, content, and author fields.
* Results are sorted by score descending (most relevant first).
*
* Returns all decisions (unfiltered) when query is empty/whitespace.
*/
search(decisions: DecisionEntry[], query: string): DecisionEntry[] {
const trimmed = query.trim();
if (!trimmed) {
return decisions;
}

const scored = this.scoreDecisions(decisions, trimmed);
return scored
.filter(s => s.score > 0)
.sort((a, b) => b.score - a.score)
.map(s => s.decision);
}

/**
* Filters decisions to those within an inclusive date range.
* Compares against the decision's `date` field (YYYY-MM-DD strings).
* Decisions without a date are excluded.
*/
filterByDate(decisions: DecisionEntry[], startDate: Date, endDate: Date): DecisionEntry[] {
const startKey = toDateKey(startDate);
const endKey = toDateKey(endDate);

return decisions.filter(d => {
if (!d.date) { return false; }
return d.date >= startKey && d.date <= endKey;
});
}

/**
* Filters decisions by author (case-insensitive substring match).
* Returns all decisions when author is empty/whitespace.
* Decisions without an author field are excluded.
*/
filterByAuthor(decisions: DecisionEntry[], author: string): DecisionEntry[] {
const trimmed = author.trim().toLowerCase();
if (!trimmed) {
return decisions;
}

return decisions.filter(d => {
if (!d.author) { return false; }
return d.author.toLowerCase().includes(trimmed);
});
}

/**
* Combined filter that chains query search, date range, and author filter.
* Applies search first (to get ranking), then narrows with date/author.
*/
filter(decisions: DecisionEntry[], criteria: DecisionSearchCriteria): DecisionEntry[] {
let results = decisions;

// Apply text search first (preserves relevance ordering)
if (criteria.query && criteria.query.trim()) {
results = this.search(results, criteria.query);
}

// Apply date range filter
if (criteria.startDate || criteria.endDate) {
const start = criteria.startDate || '0000-00-00';
const end = criteria.endDate || '9999-99-99';
results = results.filter(d => {
if (!d.date) { return false; }
return d.date >= start && d.date <= end;
});
}

// Apply author filter
if (criteria.author && criteria.author.trim()) {
results = this.filterByAuthor(results, criteria.author);
}

return results;
}

// ─── Private Helpers ────────────────────────────────────────────────

/**
* Scores each decision against the query.
* Splits the query into individual terms for multi-word matching.
*/
private scoreDecisions(decisions: DecisionEntry[], query: string): ScoredDecision[] {
const terms = query.toLowerCase().split(/\s+/).filter(t => t.length > 0);

return decisions.map(decision => {
let score = 0;
const titleLower = (decision.title || '').toLowerCase();
const contentLower = (decision.content || '').toLowerCase();
const authorLower = (decision.author || '').toLowerCase();

for (const term of terms) {
if (titleLower.includes(term)) {
score += TITLE_WEIGHT;
}
if (contentLower.includes(term)) {
score += CONTENT_WEIGHT;
}
if (authorLower.includes(term)) {
score += AUTHOR_WEIGHT;
}
}

return { decision, score };
});
}
}

/**
* Converts a Date to a YYYY-MM-DD string using local time.
* Avoids timezone-shifting issues that toISOString() causes.
*/
function toDateKey(d: Date): string {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
Loading
Loading