# Changelog

All notable changes to `@sentinel-ai-solutions/agent-kit` are documented here.

## [0.35.0] — 2026-05-29

### Added — Scout sub-agent — proactive AI ecosystem scanner

**Sentinel AI's eyes on the ecosystem so Brad never has to manually discover the next Firecrawl /monitor.** Scout is the first module under the new `src/sub-agents/` namespace. It continuously scans the AI tool ecosystem (GitHub trending, Hacker News, vendor announcements, MCP and skills registries, AI newsletters, conferences, Firecrawl changelog) and surfaces high-relevance new tools, skills, libraries, MCP servers, and services that would augment other Sentinel AI sub-agents' capabilities.

#### `src/sub-agents/scout/` — new module

- **`Scout`** class with three operations:
  - `dailyScan()` — polls every enabled scanner since the last run, scores everything, runs the recommender, pushes to the inbox, returns digest + top picks.
  - `weeklyDigest()` — aggregates the last 7 days into a single summary.
  - `evaluateTool(url)` — one-off evaluation for an operator-supplied URL.
- **`DEFAULT_SUB_AGENT_REGISTRY`** — Scout's ground truth of every Sentinel AI sub-agent shipped to date (Olive, Vera, Cora, Nyx, Ledger, Tally, Mira, LearningMind, MCP layer, Voice, Photo, Personality) with capability tags and known gaps the relevance scorer matches against.

#### `src/sub-agents/scout/scanners/` — 11 scanners

| Scanner | Status | Notes |
|---|---|---|
| `github-trending.ts` | LIVE | Public search API; auth optional |
| `hackernews-ai.ts` | LIVE | Algolia API; no auth |
| `anthropic-announcements.ts` | LIVE | News HTML + SDK release GH feeds + cookbook commits |
| `openai-announcements.ts` | LIVE | SDK release GH feeds (python / node / agents SDK) |
| `google-ai-announcements.ts` | LIVE | Gemini SDK release GH feeds |
| `mcp-registry.ts` | LIVE | Official MCP servers README extractor |
| `firecrawl-changelog.ts` | LIVE | Meta: GH releases + docs changelog |
| `ai-newsletter-feeds.ts` | LIVE | RSS for TLDR AI / Latent Space / Ben's Bites / Rundown AI |
| `conference-announcements.ts` | LIVE | AI Engineer Summit (default); AAAI/NeurIPS/EMNLP TODO |
| `skills-registry.ts` | GATED | Requires `SCOUT_GITHUB_TOKEN` for GitHub code search |
| `producthunt-ai.ts` | DEFERRED | Requires GraphQL POST + OAuth token; clear TODO for `ScoutHttpClient.post()` extension |

#### `src/sub-agents/scout/scorers/` — three deterministic scorers

- **`RelevanceScorer`** (0-100) — additive heuristic: +35 for sub-agent capability tag match (augmentation), +20 for known-gap match (pain-point), +15 for MCP/skill/SDK kind, +15 for agent terminology, +10 newness, +5 stability (stars/MRR/reputable vendor); minus 25 for horizontal-AI noise, minus 20 for closed-stack dependency.
- **`ImpactScorer`** (0-100) — composite of cost_reduction + capability_unlock + customer_facing + brand keyword matches, equally weighted.
- **`IntegrationEffortScorer`** — 1-day / 1-week / 1-month bins.

#### `src/sub-agents/scout/recommenders/integration-recommender.ts`

Produces an `adopt` / `prototype` / `evaluate` / `monitor` / `skip` action with a 2-4 sentence briefing, 3 benefits, a 1-line risk, and a tightly worded next action. Format mirrors Brad's preferred briefing shape: *"Tool X does Y. Currently sub-agent Z does this via [approach]. Switching would: [benefits]. Effort: [est]. Risk: [risk]. Recommendation: [action]."*

#### `src/sub-agents/scout/surfacer/`

- **`DigestBuilder`** — renders top-N Scout picks as a Markdown block ready to drop into the Telegram morning brief.
- **`InboxPusher`** — emits `intel`-shaped events with the `scout` sub-agent badge into the F0.10 Proactive Surfacing queue. Skips findings whose recommendation is `skip`. Default behaviour: in-memory queue (test-friendly); override with `pushCandidate` callback in production to wire into the live pipeline.

#### Configuration

- `SCOUT_GITHUB_TOKEN` (optional) — raises GitHub rate limits on github-trending, skills-registry, anthropic SDK feeds, openai/google SDK feeds, firecrawl changelog.
- `SCOUT_PRODUCTHUNT_TOKEN` (optional, deferred) — enables ProductHunt scanner once `ScoutHttpClient.post()` is implemented.
- `ScoutSourcesConfig` — enable/disable individual scanners.
- `ScoutFocusOverride` — per-operator focus hooks (verticals, prioritised sub-agents, additional tags).

#### Tests

- `tests/sub-agents/scout/scanners.test.ts` — per-scanner unit tests with mocked HTTP responses.
- `tests/sub-agents/scout/scorers.test.ts` — scorer + recommender tests with synthetic findings → expected scores.
- `tests/sub-agents/scout/end-to-end.test.ts` — full pipeline: fake scanner → score → recommend → SurfacedInsight push, plus sources-config disable behaviour and empty-state digest.

#### Package surface

- New export path `@sentinel-ai-solutions/agent-kit/sub-agents/scout` and `Scout` namespace re-export from the root entry.

---

## [0.34.0] — 2026-05-29

### Added — Relationship Cadence: F4 Phase C — outreach drafters + memory hooks + Nyx draft pipeline

**The relationship sub-agent learns to actually write the message and remember the contact.** Extends Phase B (cadence detection) with channel-specific drafters, per-contact memory (interaction history, personal facts, commitments), and a Nyx surface that pulls all of it into voice-matched drafts gated through F0.6 Approval.

#### `src/relationship-cadence/drafters/`

- **`EmailDrafter`** — subject + body, ~150 words, email register.
- **`SlackDrafter`** — body-only, under 500 chars, casual register.
- **`TextDrafter`** — body-only SMS, single-segment (~160 chars), one thought.
- **`LinkedInDrafter`** — body-only, under 300 chars, professional register.

All four implement a shared `OutreachDrafter` interface and accept `intent` (`reconnect | follow_up | check_in | congratulate | condolence | thank | invite | ask`) plus a `context` containing past interactions, personal facts, and outstanding commitments. Every result has `requiresApproval: true`. Drafters NEVER touch send APIs — only `messages.create` on the injected Anthropic client.

Voice match is enforced by piping `PersonalityDefinition` through `buildSystemPrompt` with per-channel register mapping. Tests assert that two different personalities produce two different system prompts (no template fall-through).

#### `src/relationship-cadence/memory/`

- **`InMemoryInteractionHistory`** / **`InteractionHistoryStore`** — captures `InteractionTouchpoint` records (what was discussed, commitments made, free-text notes). Distinct from `ContactPoint` (raw cadence signal).
- **`InMemoryPersonalFacts`** / **`PersonalFactsStore`** — kids, hobbies, milestones. Operator-editable. Every `addFact` / `updateFact` / `deleteFact` writes a `PersonalFactAuditEntry` with previous/next snapshots and an actor + optional reason. `listAuditEntries` supports filtering by relationship or fact id for rollback workflows.
- **`InMemoryCommitmentTracker`** / **`CommitmentTrackerStore`** — operator-to-contact and contact-to-operator obligations with `dueAt` and `status`. `listOverdue(operatorId, now)` surfaces open commitments past due.

All three follow the same `Interface + InMemory adapter` split as Phase A's `RelationshipTracker`. Production consumers implement the interface against their own Prisma schema.

#### `src/relationship-cadence/sub-agents/nyx.ts` — Phase C extensions

- **`Nyx.surfaceRecommendations(operatorId, { limit?, phase?, now? })`** — runs `scan`, returns the top-N ranked `OutreachRecommendation[]`. Drives "top 5 things to do today" dashboards.
- **`Nyx.draftOutreach(operatorId, relId, personality, { channel?, intent?, intentNote? }, now?)`** — now picks the channel-specific Phase C drafter if registered (auto-instantiates defaults using the constructor's `anthropic`), threads `InteractionHistoryStore` + `PersonalFactsStore` + `CommitmentTrackerStore` context into the prompt, and still forwards to the optional `approvalGate`. Legacy positional `channel` argument continues to work.
- **`Nyx.recordInteraction(operatorId, { touchpoint, alsoTrackContact? })`** — appends to the configured `InteractionHistoryStore`; optionally also appends a `ContactPoint` to the cadence tracker so detectors see the signal.
- **`Nyx.queryPersonalFacts(operatorId, relationshipId)`** — returns stored facts for downstream context (drafting, surfacing, dashboards). Empty array when no store is configured.

New `NyxConfig` fields (all optional): `drafters`, `interactionHistory`, `personalFacts`, `commitments`.

#### Channel union

- `DraftChannel` widened to `"email" | "linkedin" | "call_script" | "slack" | "sms"`. Additive — existing consumers compile unchanged.

#### Approval gating — strengthened

Nothing in Phase C calls a send API. Drafters only invoke `messages.create`. `Nyx.draftOutreach` only calls the drafter + optional `approvalGate.submit` (which is a queue hook for F0.6 `createProposal`, NOT a send). The "never auto-sends" guarantee is asserted end-to-end in `tests/relationship-cadence-nyx-phase-c.test.ts`.

#### Tests

- `tests/relationship-cadence-email-drafter.test.ts`
- `tests/relationship-cadence-slack-drafter.test.ts`
- `tests/relationship-cadence-text-drafter.test.ts`
- `tests/relationship-cadence-linkedin-drafter.test.ts`
- `tests/relationship-cadence-drafter-voice-match.test.ts` — personality propagation + per-personality prompt differentiation
- `tests/relationship-cadence-interaction-history.test.ts`
- `tests/relationship-cadence-personal-facts.test.ts` — including audit trail + rollback semantics
- `tests/relationship-cadence-commitment-tracker.test.ts`
- `tests/relationship-cadence-nyx-phase-c.test.ts` — full Nyx Phase C surface, including the no-auto-send guarantee

All 116 relationship-cadence tests pass (75 Phase A/B + 41 Phase C).

#### Documentation

- `src/relationship-cadence/README.md` — new "Phase C" section.

---
### Added — Pattern Analysis: F7 Phase D — Market Intelligence detectors + per-customer relevance

**Wires F5 regulator publication scanners into F7 as composable Signal sources, splits the five market intelligence detectors into pure-function modules, and adds a per-customer relevance scorer that grades each customer for each market signal.** Strategic-value scoring in `InsightRanker` now responds to exposure ratio (precision boost vs widely-known decay), lead-time decay, and per-pattern relevance.

#### `src/pattern-analysis/sources/`

- **`MarketIntelligenceSource`** — `DataSourceAdapter` for one `MarketIntelligenceFeed`. Wraps F5's `ScannerOrchestrator` (or any feed producing `RegulatorPublication[]`) and converts publications into Signals, preserving Phase B metadata (citationId, classification, industryVerticals, summary, hash). Supports `restrictToVerticals` filtering.
- **`publicationToSignal()`** — standalone helper for adapters that already have a publication stream (webhook push, etc.).
- **`orchestratorAsFeed(orch)` / `scannerAsFeed(scanner)`** — bridge utilities so the F5 `ScannerOrchestrator` (and individual `Scanner`s) drop into F7 with no boilerplate.

#### `src/pattern-analysis/detectors/market/`

Five standalone pure-function detectors mirroring the `MarketIntelligenceScanner` algorithms, but composable, deterministic, and unit-testable without storage mocks. Each implements `MarketDetector<TConfig>` taking `(Signal[], MarketDetectorContext, config?)` and returning `Pattern[]`.

| Detector | Pattern type | Source(s) | Grouping |
|---|---|---|---|
| `first-mover-opportunity` | `first_mover_opportunity` | regulator_publication, government_policy, macro_signal | per regulator or source bucket |
| `pre-emptive-positioning` | `pre_emptive_positioning` | any external source | one pattern across hot topics (3+ source convergence) |
| `competitive-intelligence` | `competitive_intelligence` | competitor_activity | per competitor (id / name) |
| `legislative-cascade` | `legislative_change_cascade` | government_policy | one aggregated pattern |
| `cross-regulator-cascade` | `cross_regulator_cascade` | regulator_publication | per matched `CrossReferenceRule` |

`runAllMarketDetectors()` composes all five in a fixed deterministic order. Detectors use `aggregateRelevantCustomers()` internally — they delegate to `CustomerRelevanceScorer` rather than doing their own keyword matching, so improvements to relevance scoring propagate to every detector.

#### `src/pattern-analysis/customer-relevance-scorer.ts`

Multi-dimensional 0.0–1.0 relevance score per `(signal, customer)` with explainable `reasons` array. Used by the new detectors and exposed for hosts that want to score independently of pattern detection.

Default weighted dimensions:

| Reason | Weight |
|---|---|
| `regulator_match` | +0.40 |
| `legislation_match` | +0.30 |
| `industry_vertical_match` | +0.25 |
| `industry_match` | +0.20 |
| `tag_overlap` | +0.15 |
| `engagement_type_boost` | +0.10 (only when stacked on a topical match) |
| `memory_boost` | +0.20 (from optional hook) |

The `memoryRelevance` hook lets the host plug F0.4 / memory store / CRM-derived attribution: return `true` for the customer if they have shown recent engagement on the signal's topic.

#### InsightRanker — Phase D strategic-value hooks

For market patterns, `InsightRanker` now applies a multiplier on top of the category strategic-value base. Configurable via `InsightRankerOptions`:

- **Exclusivity bonus** (`marketExclusivityBoost`, default ×1.20) when exposure ratio `< marketExclusivityThreshold` (0.20)
- **Widely-known decay** (`marketWidelyKnownDecay`, default ×0.75) when exposure ratio `> marketWidelyKnownThreshold` (0.80)
- **Lead-time expired decay** (`marketLeadTimeExpiredDecay`, default ×0.40) when `pattern.leadTimeHours ≤ 0`
- **Far-horizon decay** (`marketLeadTimeFarHorizonDecay`, default ×0.85) when `leadTimeHours > 168` (7d)
- **Per-pattern average relevance lift** when a `relevanceByPatternId` map is supplied — scales into `[0.85, 1.15]`

Pass via `InsightRankingContext.marketIntelligence: { totalCustomerCount, relevanceByPatternId }`.

Market patterns now also bypass the `minEvidenceSignals` hard gate — a single high-stakes regulator publication is sufficient evidence. The actionability + confidence + cross-domain framing gates still apply.

#### Type additions

- `CustomerProfile.industryVertical` — F5-aligned vertical (`"financial-advisory"`, `"ndis-provider"`, etc.) for direct matching against scanner-declared verticals.
- `CustomerProfile.engagementType` — `"retainer" | "project" | "one-off" | "prospect" | ...` for engagement-weighted relevance boost.
- `CustomerMarketRelevance` + `CustomerRelevanceReason` — return types from the relevance scorer.
- `MarketIntelligenceRankingContext` — strategic-value-aware InsightRanker context.

#### Test coverage

- `tests/pattern-analysis-customer-relevance-scorer.test.ts` — 9 tests covering each dimension, engagement boost gating, memory hook, and score clamping.
- `tests/pattern-analysis-market-detectors.test.ts` — 9 tests covering each of the five detectors + `runAllMarketDetectors` composition.
- `tests/pattern-analysis-market-intelligence-source.test.ts` — 6 tests covering `publicationToSignal`, `MarketIntelligenceSource`, vertical filtering, and the orchestrator/scanner feed adapters.
- `tests/pattern-analysis-insight-ranker-market.test.ts` — 5 tests covering market evidence-gate bypass, exclusivity boost, widely-known decay, lead-time decay, and per-pattern relevance lift.

#### Backwards compatibility

The existing `MarketIntelligenceScanner`, `RegulatoryFeedAdapter`, and `CustomerImpactMapper` from Phase D's earlier iteration remain exported and functional — Phase D adds composable building blocks without breaking the orchestrator API. Hosts can migrate detector-by-detector as needed.

#### F5 coordination

Phase D continues the F5/F7 seam established earlier:

- F5 owns the scanner infrastructure (`Scanner`, `ScannerOrchestrator`, regulator-specific scrapers).
- F7 consumes the scanner output via `MarketIntelligenceSource` and emits market patterns.
- The `MarketDetectorContext.crossRefRules` field accepts F5's `AUSTRALIAN_CROSS_REFERENCE_RULES` directly — no duplication of cascade encoding.

## [0.33.0] — 2026-05-29

### Added — Compliance Freshness: F5 Phase C — citation watermarking + cross-reference matrix + Vera/Cora sub-agents

**The defensibility seam: every regulator claim in a Sentinel AI compliance briefing is now individually traceable to a publication record, scored for freshness, and cross-referenced for cascading impact.** Adds the two flagship sub-agents — Vera (watchdog) and Cora (briefing builder) — that consume the Phase A primitives and Phase B scanners.

#### `src/compliance-freshness/citations/`

- **`CitationWatermark`** — `[citation:reg/date/rule-id]` inline markers. Stateless regex-driven format/parse/insert/strip/verify. Slugifies regulator citation ids ("RG 99" → "rg-99") so markers compare reliably across the resolver and staleness detector.
- **`CitationResolver`** — expands a marker (or every marker in a text body) into a `ResolvedCitation` with publication, internal citation source, and retrieval date.
- **`StalenessDetector`** — regulator-specific freshness thresholds (`DEFAULT_FRESHNESS_DAYS`: AUSTRAC = 30, ASIC = 30, ATO = 30, OAIC = 30, NDIS Commission = 60, AHPRA = 90, NHMRC = 180, default = 60). Attaches `supersededBy` from the publication index when a newer publication exists for the same `ruleId`.
- **`PublicationIndex`** interface + **`InMemoryPublicationIndex`** test adapter.

#### `src/compliance-freshness/cross-reference/`

- **`CrossReferenceMatrix`** — graph-theoretic extension of Phase A's `CrossReferenceMap`. Transitive cascade traversal up to bounded depth (`cascade(regulatorId, maxDepth)`), bidirectional adjacency (`upstreamOf`, `areDirectlyRelated`), path-level impact reduction (min-impact along the path so mandatory→advisory downgrades to advisory at the leaf).
- **`CascadeDetector`** — given a `RegulatorPublication`, returns `CascadeImplication[]` with rationale. Classification-aware: determinations / rulings / standard-updates upgrade predicted impact; media-releases downgrade it.
- **`VerticalImpactMapper`** — maps cascades + publications to industry verticals. Scanner-declared verticals score 1.0; cross-reference-derived ones scale by `crossReferenceConfidenceWeight × IMPACT_WEIGHT`. Output is the seam F7 Pattern Analysis consumes for market intelligence pattern construction.
- **`DEFAULT_REGULATOR_VERTICALS`** — AUSTRAC → financial-advisory/accounting/legal/real-estate; AHPRA → healthcare/gp/allied-health/dental; NDIS_COMMISSION → ndis-provider/allied-health.

#### `src/compliance-freshness/sub-agents/`

- **`Vera`** — freshness watchdog. One `sweep({ since?, operatorIds? })` polls scanners, indexes publications, predicts cascades via `CascadeDetector`, scans operator briefings via `StalenessDetector`, and emits `VeraAlert[]` ready for F0.10 Proactive Surfacing. Continuous mode (`startContinuous(intervalMinutes, onAlert)`) returns a disposer. Fail-soft per-scanner.
- **`Cora`** — on-demand briefing builder. Takes topic + regulator scope + audience, pulls grounding publications from the `PublicationIndex`, drafts via Claude (default `claude-sonnet-4-6`) with `[citation:...]` markers inline, then runs the Phase C verification stack on the output (`CitationResolver` + `StalenessDetector` + `CascadeDetector` + `VerticalImpactMapper`). The prompt explicitly instructs Claude to only emit markers matching the supplied grounding publications.

#### Alert taxonomy

| `VeraAlert.type` | Trigger | Severity scaling |
|---|---|---|
| `publication` | New scanner publication | by classification — determination / ruling / standard-update → high |
| `cascade` | `CascadeDetector` predicts downstream impact | by predicted impact — mandatory → critical |
| `staleness` | Briefing markers exceed regulator threshold | by ratio of stale markers — ≥0.5 or 5 stale → critical |
| `drift` | (Reserved for Phase A `DriftDetector` wrapper.) | — |

#### F7 coordination

`VerticalImpactMapper` is the shared seam between F5 Phase C and F7 Pattern Analysis. F5 owns the cross-reference matrix + cascade prediction; F7 consumes the `VerticalImpact[]` output (via `RegulatoryFeedAdapter` or direct injection) for market intelligence pattern types (`cross_regulator_cascade`, `legislative_change_cascade`, etc.). No code is duplicated.

#### Tests

52 new Vitest specs across `citations/`, `cross-reference/`, `sub-agents/`, plus an end-to-end integration test: synthetic publication stream → watermarks → resolution → staleness → cascade → vertical impact; AUSTRAC notice → ASIC cascade.

---

## [0.32.0] — 2026-05-29

### Added — Cross-Agent Learning: F3 Phase B (capture + contextual patterns + cross-customer sharer + LearningMind)

Phase A established the storage-side primitives (`ErrorClassifier`, `ErrorAggregator`, `FixProposer`, `CapabilityEmergenceDetector`, `SkillRecommendationEngine`, `ImprovementRolloutOrchestrator`). Phase B adds the moving parts that turn the strategic moat into a continuous loop, additively — Phase A primitives continue to work unchanged.

#### Capture layer — `src/cross-agent-learning/aggregators/`

- **`SubAgentErrorCapture`** wraps any sub-agent operation and converts thrown exceptions, timeouts, malformed inputs, and retry-exhaustion failures into normalised `ErrorEvent`s. Four entry points:
  - `captureToolCall(ctx, fn)` — captures and re-throws on failure
  - `captureWithTimeout(ctx, fn, timeoutMs?)` — hard timeout with classified `[timeout]` event
  - `captureMalformedInput(ctx, message, detail?)`
  - `captureRetryExhaustion(ctx, attempts, lastError)`
- **`ContextualPatternExtractor`** runs over the deduplicated signatures and finds the discriminating condition under which a class of error fires (emoji, attachment, long subject, specific keyword). Output is `ContextualPattern { id, signatureId, label, conditions[], affectedAgents[], affectedDeployments[], evidenceCount, confidence }` — "Olive's draft_reply fails when `contains_emoji=true`" rather than just "draft_reply sometimes fails". Deterministic, no LLM.

#### Recommenders — `src/cross-agent-learning/recommenders/`

| Class | Output | When |
|---|---|---|
| `FixRecommender` | `FixRecommendation { strategy, suggestedConfig }` — rule-based | Every pattern; maps to `input_sanitisation` / `retry_strategy` / `alternative_tool_path` / `validation_guard` / `prompt_update` / `config_change` |
| `PromptUpdateRecommender` | `PromptUpdateRecommendation { agentId, suggestedPromptDelta }` — Claude Haiku | Patterns whose underlying signature is `prompt-issue` or `model-limitation` |

Both set `requiresApproval: true` as a literal type — TypeScript enforces approval gating.

#### Cross-customer sharers — `src/cross-agent-learning/sharers/`

- **`OptInPolicyEnforcer`** + **`InMemoryCustomerSharingPolicyStore`** — per-customer policy: `shareCategories[]` and `consumeCategories[]` over `LearningCategory = "errors" | "fixes" | "capabilities"`. Default is fully opt-out — no opt-in, no flow.
- **`LearningSharer`** — three share verbs (`shareErrorSignature`, `shareFixRecommendation`, `shareCapabilityEmergence`) each returning a `AnonymisedLearning` or `null` when the origin has not opted in. Anonymisation strips customer/operator/deployment identifiers and free-text evidence excerpts; only the pattern, fix strategy, or capability label and aggregate counts survive. Origin is recorded as a salted SHA-256 hash (configurable via `originHashSalt`) — the `distribute()` helper uses the hash to refuse redelivery to the source customer without exposing the customer id.

#### `LearningMind` sub-agent — `src/cross-agent-learning/sub-agent/`

Ties the loop together. One method, `sweep()`, returns a `LearningMindSweepResult`:

```typescript
{
  patterns: ContextualPattern[];
  fixRecommendations: FixRecommendation[];
  promptUpdates: PromptUpdateRecommendation[];
  candidateEvents: CandidateEvent[];   // F0.10 proactive surfacing
  anonymised: AnonymisedLearning[];    // ready for fleet distribution
}
```

`CandidateEvent.payload.requiresApproval === true` on every surfaced fix or prompt-update — consumers route through F0.6 before any rollout. `LearningMind` never sends, never applies, never persists fixes — it produces structured recommendations and lets the operator decide.

#### New types

`CaptureKind`, `CaptureContext`, `PatternCondition`, `ContextualPattern`, `FixStrategy`, `FixRecommendation`, `PromptUpdateRecommendation`, `LearningCategory`, `CustomerSharingPolicy`, `CustomerSharingPolicyStore`, `AnonymisedLearning`.

#### Tests

30 new vitest cases across six files (`tests/cross-agent-error-capture.test.ts`, `tests/cross-agent-pattern-extractor.test.ts`, `tests/cross-agent-fix-recommender.test.ts`, `tests/cross-agent-prompt-update-recommender.test.ts`, `tests/cross-agent-learning-sharer.test.ts`, `tests/cross-agent-learning-mind.test.ts`). Total suite: **763 tests pass** (was 733 after F6 Phase B).

---

## [0.31.0] — 2026-05-29

### Added — Cost Monitoring: F6 Phase B — trackers, analyzers, Ledger & Tally

Phase B layers four instrumentation trackers, four aggregation analyzers, the **Ledger** sub-agent (on-demand reporter), and the **Tally** sub-agent (proactive cost-watcher) on top of the Phase A primitives. All Phase A files are untouched — Phase B is purely additive.

#### Trackers (`src/cost-monitoring/trackers/`)

Four wrappers that delegate to the Phase A `SpendTracker` interface. All data flows through the same `SpendStore` so margins, anomaly detection, and analyzer outputs are always consistent.

- **`ApiCallTracker`** — canonical LLM SDK call wrap point. Emits structured logs (injectable `SimpleLogger`). Static `ApiCallTracker.wrap(call, tracker, params)` returns `{ result: T; spend: SpendRecord }` — instrument any Anthropic API call in one line.
- **`CacheHitTracker`** — pure analytics over `SpendRecord[]`. `analyzeCacheHits()` returns `CacheHitReport` with `hitRatio`, `perAgent`, and optional `perWorkflow` (grouped by `taskIntent`). `loadAndAnalyzeForCustomer()` async variant fetches from store.
- **`OcrTracker`** — tracks OCR usage across four provider categories (`tesseract | aws_textract | google_vision | claude_vision`). Configurable `costPerPage` table; `costDifferential()` surfaces cheapest-provider savings.
- **`BatchTracker`** — tracks Batch API vs realtime usage, applies the Anthropic 50% batch discount, and stores `BatchAttribution` records so `batchSavingsReport()` can accurately compute savings without distorting token analytics.

#### Analyzers (`src/cost-monitoring/analyzers/`)

Four pure aggregation analyzers; async variants fetch from `SpendStore`.

- **`PerCustomerSpendAnalyzer`** — `aggregateDaily / aggregateWeekly / aggregateMonthly` returning `DailySpendBreakdown[] / WeeklySpendBreakdown[] / MonthlySpendBreakdown[]` with `perAgent` and `perModel` sub-maps.
- **`PerAgentSpendAnalyzer`** — same shape but agent-first. `loadAllAndAggregateMonthly()` answers "Olive cost $X across all customers".
- **`PerModelSpendAnalyzer`** — `breakdown()` returns `ModelSpendBreakdown` with `opusFraction`, `sonnetFraction`, `haikuFraction`, `otherFraction`. Uses `OPUS_MODEL_PATTERN` from Phase A anomaly-detector.
- **`MarginMonitorAnalyzer`** — wraps Phase A `MarginMonitor` with `analyzeTier(tierId, year, month)` and `summaryByTier(year, month)` returning `TierMarginSummary[]` (`customerCount`, `avgMarginRatio`, `customersBelowTarget`, `customersOverCap`).

#### Ledger sub-agent (`src/cost-monitoring/sub-agents/ledger.ts`)

On-demand billing intelligence. Constructor accepts optional deps and constructs defaults from required ones.

- `spendReport(customerId, period)` → `SpendReport` with `monthlyTotalUsd`, `perAgent`, `perModel` (`ModelSpendBreakdown`), `dailySeries`, `cacheHitReport`, `margin`.
- `recommendOptimisations(customerId, period)` → `LedgerOptimisationRecommendation[]` (kinds: `model_swap | caching_opportunity | opus_overuse`). All have `requiresApproval: true`.
- `checkMargin(customerId, period)` → delegates to Phase A `MarginMonitor`.
- `anomalyScan(period)` → delegates to Phase A `AnomalyDetector`.

#### Tally sub-agent (`src/cost-monitoring/sub-agents/tally.ts`)

Proactive cost-watcher and F0.10 TriggerSource adapter.

- `scan(period, now?)` → `TallyAlert[]` — runs `MarginMonitor.checkAll` + `AnomalyDetector.detectAll` (filtered to `cache_hit_ratio_drop`).
- `notifyExternalPricingChange(report)` → pure mapping of `PricingChangeImpact[]` → `TallyAlert[]` of kind `external_pricing_change`.
- `asTriggerSource(name?)` → `TriggerSource` — maps `TallyAlert` → `CandidateEvent` with `payload.baseRelevance` (0.95 critical / 0.7 warning) and `payload.suggestedUrgency` ("now" / "morning_brief"). Uses `import type` for proactive-surfacing — no runtime circular dependency.

#### Tests

10 new test files, 51 new tests (all passing). One file per tracker, one per analyzer, one for Ledger, one for Tally. All deterministic (`now` passed explicitly).

#### Recommenders directory

`src/cost-monitoring/recommenders/index.ts` added as a thin re-export barrel pointing at Phase A `ModelRecommender` and `PricingChangeAdapter`. No new logic — matches the brief's directory shape without duplicating code.

---

## [0.30.0] — 2026-05-29

### Added — Relationship Cadence: F4 Phase B detectors + Nyx sub-agent

Four standalone cadence detectors under `src/relationship-cadence/detectors/`, two recommenders under `src/relationship-cadence/recommenders/`, and the `Nyx` sub-agent that wraps them. Phase A primitives (`computeHeatScore`, `PolicyInterventionRecommender`, `ClaudeInterventionDrafter`) continue to work unchanged — Phase B is purely additive.

#### New detectors

Each is a pure function over `(operatorId, RelationshipState[], config)` returning `CadencePattern[]`. The per-relationship "signal stream" is `RelationshipState.contacts` (`ContactPoint[]`).

| Detector | Signature | Detects |
|----------|-----------|---------|
| `detectGoingCold(operatorId, relationships, config)` | `CadenceDetectorConfig` | Warm leads going cold — last contact past phase-appropriate cool/cold thresholds |
| `detectFrequencyShift(operatorId, relationships, config?)` | `FrequencyShiftDetectorConfig` | Significant change in correspondence frequency period-over-period (positive or negative delta) |
| `detectUnilateralSilence(operatorId, relationships, config?)` | `SilenceDetectorConfig` | Operator sent outbound, no inbound reply within window — one-sided silence |
| `detectEscalationNeeded(operatorId, relationships, config?)` | `EscalationNeededDetectorConfig` | Joint sentiment-drop + frequency-decline signal — relationship health deteriorating |

Each detector also exports a `CadenceDetector<TConfig>` object (`cadenceDetector`, `frequencyShiftDetector`, `silenceDetector`, `escalationNeededDetector`) implementing a common contract:

```typescript
export interface CadenceDetector<TConfig = unknown> {
  readonly name: string;
  detect(
    operatorId: string,
    relationships: RelationshipState[],
    config?: TConfig,
  ): CadencePattern[];
}
```

#### `runAllCadenceDetectors(operatorId, relationships, config)` orchestrator

New top-level export from `src/relationship-cadence/detectors/index.ts` (and re-exported from the module barrel). Invokes all four detectors over a single relationship batch and returns the unified `CadencePattern[]` list. Selectively disable detectors via `{ disable: ["escalation_needed"] }`. Per-detector config passes through under matching keys.

#### New recommenders

- **`PolicyNextTouchpointRecommender`** — coalesces a `CadencePattern[]` for each relationship into a single `OutreachRecommendation` answering *when* to reach out (proposed touch time respecting `policy.minDaysBetweenTouches`) and *which channel* (prefers the most recent positive-sentiment inbound channel, falls back to `policy.preferredChannels[0]`). Sorts high → medium → low urgency.
- **`ClaudeDraftSuggester`** — generates a `DraftedOutreach` from an `OutreachRecommendation` using F0.7 Personality (`buildSystemPrompt`). Mirrors `ClaudeInterventionDrafter`'s channel handling (email/linkedin/call_script). Always sets `requiresApproval: true` — never auto-sends.

#### Nyx sub-agent

New `Nyx` class in `src/relationship-cadence/sub-agents/nyx.ts` exposes three verbs:

```typescript
const nyx = new Nyx(tracker, { policy, anthropic });

// 1. Full sweep — all detectors + health rollup + recommendations
const { patterns, health, states, recommendations } = await nyx.scan(operatorId);

// 2. Focused per-relationship recommendation (or null when nothing is due)
const next = await nyx.recommendNextAction(operatorId, relationshipId);

// 3. Draft an outreach message — requires Anthropic client, NEVER sends
const draft = await nyx.draftOutreach(operatorId, relationshipId, personality);
```

`NyxConfig.approvalGate` provides an optional hook that lets consumers wire `createProposal` (F0.6) directly inside `Nyx.draftOutreach` so the draft lands on the approval queue in a single call. Without an approval gate, the caller routes the returned `DraftedOutreach` through `createProposal` themselves.

#### New types

- `CadencePattern` / `CadencePatternType` — pattern emitted by a detector, anchored to one relationship, with evidence + confidence + detector-specific `metrics`.
- `CadenceState` — point-in-time snapshot: heat + phase + 90-day inbound/outbound counts.
- `RelationshipHealth` / `RelationshipHealthLevel` — `healthy` / `watching` / `at_risk` / `critical` rollup of all observed patterns for a relationship.
- `OutreachRecommendation` — "reach out THEN via THIS channel" with `proposedTouchAt`, `urgency`, `basedOnPatternIds`.
- `DraftedOutreach` — drafted message with `requiresApproval: true`. Parallel to Phase A's `DraftedIntervention` but linked to the new `OutreachRecommendation` flow.

#### Approval gating

`DraftedOutreach.requiresApproval` is typed `true`. `Nyx` NEVER sends. Consumers pipe the draft into F0.6 `createProposal` — either manually after `Nyx.draftOutreach` returns, or automatically via the `approvalGate` hook supplied at construction. README documents both paths.

#### Tests

47 new vitest cases across seven files covering each detector, both recommenders, and Nyx integration. Total suite: **682 tests pass** (was 635).

---

## [0.29.0] — 2026-05-28

### Added — Compliance Freshness: F5 Phase B regulator publication scanners

**The Phase A `RegulatorPublicationFetcher` interface now has a real implementation.** Six concrete scanners pull publications from Australian regulator websites, dedupe across runs, and project into Phase A's existing `RegulatorWatcher` via a one-line adapter.

#### Six concrete scanners

| Scanner | Regulator | Source | Cadence | Industries |
|---|---|---|---|---|
| `AustracScanner` | AUSTRAC | `austrac.gov.au/news-and-media/news` | weekly | financial-advisory, accounting, legal, real-estate |
| `AsicScanner` | ASIC | media releases + regulatory guides (two URLs) | monthly | financial-advisory, accounting |
| `AtoScanner` | ATO | `ato.gov.au/law/` | monthly | accounting, financial-advisory, legal |
| `AhpraScanner` | AHPRA | `ahpra.gov.au/News/Notifications.aspx` | monthly | healthcare, gp, allied-health, dental |
| `NdisCommissionScanner` | NDIS_COMMISSION | `ndiscommission.gov.au/news-and-media` | monthly | ndis-provider, allied-health |
| `OaicScanner` | OAIC | `oaic.gov.au/news-and-information/media-releases` | monthly | all (privacy is cross-cutting) |

Each scanner accepts an `HttpClient` and a `ScannerLogger` at construction time for testability. The default `defaultHttpClient` is a thin native-fetch wrapper with a 15s timeout, a Sentinel-AI user agent, and throws on non-2xx.

#### `ScannerOrchestrator` — registry + fail-soft runner + dedup + Phase A adapter

- **Registry** — `registerScanner(scanner)`, `listScanners()`.
- **`runOnce(since?)`** — runs every registered scanner via `Promise.allSettled`. One throwing scanner does not break the others (errors logged through `ScannerLogger.error`).
- **In-memory dedup** — every emitted `RegulatorPublication` is keyed by its sha256 hash (`computePublicationHash(title, publishedAt, sourceUrl)`). A second run with the same fixtures yields zero new results. Capped at 10,000 entries by default (`dedupCapacity` constructor option), FIFO-evicted on overflow. Persistence is intentionally out of scope for Phase B — restart-clean is by design.
- **`runScanner(regulatorId, since?)`** — single-scanner variant used by `asFetcher()`.
- **`start(intervalOverrideMinutes?)` / `stop()`** — per-scanner `setInterval` polling timers. Optional override; otherwise each scanner uses its own `intervalMinutes`.
- **`resetDedup()`** — test/manual hook.

#### `ScannerOrchestrator.asFetcher()` — the Phase A seam

```ts
const orch = new ScannerOrchestrator();
orch.registerScanner(new AustracScanner());
// ...register the other five...
const watcher = new RegulatorWatcher(KNOWN_AUSTRALIAN_REGULATORS, orch.asFetcher());
const updates = await watcher.watchAll(RegulatorWatcher.sinceForCadence("weekly"));
```

`asFetcher()` returns a `RegulatorPublicationFetcher` that calls `runScanner()` and projects each `RegulatorPublication` down to the slimmer `RegulatorUpdate` Phase A expects. Scanner-specific metadata (`hash`, `classification`, `industryVerticals`, `citationId`, `summary`, `rawBody`) is stripped at the boundary — Phase A doesn't see it, but the orchestrator's dedup still uses it internally.

#### `RegulatorPublication` — superset of `RegulatorUpdate`

Scanner-level type adds `citationId`, `classification` (one of media-release / regulatory-guide / determination / ruling / guidance / standard-update / policy-update / notice / other), `industryVerticals`, `summary` (~500 char), optional `rawBody`, and the deterministic `hash`. ASIC and ATO scanners extract citation IDs from titles where present (e.g. "RG 99", "TR 2026/1", "PCG 2024/3").

#### `cheerio ^1.2.0` added as a runtime dependency

All six scanners parse HTML via cheerio. RSS-mode and the helper `loadRss()` are exported for future scanners.

#### Import surface

```ts
import {
  ScannerOrchestrator,
  AustracScanner,
  AsicScanner,
  AtoScanner,
  AhpraScanner,
  NdisCommissionScanner,
  OaicScanner,
  defaultHttpClient,
  computePublicationHash,
  type RegulatorPublication,
  type Scanner,
} from "@sentinel-ai-solutions/agent-kit/compliance-freshness";
```

---

## [0.28.0] — 2026-05-28

### Added — Pattern Analysis: F7 Phase B detection algorithm expansion

Five standalone detector modules under `src/pattern-analysis/detectors/`, each a pure function over `Signal[]` returning `Pattern[]` candidates ready to feed `InsightRanker`. The original `PatternDetector` class (Phase A) continues to work unchanged — Phase B is purely additive.

#### New detectors

| Detector | Signature | Detects |
|----------|-----------|---------|
| `detectTemporalClusters(signals, config?)` | `TemporalDetectorConfig` | Rolling-window topic spikes — "N signals raised topic X within window W" |
| `detectCooccurrences(signals, config?)` | `CooccurrenceDetectorConfig` | Conditional-probability pairs — "P(Y \| X) > threshold within window W" |
| `detectClusters(signals, config?)` | `ClusterDetectorConfig` | Group-convergence — "M of N members of group G are raising topic X" (industry / segment / business-type) |
| `detectTrajectoryShifts(signals, config?)` | `TrajectoryDetectorConfig` | Period-over-period frequency shifts — "Brad↔Drew correspondence is down 70% MoM" |
| `detectSentimentTrends(signals, config?)` | `SentimentDetectorConfig` | Linear-regression sentiment trend with consistency check; pluggable `SentimentScorer` hook so hosts can swap in a Claude-backed scorer |

Each detector also exports a `Detector<TConfig>` object (`temporalDetector`, `cooccurrenceDetector`, `clusterDetector`, `trajectoryDetector`, `sentimentDetector`) implementing a common contract:

```typescript
export interface Detector<TConfig = unknown> {
  name: string;
  detect(signals: Signal[], config?: TConfig): Pattern[];
}
```

#### `runAllDetectors(signals, config?)` orchestrator

New top-level export from `src/pattern-analysis/pattern-detector.ts` (and re-exported from the module barrel). Invokes all five detectors over a single signal stream and returns the unified `Pattern[]` list. Selectively disable detectors via `{ disable: ["sentiment"] }`. Per-detector config passes through under matching keys: `{ temporal, cooccurrence, cluster, trajectory, sentiment }`.

#### Shared detector helpers

`src/pattern-analysis/detectors/shared.ts` exports `tokenise`, `groupBy`, `buildTokenIndex`, `defaultSentimentScorer`, `singleOperator`. Phase B's tokeniser is slightly more permissive than Phase A's (3+ chars instead of 4+) so three-letter compliance terms like `aml`, `tax`, `rba` survive.

#### Pluggable sentiment scorer

`SentimentDetectorConfig.sentimentScorer?: (signal: Signal) => number` lets the host swap the lexicon-based default for a Claude-backed or local-model scorer. The default scorer remains for offline/test paths.

#### `Pattern` interface — no breaking changes

Phase A's `Pattern` interface was already general enough to cover Phase B's five detector outputs (id, operatorId, type, description, customerIds, evidenceSignalIds, confidence, windowStart, windowEnd, detectedAt, optional leadTimeHours). Phase B preserves it as-is — no breaking changes, no extensions.

#### Tests

Six new test files in `tests/pattern-analysis-*.test.ts` — one per detector plus an integration test for `runAllDetectors`. Each file contains positive cases (matching the spec's worked examples) and negative cases (insufficient signals, out-of-window references, below-threshold deltas). 14 new tests, all passing.

#### Dev experience

`vitest.config.ts` added at the repo root. Disables CSS/PostCSS processing so vitest does not search the directory tree for a `postcss.config.*` (which fails on Windows when a parent directory's `package.json` carries a UTF-8 BOM). Tests run pure-TS — no CSS handling needed.

`package.json` re-saved without UTF-8 BOM so PostCSS config search no longer chokes when vitest is invoked from inside a worktree.

---

## [0.27.0] — 2026-05-28

### Added — Pattern Analysis: F7 Market Intelligence extension

**The strategic moat: Sentinel AI reads signals in the news while competitors read the news.**

Extends F7 beyond internal customer data to monitor external intelligence feeds — regulators, government, industry press, competitors, macro events, adjacent technology — and maps their impact to specific clients before mainstream pick-up.

#### New classes

- **`MarketIntelligenceScanner`** (`src/pattern-analysis/market-intelligence-scanner.ts`) — orchestrates external `DataSourceAdapter` instances and runs five new deterministic market intelligence pattern detection algorithms. Accepts `CrossReferenceRule[]` from F5 Compliance Freshness for cascade prediction without duplicating the regulator scanner infrastructure.

- **`RegulatoryFeedAdapter`** — wraps F5's `RegulatorPublicationFetcher` into the F7 `DataSourceAdapter` interface. Converting `RegulatorUpdate` records into normalised `Signal` objects. This is the shared-service seam: F5 owns the fetcher logic; F7 owns pattern detection.

- **`CustomerImpactMapper`** — resolves which customers are affected by an external signal. Matches on `relevantRegulators`, `relevantLegislation`, topic `tags`, and `industry`. Populates `Pattern.customerIds` with real CRM entity IDs for affected clients.

#### Five new pattern types

| Type | What it detects |
|------|-----------------|
| `first_mover_opportunity` | Regulator/government update with time-sensitive client impact — brief before mainstream pick-up |
| `pre_emptive_positioning` | Topic appearing across 3+ distinct external source types — building toward mainstream coverage |
| `competitive_intelligence` | Competitor move mapped to at-risk clients via industry/tag matching |
| `legislative_change_cascade` | Pending legislation → client exposure mapping → briefing window open now |
| `cross_regulator_cascade` | Regulator A publishes → F5 `CrossReferenceRule` predicts Regulator B follows → brief before confirmation |

#### Lead time constants

`LEAD_TIME_HOURS` exported: `{ government_policy: 72, regulator_publication: 48, competitor_activity: 24, adjacent_technology: 8, industry_news: 4, macro_signal: 2 }`. Used by `InsightRanker` to display the remaining window in insight headlines.

#### Four new insight categories (with scoring)

| Category | Actionability | Strategic Value | Notes |
|----------|--------------|-----------------|-------|
| `first_mover_advantage` | 0.99 | 1.00 | Maximum — exclusive intelligence competitors don't have |
| `legislative_impact` | 0.95 | 0.98 | Client compliance exposure before commencement |
| `regulatory_opportunity` | 0.90 | 0.90 | Proactive briefing builds trust and protects clients |
| `competitive_response` | 0.85 | 0.82 | Pre-empt competitor outreach to at-risk clients |

#### CustomerProfile + CustomerProfileStore

Lightweight CRM profile used by `CustomerImpactMapper` for relevance matching: `industry`, `entityType`, `relevantRegulators`, `relevantLegislation`, `tags`. `InMemoryCustomerProfileStore` included for testing.

#### F5 coordination

F7 and F5 Compliance Freshness share the same `RegulatorPublicationFetcher` and `CrossReferenceRule[]` via constructor injection — the regulator scanner is not duplicated between modules.

#### InsightRanker additions

All five new market pattern types added to: `INHERENT_ACTIONABILITY_BY_TYPE`, `DEFAULT_FRAMING`, `ALTERNATIVE_FRAMINGS`, `defaultAction`. Market intelligence patterns bypass the `minActionabilityScore` gate by design — all have inherent actionability ≥ 0.80.

---

## [0.26.0] — 2026-05-28

### Added — Pattern Analysis module — F7 Foundation

Flagship intelligence capability: **deep-coaching / "ahead of the game"**. SMB operators are submerged in their own data. They see individual conversations, individual deals, individual emails — never the forest. Pattern Analysis surfaces the forest, delivering cross-client, cross-industry, cross-channel, and cross-conversation intelligence before the operator has to ask.

The commercial framing: "you should be reaching out to client X because the signal pattern matches your strongest converts" — instinct codified as a systematic, cross-domain intelligence layer.

#### Core classes

- **`DataAggregator`** (`src/pattern-analysis/data-aggregator.ts`) — producer-implemented `DataSourceAdapter` interface converts each F1 Connections provider (email, HubSpot, calls, documents, memory episodes, agent conversations) into normalised `Signal` objects and appends them to the `SignalStore`. `register()` / `aggregateSource()` for selective per-source runs.

- **`PatternDetector`** (`src/pattern-analysis/pattern-detector.ts`) — six deterministic (no LLM) detection algorithms run over the `SignalStore`:
  - **`detectTemporalClusters`** — identifies topics mentioned by 3+ distinct customers within the analysis window. The "three clients mentioned succession planning this week" signal.
  - **`detectCoOccurrences`** — finds A→B topic pairs that recur across 2+ customers. Pre-empt the follow-up before it arrives.
  - **`detectIndustryClusters`** — compares topic frequency across customer verticals (2.5× default threshold). Vertical-specific concerns and competitive differentiators.
  - **`detectRelationshipShifts`** — compares signal frequency between the first and second half of the analysis window. 50%+ shift flags a relationship strengthening or cooling.
  - **`detectSentimentTrends`** — splits correspondence into rolling periods, checks for consistent positive or negative sentiment direction across 3+ consecutive periods.
  - **`detectCrossChannelGaps`** — topics appearing in email but absent from calls (or vice-versa) for the same customer. Avoidance signal; preference for written records.

- **`InsightRanker`** (`src/pattern-analysis/insight-ranker.ts`) — converts `Pattern` objects into operator-ready `Insight` objects. Four-dimension scoring:
  - `actionabilityScore` (35%) — how concretely can the operator act?
  - `noveltyScore` (25%) — recently delivered insights of the same category score lower
  - `strategicValueScore` (25%) — does this advance the engagement or commercial relationship?
  - `confidenceScore` (15%) — signal strength of the underlying pattern
  - Composite `rankScore` determines surfacing order. Optional Anthropic client (Claude Sonnet) enriches `body` and `recommendedAction` with specific, context-aware language. All insights have `requiresApproval: true` — never auto-acted.

- **`PatternAnalysisTriggerSource`** (`src/pattern-analysis/proactive-surfacer.ts`) — implements the F0.10 `TriggerSource` protocol (registered as `"intel"` source). Plug directly into `ProactiveSurfacingPipeline`. Each `Insight` maps to one `CandidateEvent` with `baseRelevance` pre-set from `rankScore` for the F0.10 rule-based scorer. `formatInsightForTelegram()` exported for direct Telegram delivery without the full pipeline.

- **`CrossCustomerAnonymiser`** (`src/pattern-analysis/cross-customer-anonymiser.ts`) — enforces the per-customer trust contract. `filterSignals()` strips `"redact"` customers before detection. `anonymise()` replaces customer IDs with deterministic labels ("Client A", "Client B", …) for `"label"`-policy customers. `canCrossReference()` guards pattern creation when any participant prohibits cross-customer analysis.

#### Types

`SignalSource`, `PatternType`, `InsightCategory`, `AnonymisationLevel`, `Signal`, `Pattern`, `Insight`, `InsightDelivery`, `CustomerAnonymisationPolicy`

#### Storage interfaces + in-memory implementations

`SignalStore` / `InMemorySignalStore`, `PatternStore` / `InMemoryPatternStore`, `InsightStore` / `InMemoryInsightStore`, `InsightDeliveryStore` / `InMemoryInsightDeliveryStore`, `AnonymisationPolicyStore` / `InMemoryAnonymisationPolicyStore`

#### Integration with existing primitives

- **F0.10 Proactive Surfacing** — `PatternAnalysisTriggerSource` implements `TriggerSource`; register with `ProactiveSurfacingPipeline`. Insights flow through Telegram morning brief, Dashboard widget, or ad-hoc query
- **F1 Connections** — each OAuth-connected provider gets a `DataSourceAdapter` that translates provider records into `Signal` objects
- **F3 Cross-Agent Learning** — complementary, not overlapping: F3 handles operational patterns (error rates, capability gaps), F7 handles content patterns (topic clusters, relationship signals, sentiment trends)
- **Memory** — per-client memory episodes ingestible as `"memory_episode"` signals for longitudinal pattern detection
- **Personal-Business Separation Trust Contract** — personal-scope signals are never ingested into the pattern analysis pipeline

#### Sub-path export

```ts
import { PatternDetector, InsightRanker } from "@sentinel-ai-solutions/agent-kit/pattern-analysis";
```

---

---
## [0.25.0] — 2026-05-28

### Added — Cost Monitoring module — F6 Foundation

Generic primitive for **margin protection at scale**. Without per-customer spend
attribution, Sentinel AI's unit economics blow up the moment customer usage
outpaces subscription revenue. With it, you have real-time visibility into every
API call, automatic adaptation to pricing changes, and model-swap recommendations
that preserve quality while protecting margin.

Designed as the foundation for sub-agents **Ledger** (billing intelligence) and
**Tally** (usage analytics).

#### Core classes

- **`CostCalculator`** (`src/cost-monitoring/cost-calculator.ts`) — config-driven
  pricing table. Ships `DEFAULT_PRICING` with current Anthropic model pricing
  (Opus 4.7, Sonnet 4.6, Haiku 4.5) including prompt-cache write/read rates.
  A pricing change is a `pricingOverrides` config swap — no code changes.
  `computeCost()` returns 0 for unknown models (never throws) so cost tracking
  never blocks the hot path. `computeRecordCost()` fills `costUsd` on a
  `SpendRecord` automatically.

- **`DefaultSpendTracker`** (`src/cost-monitoring/spend-tracker.ts`) — implements
  `SpendTracker`. Records every API call with model, customer, agent, token counts,
  task intent, and auto-computed `costUsd`. Storage-agnostic: inject any `SpendStore`.
  `InMemorySpendStore` is provided for tests and local dev; production consumers
  implement `SpendStore` against Prisma.

- **`MarginMonitor`** (`src/cost-monitoring/margin-monitor.ts`) — per-customer
  monthly margin computation and threshold alerting. `computeMargin()` returns a
  `CustomerMargin` snapshot (spend, revenue, margin ratio, cap utilisation).
  `evaluateAlerts()` is pure — apply to any snapshot without I/O.
  `checkAll()` sweeps all configured customers. Alert types:

  | Alert | Trigger |
  |---|---|
  | `approaching_cap` | capUtilisation ≥ 0.70 (configurable) |
  | `over_cap` | capUtilisation ≥ 1.00 (configurable) |
  | `margin_below_target` | marginRatio < tier.targetMarginRatio |

- **`AnomalyDetector`** (`src/cost-monitoring/anomaly-detector.ts`) — detects
  spend pattern anomalies. `detectFromRecords()` is pure (no I/O) for batch use;
  `detectForCustomer()` and `detectAll()` fetch from the store. Three checks:

  | Anomaly | Default threshold | Severity |
  |---|---|---|
  | `opus_spike` | Opus spend > 30% of total | warning / critical at 50% |
  | `cache_hit_ratio_drop` | hit ratio < 20% (after 10k token warmup) | warning |
  | `spend_velocity_spike` | recent half > 3× prior half | warning / critical at 6× |

- **`ModelRecommender`** (`src/cost-monitoring/model-recommender.ts`) — given a
  `TaskSignature` (intent + estimated tokens + quality requirement + cacheability),
  recommends the cheapest model that meets the quality bar. Ships
  `DEFAULT_QUALITY_MODEL_MAP` mapping `highest` → Opus, `high` / `balanced` →
  Sonnet, `efficient` → Haiku → Sonnet → Opus. Cache-aware cost estimation assumes
  80% hit ratio on cacheable context. All recommendations have `requiresApproval: true`;
  consumers wire into F0.6 `createProposal` before applying any swap.

- **`PricingChangeAdapter`** (`src/cost-monitoring/pricing-change-adapter.ts`) —
  when Anthropic or a competitor changes pricing, instantiate with the old
  `CostCalculator` and the new one, call `recalculate()`. Uses historical
  `SpendRecord`s as the usage baseline, applies both pricing tables, and returns
  `PricingChangeImpact[]` sorted by `deltaUsd` descending. `atRiskCustomers()`
  filters to customers where new pricing pushes them over cap or below target margin.

#### Types

`SpendRecord`, `ModelPricing`, `CustomerTier`, `CustomerMargin`, `MarginAlert`,
`AnomalyDetectionResult`, `AnomalyType`, `AnomalySeverity`, `TaskSignature`,
`TaskIntent`, `QualityRequirement`, `ModelRecommendation`, `PricingChangeImpact`,
`SpendStore`, `CustomerTierStore`

#### In-memory adapters (testing + local dev)

- `InMemorySpendStore` — full `SpendStore` implementation. `clear()` and `size()`
  test helpers.
- `InMemoryCustomerTierStore` — full `CustomerTierStore` implementation. Manages
  both tier definitions and customer→tier assignments.

#### Integration with existing primitives

- **F0.10 Proactive Surfacing** — wrap `MarginMonitor.checkAll()` or
  `AnomalyDetector.detectAll()` in a `TriggerSource`. Map `MarginAlert` and
  `AnomalyDetectionResult` to `CandidateEvent` with `urgency: "now"` for over-cap
  and critical anomalies.
- **F0.6 Approval Flow** — `ModelRecommendation.requiresApproval` is always `true`.
  Call `createProposal({ type: "model_swap", payload: rec })` before applying.
- **Memory** — implement `SpendStore` against Prisma for durable, cross-process
  spend history with the recommended `SpendRecord` schema in the module README.
- **Scheduled Tasks** — call `monitor.checkAll()` + `detector.detectAll()` from a
  daily cron for proactive margin and anomaly surfacing.

#### Sub-path export

```ts
import { ... } from "@sentinel-ai-solutions/agent-kit/cost-monitoring";
```

---

---
## [0.24.0] — 2026-05-28

### Added — Relationship Cadence module — F4 Foundation

Generic primitive for "don't let warm leads go cold." Designed as the foundation for Sentinel AI sub-agent **Nyx** (relationship intelligence) and any deployment that needs cadence-aware outreach.

#### Core classes

- **InMemoryRelationshipTracker** (src/relationship-cadence/relationship-tracker.ts) — implements RelationshipTracker, the storage contract for contact history. Scoped per operatorId. Records ContactPoint entries (channel + direction + sentiment + optional note), upserts relationship metadata, keeps contacts sorted by occurredAt. Production consumers implement RelationshipTracker against their own Prisma store.

- **computeHeatScore** (src/relationship-cadence/heat-scorer.ts) — deterministic, no-LLM heat computation. Three factors: recency (60% weight), reciprocity (25%, inbound proportion over 90 days), intensity (15%, contact count over 90 days capped at 5). Returns HeatScore with level (warm / cool / cold), normalised score (0.0–1.0), daysSinceLastContact, and rationale.

- **getCadencePolicy / DEFAULT_POLICIES** (src/relationship-cadence/cadence-policy.ts) — five vertical-specific CadencePolicy pre-sets:

  | Vertical | Nurture cool/cold | Engagement-active cool/cold |
  |---|---|---|
  | saas_b2b | 14d / 30d | 3d / 7d |
  | professional_services | 21d / 45d | 5d / 14d |
  | inancial_advisory | 30d / 60d | 7d / 21d |
  | 
ecruiting | 10d / 21d | 2d / 5d |
  | general | 21d / 45d | 5d / 14d |

  Each policy includes engagementActive, 
urture, and maintenance threshold sets plus preferredChannels.

- **PolicyInterventionRecommender** (src/relationship-cadence/intervention-recommender.ts) — implements InterventionRecommender. Scores all relationships, applies a per-operator calibration multiplier from CalibrationLearner (F0.10 proactive-surfacing, accepted structurally — no hard coupling), and returns recommendations for those below scoreThreshold. Resolves InterventionReason from contact history patterns. Results sorted high → low urgency. Configurable maxRecommendations cap.

- **ClaudeInterventionDrafter** (src/relationship-cadence/intervention-drafter.ts) — implements InterventionDrafter. Composes F0.7 Personality (uildSystemPrompt) to keep the operator's voice consistent. Produces email / LinkedIn message / call script. All DraftedIntervention objects have 
equiresApproval: true — callers must wire into F0.6 createProposal before any outreach is sent. Anthropic client injected as structural type.

#### Types

ContactChannel, ContactDirection, ContactSentiment, ContactPoint, HeatLevel, HeatScore, RelationshipState, CadenceVertical, CadenceThresholds, CadencePolicy, CadencePhase, InterventionReason, InterventionRecommendation, DraftChannel, DraftedIntervention

#### Integration with existing primitives

- **F0.6 Approval Flow** — DraftedIntervention.requiresApproval typed 	rue; consumers convert to ProposalRecord<DraftedIntervention> via createProposal
- **F0.7 Personality** — ClaudeInterventionDrafter calls uildSystemPrompt with the operator's PersonalityDefinition
- **F0.10 Proactive Surfacing** — PolicyInterventionRecommender accepts any CalibrationLearner structurally; heat level maps naturally to Urgency for surfacing pipeline integration
- **F1 Connections** — ContactChannel union covers email, LinkedIn, Slack, Telegram, phone, in-person, conference, SMS

#### Sub-path export

```ts
import { ... } from "@sentinel-ai-solutions/agent-kit/relationship-cadence";
```

---
## [0.23.0] — 2026-05-28

### Added — Cross-Agent Learning module — F3 Foundation

Strategic moat: every customer deployment compounds the resilience and capability surface of every future deployment. Customer N+1 launches with the cumulative fixes and feature signal generated by customers 1...N.

#### Core classes

- **`ErrorClassifier`** (`src/cross-agent-learning/error-classifier.ts`) — rule-based, zero-LLM categorisation of `ErrorEvent` objects into six categories: `transient` / `config` / `schema` / `tool-bug` / `model-limitation` / `prompt-issue`, each with a severity level. Pattern matching operates on normalised message + detail + source component text. Fast enough to run inline on every ingest call.

- **`ErrorAggregator`** (`src/cross-agent-learning/error-aggregator.ts`) — ingests `ErrorEvent` objects, normalises the error message into a stable pattern (UUIDs, timestamps, numbers, long strings stripped), and upserts an `ErrorSignature` that tracks `occurrenceCount`, `deploymentCount`, and `operatorCount` across the fleet. `normalizePattern()` is exported for standalone use. Batch ingestion via `ingestBatch()`.

- **`FixProposer`** (`src/cross-agent-learning/fix-proposer.ts`) — for signatures above a recurrence threshold (`minOccurrencesForProposal`, default: 5), calls Claude Haiku to draft a `FixProposal` with a concrete proposed change (code diff, config update, prompt correction, or retry strategy). All proposals have `requiresApproval: true` and status `DRAFT` — callers must route through F0.6 Approval Flow before any rollout. Confidence score is computed from recurrence × deployment breadth.

- **`CapabilityEmergenceDetector`** (`src/cross-agent-learning/capability-emergence-detector.ts`) — analyses `ConversationExcerpt` batches from across the fleet. Uses Claude Sonnet to identify user requests the agent couldn't fulfil (explicit refusals, partial answers, repeated misunderstandings), then clusters the signals into `CapabilityEmergencePattern` objects scoped by deployment and operator count. Patterns that span only one deployment are discarded as noise. Merges into existing patterns on re-detection. Batched at up to 20 excerpts per API call.

- **`SkillRecommendationEngine`** (`src/cross-agent-learning/skill-recommendation-engine.ts`) — ranks `CapabilityEmergencePattern` objects by demand score (`eventCount × deploymentCount × confidence`) and calls Claude Haiku to derive a canonical skill name, rationale, and suggested `src/<module>/` path for each. Works without an Anthropic client (uses the pattern label directly) for offline ranking. Results sorted highest demand first.

- **`ImprovementRolloutOrchestrator`** (`src/cross-agent-learning/improvement-rollout-orchestrator.ts`) — when a `FixProposal` or `CapabilityEmergencePattern` is approved, initiates a staged `ImprovementRollout` across the deployment fleet: dev (10%) → staging (30%) → prod (60%). Each stage requires an explicit `advanceStage()` call with a `telemetrySummary` — no auto-promotion. Supports `pauseRollout()` and custom `targetStages` for two-stage or single-stage scenarios.

#### Types

`ErrorCategory`, `ErrorSeverity`, `ErrorEvent`, `ErrorSignature`, `FixProposal`, `FixProposalStatus`, `ConversationExcerpt`, `CapabilityEmergencePattern`, `EmergencePatternStatus`, `SkillRecommendation`, `RolloutStage`, `RolloutStatus`, `RolloutStageRecord`, `ImprovementRollout`, `RolloutOptions`

#### Storage interfaces + in-memory implementations

`ErrorEventStore` / `InMemoryErrorEventStore`, `ErrorSignatureStore` / `InMemoryErrorSignatureStore`, `FixProposalStore` / `InMemoryFixProposalStore`, `EmergencePatternStore` / `InMemoryEmergencePatternStore`, `RolloutStore` / `InMemoryRolloutStore`

#### Integration with existing primitives

- **F0.6 Approval Flow** — `FixProposal.requiresApproval` typed `true`; all proposals stored as `DRAFT` pending operator approval before `ImprovementRolloutOrchestrator` is called
- **F0.7 Audit Log** — every `ErrorEvent.receivedAt` timestamp is captured; operators wire `ErrorAggregator.ingest` into their audit pipeline
- **F0.10 Proactive Surfacing** — `CapabilityEmergencePattern` objects and `SkillRecommendation` lists map naturally to `CandidateEvent` for surfacing to the operator

#### Sub-path export

```ts
import { ... } from "@sentinel-ai-solutions/agent-kit/cross-agent-learning";
```

---

## [0.22.0] — 2026-05-28

### Added — Compliance Freshness module — F5 Foundation

Generic primitive for "regulatory content stays current, cross-referenced, and ahead of the curve." Designed as the foundation for Sentinel AI sub-agents **Vera** (compliance advisory) and **Cora** (regulatory navigation).

#### Core classes

- **`RegulatorWatcher`** (`src/compliance-freshness/regulator-watcher.ts`) — polls configured regulators for new publications on a per-regulator cadence (daily/weekly/monthly). `cadenceOverrides` allows raising specific regulators to "weekly" during high-churn rollouts (e.g. AUSTRAC Tranche 2) without touching other regulators.

  ```ts
  import { RegulatorWatcher, KNOWN_AUSTRALIAN_REGULATORS } from "@sentinel-ai-solutions/agent-kit/compliance-freshness";

  const watcher = new RegulatorWatcher(KNOWN_AUSTRALIAN_REGULATORS, fetcher, {
    cadenceOverrides: { AUSTRAC: "weekly" },
  });
  const updates = await watcher.watchAll(RegulatorWatcher.sinceForCadence("weekly"));
  ```

- **`DriftDetector`** (`src/compliance-freshness/drift-detector.ts`) — maps incoming `RegulatorUpdate[]` to affected `BriefingRecord` documents. A briefing is affected if any of its `citationIds` reference a citation whose version has changed. Applies `CrossReferenceMap` to identify cascading impacts from inter-regulator interactions. Computes `DriftSeverity` (low / medium / high / critical) based on citation count and cross-reference impact level.

- **`CrossReferenceMap`** (`src/compliance-freshness/cross-reference-map.ts`) — encodes inter-regulator dependencies. When AUSTRAC updates, ASIC and ATO briefings may also need review. Ships `AUSTRALIAN_CROSS_REFERENCE_RULES` (13 rules covering financial services, health, disability, tax, and consumer protection cross-impacts). Consumers extend with sector-specific rules.

  | Primary | Related | Impact |
  |---|---|---|
  | AUSTRAC | ASIC, ATO | mandatory |
  | ASIC ↔ AFCA | each other | advisory |
  | ATO ↔ TPB | each other | mandatory / advisory |
  | AHPRA ↔ OAIC | each other | advisory / mandatory |
  | NDIS Commission ↔ AHPRA | each other | mandatory / advisory |
  | ACCC → State Fair Trading | NSW, VIC, QLD | mandatory |

- **`InterventionDrafter`** (`src/compliance-freshness/intervention-drafter.ts`) — when drift is detected, calls Claude (Haiku by default) to produce: (1) a revised briefing reflecting the regulatory changes and (2) a concise client-facing notification summary (≤ 150 words). Both are stored as `InterventionDraft` with status `DRAFT`. Consumers convert to `ProposalRecord<InterventionDraft>` via F0.6 ProposalStorage — the drafter **never auto-publishes**.

- **`VersionHistory`** (`src/compliance-freshness/version-history.ts`) — append-only audit trail for briefing lifecycle transitions (DRAFT → PUBLISHED → SUPERSEDED → WITHDRAWN). Each entry captures the content snapshot, status, reason, and `supersededById` chain.

#### Pre-configured Australian regulatory landscape

- **`KNOWN_AUSTRALIAN_REGULATORS`** — `RegulatorConfig[]` for 13 regulators: AUSTRAC, ASIC, ATO, AHPRA, NHMRC, TPB, AFCA, NDIS Commission, ACCC, OAIC, Fair Trading NSW/VIC/QLD
- **`AUSTRALIAN_CROSS_REFERENCE_RULES`** — `CrossReferenceRule[]` encoding 13 known inter-regulator interactions

#### In-memory adapters (testing + local dev)

- `InMemoryCitationStore`
- `InMemoryBriefingStore` (with `.seed()` test helper)
- `InMemoryInterventionStore`
- `InMemoryVersionHistoryStore`
- `InMemoryRegulatorPublicationFetcher` (seed with `.addUpdates()`)

#### Integration with existing primitives

- **F0.6 Approval Flow** — `InterventionDraft` wraps into `ProposalRecord<InterventionDraft>` at the consumer layer; no direct coupling
- **F0.10 Proactive Surfacing** — `DriftDetectionResult` maps to `CandidateEvent` with `sourceName: "compliance"`; see README for reference `ComplianceDriftTriggerSource`
- **F2 MCP / F1 Connections** — `RegulatorPublicationFetcher` interface abstracts over WebFetch, RSS, and regulator API adapters (concrete implementations deferred to future batch)
- **Memory** — `CitationStore` and `VersionHistoryStore` implementations persist per-citation and per-briefing history

#### Sub-path export

```ts
import { ... } from "@sentinel-ai-solutions/agent-kit/compliance-freshness";
```

---

## [Unreleased] — F2 MCP Integration Layer

- Add MCP (Model Context Protocol) client + registry integration (`src/mcp/`)
- Customer deployments can register MCP servers; tools appear as first-class in the agent's tool list
- Example: filesystem MCP server in `examples/mcp-filesystem`

---

## [Unreleased] — Rebrand to Sentinel AI Solutions

- Renamed from `bw-advisory-agent-kit` to `sentinel-ai-agent-kit` (GitHub)
- Republished under npm scope `@sentinel-ai-solutions/agent-kit` (public registry, npmjs.com)
- GitHub auto-redirects old URL `bw-advisory/bw-advisory-agent-kit` → `bw-advisory/sentinel-ai-agent-kit`

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/).

> **Skipped version slots.** `0.8.0`, `0.9.0`, and `0.16.0` were reserved
> during planning but never released. The scope intended for each rolled
> forward into the next published version. No commit was ever tagged at
> those slots — see git tag history for the published timeline.

## [0.20.0] — 2026-05-28

### Added — Connections Module: GitHub, Linear, Atlassian providers (F1 Batch 4 complete)

Completes F1 Batch 4 by adding the final three OAuth 2.0 providers. The
Connections module now covers nine providers across identity, productivity, CRM,
accounting, project management, and source control — enough to serve as the
OAuth backbone for both Sentinel AI customer integrations and Life Admin onboarding.

- **`GitHubProvider`** (`src/connections/providers/github-provider.ts`) — GitHub
  OAuth 2.0 for repositories, organisations, notifications, and GitHub Packages.

  ```ts
  import { ConnectionManager, GitHubProvider, GITHUB_SCOPES } from "@bw-advisory/agent-kit/connections";

  const { authorizationUrl } = await manager.connect(operatorId, "github", {
    scopes: GITHUB_SCOPES.READ_WRITE,
  });
  ```

  - `GitHubScopes` — 8 named constants: `REPO`, `PUBLIC_REPO`, `READ_USER`,
    `USER_EMAIL`, `READ_ORG`, `NOTIFICATIONS`, `WRITE_PACKAGES`, `READ_PACKAGES`
  - `GITHUB_SCOPES` — 3 pre-composed groupings: `READ_ONLY`, `READ_WRITE`, `FULL_DEV`
  - Classic GitHub OAuth App tokens are long-lived (no expiry, no refresh). GitHub
    Apps with "Expiring user tokens" enabled issue tokens that expire after 8 hours;
    the provider handles both cases transparently via `expires_in` detection.
  - `revokeAccess()` warns and resolves (revocation requires client credentials not
    available in the provider method signature — users revoke via GitHub settings).
  - `fetchUserInfo()` calls `GET https://api.github.com/user` with the required
    `Accept: application/vnd.github+json` and `X-GitHub-Api-Version: 2022-11-28` headers.
  - Required env vars: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`

- **`LinearProvider`** (`src/connections/providers/linear-provider.ts`) — Linear
  OAuth 2.0 for issues, projects, teams, cycles, and workspace management.

  ```ts
  import { ConnectionManager, LinearProvider, LINEAR_SCOPES } from "@bw-advisory/agent-kit/connections";

  const { authorizationUrl } = await manager.connect(operatorId, "linear", {
    scopes: LINEAR_SCOPES.READ_WRITE,
  });
  ```

  - `LinearScopes` — 5 named constants: `READ`, `WRITE`, `ISSUES_CREATE`,
    `COMMENTS_CREATE`, `ADMIN`
  - `LINEAR_SCOPES` — 3 pre-composed groupings: `READ_ONLY`, `READ_WRITE`, `ADMIN`
  - Supports refresh tokens (standard `grant_type=refresh_token` flow).
  - `revokeAccess()` calls `POST https://api.linear.app/oauth/revoke` with the
    access token in the request body. No client credentials required.
  - `fetchUserInfo()` issues a GraphQL `viewer` query to `https://api.linear.app/graphql`,
    returning `id`, `name`, `email`, `displayName`, and `avatarUrl`.
  - Linear scopes are comma-separated in URL and token response (provider normalises).
  - Required env vars: `LINEAR_CLIENT_ID`, `LINEAR_CLIENT_SECRET`

- **`AtlassianProvider`** (`src/connections/providers/atlassian-provider.ts`) —
  Atlassian OAuth 2.0 (3LO) covering Jira and Confluence through a single connection.

  ```ts
  import { ConnectionManager, AtlassianProvider, ATLASSIAN_SCOPES } from "@bw-advisory/agent-kit/connections";

  const { authorizationUrl } = await manager.connect(operatorId, "atlassian", {
    scopes: ATLASSIAN_SCOPES.JIRA_FULL,
  });
  ```

  - `AtlassianScopes` — 6 named constants: `READ_ME`, `READ_JIRA_WORK`,
    `WRITE_JIRA_WORK`, `READ_CONFLUENCE_CONTENT`, `WRITE_CONFLUENCE_CONTENT`,
    `OFFLINE_ACCESS`
  - `ATLASSIAN_SCOPES` — 4 pre-composed groupings: `JIRA_READ_ONLY`, `JIRA_FULL`,
    `CONFLUENCE_FULL`, `FULL_ALL`
  - Authorization URL includes `audience=api.atlassian.com` and `prompt=consent`
    (required by Atlassian's 3LO flow; `prompt=consent` ensures a refresh token
    is always returned on re-authorization).
  - Access tokens expire after 1 hour. Always include `AtlassianScopes.OFFLINE_ACCESS`
    (or use any pre-composed grouping) when token refresh is needed.
  - `revokeAccess()` warns and resolves (Atlassian revocation requires identity that
    is not available in the provider method — users revoke via id.atlassian.com).
  - `fetchUserInfo()` calls `GET https://api.atlassian.com/me`.
  - Required env vars: `ATLASSIAN_CLIENT_ID`, `ATLASSIAN_CLIENT_SECRET`

### Changed

- `src/connections/index.ts` — exports `GitHubProvider`, `GitHubScopes`,
  `GITHUB_SCOPES`, `LinearProvider`, `LinearScopes`, `LINEAR_SCOPES`,
  `AtlassianProvider`, `AtlassianScopes`, `ATLASSIAN_SCOPES`

### Tests

- `tests/connections.test.ts` — 212 tests total (up from ~145). New describe blocks:
  `GitHubProvider` (buildAuthorizationUrl, exchangeCode, refreshAccessToken,
  revokeAccess, fetchUserInfo, scope constants), `LinearProvider` (same + GraphQL
  error paths), `AtlassianProvider` (same + audience/prompt URL parameters,
  message-field error path). All 556 suite tests pass.

---

## [0.19.0] — 2026-05-27

### Added — MCP Integration Layer (F2 Foundation)

New module `src/mcp/` — the Model Context Protocol integration substrate for
agent-kit. MCP is the open standard that lets LLM applications connect to external
data sources and tools through a common JSON-RPC 2.0 protocol. As vendors adopt it,
each MCP-native service becomes available to agent-kit without any bespoke OAuth
or API wrappers — a 3–6× reduction in integration cost over F1-style Connections.

- **`MCPClient`** (`src/mcp/client.ts`) — lifecycle wrapper around the
  `@modelcontextprotocol/sdk` Client. Single stable surface for connecting to MCP
  servers over either transport. Seven public methods:

  ```ts
  const client = new MCPClient({
    name: "sentinel-ai",
    version: "1.0.0",
    transport: { type: "stdio", command: "npx", args: ["-y", "@my-org/mcp-server"] },
  });
  await client.connect();
  const tools = await client.listTools();
  const result = await client.callTool("read_file", { path: "/workspace/README.md" });
  const resources = await client.listResources();
  const content = await client.readResource("file:///workspace/README.md");
  const prompts = await client.listPrompts();
  await client.close();
  ```

  `connect()` is idempotent (second call is a no-op). `close()` is safe to call
  multiple times or before `connect()`. `connected` getter reflects current state.

- **Stdio transport** (`src/mcp/transport-stdio.ts`) — spawns the MCP server as a
  child process. Communicates over stdin/stdout with newline-delimited JSON-RPC.
  Inherits the current process's environment; optional `env` overlay for secrets.

- **HTTP+SSE transport** (`src/mcp/transport-http.ts`) — connects to a remote MCP
  server via HTTP POST + Server-Sent Events. `headers` config for bearer-token
  auth:

  ```ts
  const client = new MCPClient({
    name: "my-agent",
    version: "1.0.0",
    transport: {
      type: "http",
      url: "https://mcp.example.com/endpoint",
      headers: { Authorization: `Bearer ${process.env.MCP_API_KEY}` },
    },
  });
  ```

- **`ToolRegistryBridge`** (`src/mcp/tool-registry-bridge.ts`) — the seam between
  MCP and agent-kit's native tool system. Wraps a `ToolRegistry`, discovers tools
  from one or more connected MCP clients, and produces a merged
  `toAnthropicTools()` where each MCP tool is presented to Claude using the
  server's own JSON Schema (no Zod round-trip, so Claude gets accurate parameter
  descriptions). Overwrite semantics on duplicate tool names (last-registered wins)
  match hot-reload scenarios where an MCP server updates its tool list.

  ```ts
  const bridge = new ToolRegistryBridge(registry);
  const count = await bridge.registerMcpServer(client); // returns # tools registered
  const allTools = bridge.toAnthropicTools(); // native + MCP merged
  bridge.hasMcpTool("read_file"); // → true
  bridge.mcpToolNames(); // → ["read_file", "list_directory", ...]
  ```

- **`dispatchMcpToolUse`** — routes a Claude `tool_use` block to the correct MCP
  server and returns a `tool_result` block. Catches transport-level errors and
  returns them as `is_error: true` blocks (non-throwing), matching the behaviour
  of the native `dispatchToolUse` helper so Claude can recover gracefully:

  ```ts
  if (bridge.hasMcpTool(block.name)) {
    const result = await dispatchMcpToolUse(bridge, block);
    messages.push({ role: "user", content: [result] });
  }
  ```

- **Four named error classes** (`src/mcp/errors.ts`):
  - `MCPNotConnectedError` — method called before `connect()`; carries `clientName`
  - `MCPConnectionError` — handshake failed; carries `clientName` + `cause`
  - `MCPToolCallError` — tool invocation failed at transport/RPC layer; carries `toolName` + `clientName`
  - `MCPResourceError` — resource read failed; carries `uri` + `clientName`

### Package surface

- New subpath export: `@bw-advisory/agent-kit/mcp`
- Top-level re-export as `Mcp` namespace: `import { Mcp } from "@bw-advisory/agent-kit"`
- New peer dependency: `@modelcontextprotocol/sdk ^1.0.0` (optional — only required when
  using the `mcp` module)

### SDK decision: use official `@modelcontextprotocol/sdk`, not a minimal client

The official `@modelcontextprotocol/sdk` (v1.29.0 at time of writing, 50K+ weekly
downloads) handles JSON-RPC 2.0 framing, capability negotiation, and transport
management. Rather than reimplementing the protocol, we wrap the SDK's `Client`
class behind a stable agent-kit surface. The SDK is a peer dependency (listed in
`peerDependenciesMeta` as optional) so consumers who don't use the MCP module
don't pay the install cost. tsup's `external` list ensures the SDK is never
bundled into the `dist/` output.

### Internal

- 53 unit tests in `tests/mcp.test.ts` covering:
  `MCPClient` construction + connect (stdio, HTTP, idempotent, connection failure,
  error fields); `listTools` (normalised output, empty, minimal tool);
  `callTool` (SDK invocation, success content, isError passthrough, transport error,
  error fields); `listResources` (normalised output, empty); `readResource`
  (content passthrough, transport error, error fields); `close` (disconnect,
  double-close safe, pre-connect safe); all four error class fields;
  `ToolRegistryBridge` registerMcpServer (discovery, tool count, empty server,
  last-registered wins); hasMcpTool; mcpToolNames; toAnthropicTools (merge,
  server-side JSON Schema passthrough, native-only, empty); callMcpTool (dispatch,
  unregistered error); registry accessor;
  `dispatchMcpToolUse` (tool_use_id passthrough, text extraction, multi-block
  concatenation, isError flag, transport error non-throwing, null input).

### Changed

- `package.json` — `0.17.0` → `0.19.0` (0.18.0 reserved for F1 Batch 4 if that
  ships first; this PR targets 0.19.0 to avoid version collision).
  `./mcp` subpath export added; `@modelcontextprotocol/sdk ^1.0.0` added as
  optional peer dependency.
- `tsup.config.ts` — `mcp/index` entry point added;
  `@modelcontextprotocol/sdk` added to `external` list.
- `src/index.ts` — `Mcp` namespace re-exported from root.
- `README.md` — ten modules table; `MCP` section with code example.
- `ROADMAP.md` — F2 Foundation marked shipped; F2.x extensions signposted in
  Active table.

### Why this matters

Every Sentinel AI vendor integration today is either bespoke OAuth (F1 Connections)
or direct API calls. MCP changes that: as vendors adopt the protocol,
`registerMcpServer(client)` replaces weeks of provider code. The 3–6× cost
reduction estimate comes from eliminating the auth handshake, token lifecycle,
and schema-wrangling that F1 providers require. F2 Foundation ships the client
and the bridge; serving agent-kit capabilities as an MCP server (so Sentinel AI
specialists become MCP endpoints themselves) is the next natural extension.


## [0.18.0] — 2026-05-27

### Added — Connections Module F1 Batch 4 (partial): HubSpot + Xero + Notion

Three new OAuth providers that follow the same `Provider` interface contract
established by Google (B1), Microsoft 365, and Slack (B3). GitHub, Linear,
and Atlassian remain queued for Batch 4b.

#### `HubSpotProvider` (`src/connections/providers/hubspot-provider.ts`)

HubSpot CRM OAuth 2.0 Authorization Code grant. Standard body-encoded
credentials (not Basic Auth). Access tokens expire in 30 minutes; refresh
tokens are long-lived and do not rotate.

- `HubSpotScopes` — named constants for the seven supported CRM scopes
  (`crm.objects.contacts.read/write`, `crm.objects.deals.read/write`,
  `crm.objects.companies.read/write`, `oauth`)
- `HUBSPOT_SCOPES.READ_ONLY` — read contacts, deals, companies + oauth
- `HUBSPOT_SCOPES.READ_WRITE` — read/write contacts, deals, companies + oauth
- `HUBSPOT_SCOPES.FULL_CRM` — full CRM read/write (extend as more object
  types are added)
- `revokeAccess` — warning-only (HubSpot has no token revocation endpoint)
- `fetchUserInfo` — calls HubSpot's token introspection endpoint at
  `api.hubapi.com/oauth/v1/access-tokens/{token}` (no Auth header needed)

#### `XeroProvider` (`src/connections/providers/xero-provider.ts`)

Xero OAuth 2.0 for confidential clients. Basic Auth credentials in the
Authorization header for token exchange and refresh. Access tokens expire in
30 minutes. **Refresh tokens rotate on every use** — the old token is
immediately invalidated; ConnectionManager writes the new one back to the
store automatically via `updateTokens`.

- `XeroScopes` — nine named constants covering identity and all standard
  accounting scopes (`accounting.transactions`, `.settings`, `.contacts`,
  `.attachments`, `.reports.read`, plus OpenID scopes)
- `XERO_SCOPES.IDENTITY_ONLY` — openid + profile + email
- `XERO_SCOPES.READ_ONLY_ACCOUNTING` — identity + offline_access +
  `accounting.reports.read`
- `XERO_SCOPES.FULL_ACCOUNTING` — identity + offline_access + all accounting
  scopes
- `revokeAccess` — calls `identity.xero.com/connect/revocation`
- `fetchUserInfo` — calls `identity.xero.com/connect/userinfo` (OpenID Connect)

#### `NotionProvider` (`src/connections/providers/notion-provider.ts`)

Notion OAuth 2.0. Two notable quirks documented in the provider:

1. **JSON token exchange body** — Notion's token endpoint expects
   `Content-Type: application/json` with Basic Auth credentials in the header,
   unlike every other provider that uses `application/x-www-form-urlencoded`.
2. **No refresh tokens / no expiry** — Notion issues permanent access tokens.
   `expiresAt` is always `undefined`; ConnectionManager never attempts refresh.
   `refreshAccessToken` throws a clear error if called unexpectedly.
3. **Workspace-level grants, no granular scopes** — pass an empty scope list
   or omit it; Notion ignores the `scope` param entirely.

- `revokeAccess` — warning-only (Notion has no revocation API endpoint)
- `fetchUserInfo` — calls `api.notion.com/v1/users/me` with
  `Notion-Version: 2022-06-28` header

### Changed

- `src/connections/index.ts` — exports `HubSpotProvider`, `HubSpotScopes`,
  `HUBSPOT_SCOPES`, `XeroProvider`, `XeroScopes`, `XERO_SCOPES`,
  `NotionProvider`; barrel comment updated to reflect Batch 4 additions and
  Batch 4b queue
- `src/connections/README.md` — supported providers table updated; scope
  bundle reference sections for HubSpot and Xero; full setup guides for all
  three new providers; deferred-features table updated (GitHub, Linear,
  Atlassian → Batch 4b)
- `README.md` — version badge → v0.18.0; Connections module version range →
  v0.11–v0.18; env vars table adds `HUBSPOT_*`, `XERO_*`, `NOTION_*`
- `ROADMAP.md` — F1 Batch 4 row split into 4 (partial, shipped) and 4b
  (GitHub + Linear + Atlassian, queued)
- `package.json` — `0.17.0` → `0.18.0`

### Tests

`tests/connections.test.ts` — 56 new test cases across three new provider
suites:

| Suite | Cases |
|---|---|
| HubSpotProvider — buildAuthorizationUrl | 3 |
| HubSpotProvider — exchangeCode | 4 |
| HubSpotProvider — refreshAccessToken | 2 |
| HubSpotProvider — revokeAccess | 1 |
| HubSpotProvider — fetchUserInfo | 2 |
| HubSpotScopes constants | 2 |
| HUBSPOT_SCOPES groupings | 3 |
| XeroProvider — buildAuthorizationUrl | 3 |
| XeroProvider — exchangeCode | 3 |
| XeroProvider — refreshAccessToken | 3 |
| XeroProvider — revokeAccess | 2 |
| XeroProvider — fetchUserInfo | 2 |
| XeroScopes constants | 2 |
| XERO_SCOPES groupings | 3 |
| NotionProvider — buildAuthorizationUrl | 3 |
| NotionProvider — exchangeCode | 4 |
| NotionProvider — refreshAccessToken | 1 |
| NotionProvider — revokeAccess | 1 |
| NotionProvider — fetchUserInfo | 3 |

### Required env vars (new)

Consumers who want to use the new providers must create OAuth apps and
configure these environment variables:

| Variable | Provider | Where to get it |
|---|---|---|
| `HUBSPOT_CLIENT_ID` | HubSpotProvider | [developers.hubspot.com](https://developers.hubspot.com) → App settings |
| `HUBSPOT_CLIENT_SECRET` | HubSpotProvider | [developers.hubspot.com](https://developers.hubspot.com) → App settings |
| `XERO_CLIENT_ID` | XeroProvider | [developer.xero.com/app/manage](https://developer.xero.com/app/manage) |
| `XERO_CLIENT_SECRET` | XeroProvider | [developer.xero.com/app/manage](https://developer.xero.com/app/manage) |
| `NOTION_CLIENT_ID` | NotionProvider | [notion.so/profile/integrations](https://www.notion.so/profile/integrations) |
| `NOTION_CLIENT_SECRET` | NotionProvider | [notion.so/profile/integrations](https://www.notion.so/profile/integrations) |

---

## [0.17.0] — 2026-05-27

### Added — Proactive Surfacing Module (F0.10 Batch 3 deltas)

Follow-up to the F0.10 B3 release (v0.12.0) that closes three gaps in the
original spec. Assumes F1 B2 lands as v0.16.0 first; this entry targets
0.17.0 to leave that slot open.

- `RateLimiter` (`src/proactive-surfacing/rate-limiter.ts`) — standalone
  rate-limit primitive split out of `Debouncer`. Per-source overrides
  layered over global defaults:

  ```typescript
  new RateLimiter(store, {
    default: { maxPerHour: 5, maxPerDay: 20 },
    perSource: { intel: { maxPerHour: 2 }, pipeline: { maxPerDay: 50 } },
  });
  ```

  `Debouncer` now composes a `RateLimiter` internally for its hourly/daily
  guards. Behaviour and public surface unchanged — existing callers do not
  need to touch their code.

- `PrismaDebounceStore` (`src/proactive-surfacing/debounce-store-prisma.ts`) —
  cross-process `DebounceStore` backed by Prisma. Required when web + worker
  + cron share quota state; the in-memory store isolates per-process and
  breaks rate limits in multi-process deployments. Accepts a structural
  `SurfacingPrismaClient` so the kit stays vendor-agnostic at the type
  level. Originally deferred to Batch 5 in the README — pulled forward.

- `src/proactive-surfacing/schema.prisma.example` — required Prisma model
  for `PrismaDebounceStore`:

  ```prisma
  model SurfacingEmissionLog {
    id         String   @id @default(cuid())
    operatorId String
    sourceName String
    emittedAt  DateTime @default(now())
    @@index([operatorId, sourceName, emittedAt])
  }
  ```

  Bundled in `package.json` `files` so consumers can copy from
  `node_modules`. Auto-prune is the consumer's responsibility — see the
  example file for the one-line `deleteMany` query.

- `LLMAugmentedRelevanceScorer` result cache — scored results are now
  cached for 60 seconds per `(operatorId, sourceName, candidate.id)`
  triple. Re-scoring the same candidate within the window does not
  re-invoke the LLM (or even the base scorer). Cache TTL configurable via
  a 5th constructor argument:

  ```typescript
  new LLMAugmentedRelevanceScorer(
    base, anthropic, "claude-sonnet-4-6",
    { lower: 0.4, upper: 0.75 },
    120_000,  // 2-minute TTL
  );
  ```

### Internal

- New exports: `RateLimiter`, `RateLimiterConfig`, `RateLimiterPerSourceConfig`,
  `PrismaDebounceStore`, `SurfacingPrismaClient`.
- Test coverage added in `tests/proactive-surfacing.test.ts`:
  `RateLimiter` (7 cases — defaults, per-source override, partial override,
  hourly + daily independence, isolation per operator/source, built-in
  defaults); `PrismaDebounceStore` (4 cases — write, window count,
  isolation, Debouncer integration); `LLMAugmentedRelevanceScorer caching`
  (5 cases — TTL hit, LLM-skipped path cached, TTL expiry, no cross-operator
  collision, no cross-candidate collision).

### Changed

- `package.json` — `0.15.0` → `0.17.0` (0.16.0 reserved for F1 Batch 2 if it
  ships first; this PR targets 0.17.0 to avoid version collision).
  `src/proactive-surfacing/schema.prisma.example` added to `files`.
- `src/proactive-surfacing/index.ts` — exports new primitives.
- `src/proactive-surfacing/README.md` — new `RateLimiter` and
  `PrismaDebounceStore` sections; LLM scorer section documents the cache TTL
  argument; deferred-features table marks Prisma-backed store as shipped.
- `src/proactive-surfacing/debouncer.ts` — refactored to compose
  `RateLimiter`. No public-surface change.

### Why this matters

Three concrete production gaps in F0.10 B3:

1. **Multi-process deployments need a shared store.** The Sentinel AI
   deployment topology (web + queue worker + scheduler) was about to hit
   per-process quotas that drifted between replicas. `PrismaDebounceStore`
   fixes that without a Redis dependency.
2. **Per-source quotas need per-source config.** A chatty intel source and a
   slow-burning pipeline source can't share a single global cap. The
   `perSource` config in `RateLimiter` lets each source breathe at its own
   rate.
3. **LLM scoring is the most expensive step.** A pipeline run that scores
   the same candidate twice (retry, debug, secondary path) was paying for
   Sonnet calls twice. The 60-second cache eliminates that without
   complicating the calling code.

## [0.15.0] — 2026-05-27

### Added — Connections Module (F1.x Batch 3): Microsoft 365 + Slack providers

- `MicrosoftGraphProvider` — Microsoft 365 / Microsoft Graph OAuth 2.0 provider.
  Supports Outlook Mail, Calendar, OneDrive, Teams, Contacts, Presence, and any
  other service accessible via Microsoft Graph.

  - **Tenant configuration via constructor** — pass `tenantId` to
    `new MicrosoftGraphProvider({ tenantId })`. Defaults to `"common"` (accepts
    both personal Microsoft accounts and any M365 tenant). Override to
    `"organizations"`, `"consumers"`, or a specific Azure AD tenant GUID for
    locked deployments. Tenant is embedded in the authorization and token endpoint
    URLs as `https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/*`.

  - `buildAuthorizationUrl` — includes `response_type=code`, `response_mode=query`,
    and space-separated scopes. State param round-tripped for CSRF protection.

  - `exchangeCode` / `refreshAccessToken` — POST to the tenant-scoped token endpoint
    with `grant_type=authorization_code` / `grant_type=refresh_token`. Parses
    `access_token`, `refresh_token`, `expires_in`, `scope`, `token_type`.

  - `revokeAccess` — no-op with a console warning. Microsoft does not expose a
    self-service token revocation endpoint for delegated permissions; the local
    Connection record is deleted by ConnectionManager regardless. Users can remove
    app access at https://myapps.microsoft.com.

  - `fetchUserInfo` — GET `https://graph.microsoft.com/v1.0/me`. Returns
    `userPrincipalName`, `displayName`, `mail`, `id`.

- `MicrosoftGraphScopes` — named constants for 13 common Microsoft Graph
  permission strings: `USER_READ`, `MAIL_READ`, `MAIL_SEND`, `MAIL_READWRITE`,
  `CALENDARS_READ`, `CALENDARS_READWRITE`, `FILES_READ`, `FILES_READWRITE`,
  `CONTACTS_READ`, `PRESENCE_READ`, `TEAMS_READ`, `CHAT_READ`, `OFFLINE_ACCESS`.
  Always include `OFFLINE_ACCESS` — Microsoft requires it to be explicitly
  requested to issue a refresh_token.

- `SlackProvider` — Slack OAuth v2 provider.
  Supports posting messages, reading channels, accessing DMs, and any other
  operation available via Slack's Web API.

  - `buildAuthorizationUrl` — redirects to `https://slack.com/oauth/v2/authorize`
    with bot scopes in the `scope` param (space-separated). Documented: to request
    user scopes, split into `scope` + `user_scope` params (not yet implemented
    in this provider — extend `buildAuthorizationUrl` if needed).

  - `exchangeCode` — POST to `https://slack.com/api/oauth.v2.access` with
    HTTP Basic Auth (clientId:clientSecret). Slack's response wraps the bot
    token in `access_token` and user info in `authed_user`. The raw response is
    stored in `TokenResponse.raw` so consumers can extract the user token if needed.

  - `refreshAccessToken` — works only when **Token Rotation** is enabled in the
    Slack app's OAuth & Permissions settings. When rotation is not enabled, Slack
    tokens do not expire and this method is never called (no `expiresAt` → no
    refresh window). If called without rotation: throws a clear error explaining
    how to enable rotation. Specific Slack error codes (`token_rotation_not_enabled`,
    `invalid_refresh_token`) produce actionable messages.

  - `revokeAccess` — POST `https://slack.com/api/auth.revoke` with the token
    in the request body. Throws on `ok: false`.

  - `fetchUserInfo` — POST `https://slack.com/api/auth.test` with a Bearer
    header. Returns `team`, `user`, `user_id`, `team_id`, `bot_id`.

  - **Scope parsing** — Slack returns scopes as a comma-separated string
    (not space-separated like other OAuth providers). Parsed correctly into
    `TokenResponse.scopes`.

  - **`expiresAt` semantics** — when rotation is disabled (the default),
    `expires_in` is absent from the token response and `expiresAt` is left
    `undefined`. ConnectionManager treats tokens without `expiresAt` as
    non-expiring and never attempts refresh. When rotation is enabled,
    `expires_in` is set (typically 43200 seconds / 12 hours) and
    `expiresAt` is calculated accordingly.

- `SlackScopes` — named constants for 13 common Slack bot scope strings:
  `CHAT_WRITE`, `CHAT_WRITE_PUBLIC`, `CHANNELS_READ`, `CHANNELS_HISTORY`,
  `GROUPS_READ`, `GROUPS_HISTORY`, `IM_READ`, `IM_HISTORY`, `USERS_READ`,
  `USERS_READ_EMAIL`, `TEAM_READ`, `FILES_READ`, `FILES_WRITE`.

### Internal

- ~30 new tests in `tests/connections.test.ts` covering:
  `MicrosoftGraphProvider` — URL construction (tenant embedded, scopes, state,
  response_mode, redirect_uri), exchangeCode success + error paths, refreshAccessToken
  success + error paths, revokeAccess no-op warning, fetchUserInfo success + error
  paths, custom tenant configuration, MicrosoftGraphScopes constants accessibility;
  `SlackProvider` — URL construction (scopes, state, redirect_uri), exchangeCode
  success + ok-false error + missing access_token, refreshAccessToken with rotation
  enabled + rotation disabled (clear error), revokeAccess success + error,
  fetchUserInfo success + error, comma-separated scope parsing, expiresAt set when
  rotation enabled / undefined when not, SlackScopes constants accessibility.

### Changed

- `package.json` — `0.13.0` → `0.15.0` (0.14.0 reserved for F0.10 B4 if that
  ships first; this PR targets 0.15.0 to avoid version collision).
- `src/connections/index.ts` — exports `MicrosoftGraphProvider`,
  `MicrosoftGraphScopes`, `SlackProvider`, `SlackScopes`.
- `src/connections/README.md` — added Microsoft 365 and Slack setup sections,
  scope bundle tables, and common gotchas section.

### Why this matters

Two of the three highest-frequency integrations for both Sentinel AI (M365 is the
dominant SMB productivity suite) and Life Admin (Slack for family/team comms).
With Google (B1), Microsoft 365, and Slack (B3) shipped, the most-requested
provider coverage is complete. HubSpot, Xero, GitHub, Notion, Linear, and
Atlassian follow in subsequent batches.

## [0.14.0] — 2026-05-27

### Added — Proactive Surfacing Module (F0.10 Batch 4): Dashboard channel + Operator Preferences

- `DashboardSurfacingChannel` (`src/proactive-surfacing/surfacing-channel-dashboard.ts`) —
  second `SurfacingChannel` implementation alongside `TelegramSurfacingChannel`.
  Instead of calling Telegram, it serialises a scored candidate into a
  `SerializedSurfacingMessage` and enqueues it on a `SurfacingMessageQueue`.
  Consuming web apps (Command Centre, FrankiAI portal) read from the queue to
  render an operator inbox. CTA buttons use the same `ps_<action>|<shortId>`
  callback format as Telegram so a single callback handler can service both
  surfaces.

- `SurfacingMessageQueue` interface — three methods: `enqueue`, `list`,
  `markRead`. Production deployments back this with Prisma; the
  `SurfacingMessage` Prisma model is documented in
  `src/proactive-surfacing/README.md`. The bundled
  `InMemorySurfacingMessageQueue` is for tests and local dev.

- `InMemoryOperatorPreferencesStore` (`src/proactive-surfacing/operator-preferences.ts`) —
  first implementation of `OperatorPreferencesStore`. Stores per-operator
  overrides that the pipeline applies automatically before each run:

  - `scoreThreshold` — replaces the pipeline default for this operator
  - `preferredChannels` — replaces `operatorContext.preferredChannels`
  - `perSourceCalibration.enabled = false` — silently skips a source
  - `perSourceCalibration.multiplier` — `effectiveScore = originalScore × multiplier`, clamped to `[0, 1]`

  `debounceConfig` and `quietHours` are stored but require a
  preference-aware `Debouncer` wrapper to apply per-operator (deferred to a
  later batch).

- `OperatorSurfacingPreferences` Prisma model documented in
  `src/proactive-surfacing/README.md`.

### Changed

- `package.json` — `0.13.0` → `0.14.0`.
- `src/proactive-surfacing/index.ts` — exports `DashboardSurfacingChannel`,
  `InMemorySurfacingMessageQueue`, `SurfacingMessageQueue`,
  `SerializedSurfacingMessage`, `OperatorPreferences`,
  `OperatorPreferencesStore`, `InMemoryOperatorPreferencesStore`.

### Why this matters

The Telegram channel alone is not enough for portal-shaped consumers. The
FrankiAI family wealth portal and the Command Centre operator dashboard
both need a queue they can read from on every page render. The
`SurfacingMessageQueue` interface lets each consumer back it with whatever
durable store they already use (Prisma in both current consumers), while
the pipeline stays vendor-agnostic. Operator Preferences move per-operator
tuning out of agent code and into a record the operator (or their
administrator) can edit.

## [0.13.0] — 2026-05-26

### Added — Connections Module (F1.x Batch 2): Prisma store + AES-256-GCM token encryption

- `PrismaConnectionStore` (`src/connections/connection-store-prisma.ts`) —
  production `ConnectionStore` implementation backed by Prisma. Replaces
  `MemoryConnectionStore` for any deployment that needs persistence across
  process restarts. Accepts a structural Prisma client at construction so
  the kit stays vendor-agnostic at the type level — consumers pass their
  own `PrismaClient`.

- `ConnectionCrypto` (`src/connections/crypto.ts`) — authenticated
  encryption primitive used by `PrismaConnectionStore`. **AES-256-GCM** with
  the 256-bit key derived via `scrypt` from
  `CONNECTION_ENCRYPTION_KEY`, a 96-bit random IV per `encryptToken()`
  call, and a 128-bit auth tag stored alongside the ciphertext. Only
  `accessToken` and `refreshToken` are encrypted; `operatorId`,
  `expiresAt`, `scopes`, and `metadata` remain plaintext for indexing
  and debugging.

- `src/connections/schema.prisma.example` — the required `Connection`
  Prisma model with the encrypted-field columns
  (`encryptedAccessToken`, `accessTokenIv`, `accessTokenTag`, plus the
  nullable refresh-token equivalents) and the
  `@@unique([operatorId, providerId])` constraint. Bundled in
  `package.json` `files` so consumers can copy from `node_modules`.

### Changed

- `package.json` — `0.11.0` → `0.13.0` (0.12.0 was consumed by F0.10 B3).
- `src/connections/index.ts` — exports `PrismaConnectionStore`,
  `ConnectionCrypto`.
- `src/connections/README.md` — added the production-setup walkthrough
  (Connection model copy, `CONNECTION_ENCRYPTION_KEY` generation via
  `openssl rand -base64 32`, key-rotation caveat), the encryption-details
  table, and the deferred-features roster.

### Why this matters

Token storage is the load-bearing piece of every OAuth-based integration.
Storing access and refresh tokens in plaintext is the kind of
single-incident vulnerability that closes companies. AES-256-GCM with
unique IVs per write is the modern default for authenticated encryption,
and `scrypt` key derivation means the operational key (`CONNECTION_ENCRYPTION_KEY`)
can be a human-rotatable passphrase rather than a raw 32-byte secret.
Combined with the F1 B1 `ConnectionManager`, this is the first release
that is safe to point at real consumer credentials.

## [0.12.0] — 2026-05-26

### Added — Proactive Surfacing Module (F0.10 Batch 3): LLM-augmented scoring + debouncing

- `LLMAugmentedRelevanceScorer` (`src/proactive-surfacing/relevance-scorer-llm.ts`) —
  wraps any `RelevanceScorer` (typically `RuleBasedRelevanceScorer`) and adds
  a Sonnet sanity-check for candidates that land in the "uncertain"
  middle band. Candidates with base score `< 0.4` (clearly irrelevant)
  and `> 0.75` (clearly relevant) skip the LLM call entirely — no cost,
  no latency. Configurable band bounds via constructor.

- `Debouncer` + `InMemoryDebounceStore` (`src/proactive-surfacing/debouncer.ts`) —
  per-`(operatorId, sourceName)` rate-limiting to prevent floods. Enforces
  `maxPerHour`, `maxPerDay`, and optional `quietHours` (with timezone).
  Candidates with `urgency === "now"` bypass quiet hours but still
  respect hourly and daily limits.

- `SurfacingAggregator` (`src/proactive-surfacing/aggregator.ts`) — batches
  `morning_brief` and `weekly_brief` candidates into a single bundled
  message per operator per urgency, instead of sending them one at a time.
  `"now"` urgency always bypasses the aggregator. Flushes on volume
  threshold (5 items per operator+urgency) or a manual
  `aggregator.flush()` call (e.g. from a scheduled 8am routine).

- `PipelineCoachingTriggerSource` (`src/proactive-surfacing/trigger-source-example.ts`) —
  documented reference shape for a CRM-backed `TriggerSource`. Takes a
  `DealFetcher` (HubSpot, Prisma, anything) and surfaces deals that have
  been cold for longer than `coldThresholdDays`. Intended as a copy-paste
  template for consumers building their own trigger sources.

### Changed

- `package.json` — `0.11.0` → `0.12.0`.
- `src/proactive-surfacing/index.ts` — exports `LLMAugmentedRelevanceScorer`,
  `Debouncer`, `InMemoryDebounceStore`, `DebounceStore`, `SurfacingAggregator`,
  `PipelineCoachingTriggerSource`, `DealFetcher`.
- `src/proactive-surfacing/README.md` — added LLM scorer, Debouncer,
  SurfacingAggregator, and PipelineCoachingTriggerSource sections.

### Why this matters

The B1 rule-based scorer was deliberately conservative — it routes a lot
of candidates into the "maybe" band. Without an LLM sanity-check, those
either flood the operator or get filtered out by threshold and lost.
Routing only the uncertain band through Sonnet keeps the cost bounded
while removing the noise. The Debouncer + Aggregator are the production
guards that make the pipeline safe to attach to a noisy intel source
without burying the operator.

## [0.11.0] — 2026-05-26

### Added — Connections Module (F1.x Batch 1)

- `src/connections/` — new module. Vendor-agnostic OAuth orchestration.
  Define a Provider once; the framework handles authorization URL
  construction, code exchange, token storage, transparent refresh, and
  revocation. The same primitive serves SMB onboarding (Sentinel AI
  customer integrations) and consumer Life Admin product onboarding.
  Connections are the biggest barrier to high-volume adoption — F1.x
  changes that.

- `ProviderName` — open string union of well-known provider IDs
  (`"google"`, `"microsoft_365"`, `"slack"`, `"hubspot"`, `"xero"`,
  `"notion"`, `"linear"`, `"monday"`, `"github"`, `"atlassian"`) plus
  `string` tail for custom providers without a library change.

- `TokenResponse` — normalised token payload returned after code exchange
  or refresh: `accessToken`, `refreshToken?`, `expiresAt?` (absolute, not
  relative), `scopes`, `tokenType`, `raw` (vendor response for debugging).

- `Connection` — persisted operator↔provider link stored by a
  `ConnectionStore`. Fields: `id`, `providerId`, `operatorId`,
  `accessToken`, `refreshToken?`, `expiresAt?`, `scopes`, `connectedAt`,
  `lastUsedAt?`, `metadata?`.

- `ConnectionStartResult` — return type of `ConnectionManager.connect()`.
  Carries the authorization URL, a CSRF state token, and the providerId.
  The caller redirects the user and round-trips `state` through the OAuth
  callback for verification.

- `ConnectionStatus` — discriminated union (`connected: true | false`).
  Includes `expiresIn` (seconds) when the token has a known expiry.

- `Provider` interface — OAuth contract any vendor implements. Four required
  methods: `buildAuthorizationUrl`, `exchangeCode`, `refreshAccessToken`,
  and two optional: `revokeAccess`, `fetchUserInfo`. Stateless — one
  instance per provider, shared across all operators.

- `ConnectionStore` interface — persistence contract with five methods:
  `save`, `get`, `list`, `delete`, `updateTokens`. Batch 2 adds
  `PrismaConnectionStore` (PostgreSQL + AES-256-GCM token encryption at
  rest). The interface is stable — consumers can swap stores without
  touching the rest of their code.

- `ConnectionManagerConfig` — wires providers, credentials, store, and
  `defaultRedirectUri` into a single config object passed to the manager
  constructor.

- `ConnectionManager` — high-level API. Six public methods:
  - `connect(operatorId, providerId, options?)` → `ConnectionStartResult`
  - `completeConnection(operatorId, providerId, code, redirectUri?)` → `Connection`
  - `getAccessToken(operatorId, providerId)` → `string` (refreshes transparently within 5-min window)
  - `getConnectionStatus(operatorId, providerId)` → `ConnectionStatus` (non-throwing)
  - `disconnect(operatorId, providerId)` → revoke + delete (revocation failure non-fatal)
  - `listConnections(operatorId)` → `Connection[]`
  - One internal helper: `refreshIfNeeded(connection)` — refreshes when within
    `REFRESH_BUFFER_MS` (5 minutes) of expiry; retains existing refresh token
    when vendor does not return a new one.

- Four named error classes — `NoConnectionError`, `ProviderNotRegisteredError`,
  `OAuthExchangeError`, `TokenRefreshError`. All extend `Error` with a
  meaningful `name` field and a cause where applicable.

- `GoogleProvider` — reference implementation of `Provider` for Google
  OAuth 2.0. Uses `access_type=offline` and `prompt=consent` so refresh
  tokens are always issued. Implements all five methods including
  `revokeAccess` (`oauth2.googleapis.com/revoke`) and `fetchUserInfo`
  (`googleapis.com/oauth2/v2/userinfo`). Token exchange response parsed
  into `TokenResponse` with absolute `expiresAt` derived from `expires_in`.

- `GoogleScopes` — named constants for eleven common Google scope URLs:
  `CALENDAR_READ`, `CALENDAR_FULL`, `CALENDAR_EVENTS`, `GMAIL_READ`,
  `GMAIL_SEND`, `GMAIL_COMPOSE`, `DRIVE_READ`, `DRIVE_FULL`, `SHEETS`,
  `DOCS`, `TASKS`. Consumers compose scope lists without memorising URLs.

- `MemoryConnectionStore` — in-memory reference implementation of
  `ConnectionStore`. Nested `Map<operatorId, Map<providerId, Connection>>`.
  Safe for single-process use (JS event loop serialises Map operations).
  Includes `size()` and `clear()` helpers for test assertions and teardown.
  Documented: Batch 2 replaces with `PrismaConnectionStore`.

### Package surface

- New subpath export: `@bw-advisory/agent-kit/connections`
- Top-level re-export as `Connections` namespace: `import { Connections } from "@bw-advisory/agent-kit"`

### Internal

- 38 unit tests in `tests/connections.test.ts` covering:
  `MemoryConnectionStore` round-trip, list, delete, updateTokens, size/clear;
  `GoogleProvider` URL construction (scopes, state, offline params), token
  exchange parsing, refresh parsing, error propagation;
  `GoogleScopes` constants; `ConnectionManager` connect (state, scopes,
  unknown provider), completeConnection (persistence, exchange error,
  metadata, non-fatal fetchUserInfo failure), getAccessToken (valid,
  missing, refresh-triggered, no-refresh-when-fresh, refresh failure,
  no-expiry), getConnectionStatus (connected/not, expiresIn, no-expiry),
  disconnect (revoke + delete, revocation failure non-fatal, missing
  connection, provider without revokeAccess), listConnections (multi-provider,
  empty); error class names and causes.

### Why this matters

Every new Sentinel AI customer integration is currently bespoke OAuth
plumbing. F1.x makes that a one-time Provider definition. The same
`ConnectionManager` API that onboards an SMB to Google Calendar will
onboard a Life Admin consumer to Gmail. Batch 2 adds the Prisma-backed
store and token encryption. Batch 3 adds the remaining eight providers.

## [0.10.0] — 2026-05-26

### Added — Proactive Surfacing Module (F0.10 Batches 1 + 2)

- `src/proactive-surfacing/` — new module. Generic infrastructure for
  "agent notices something the operator should look at, scores it,
  and delivers it on the right channel." Build a `TriggerSource` once
  and scoring + channel delivery + calibration are free.

- Four core interfaces (`src/proactive-surfacing/interfaces.ts`):

  - `TriggerSource` — emits `CandidateEvent` objects from a data domain
    (intel scan, pipeline stall, AR overdue, school calendar). Required
    methods: `name`, `fetchCandidates(operatorContext, since)`.
  - `RelevanceScorer` — scores each candidate against the current
    `OperatorContext` and returns `{ score, urgency, rationale }`.
  - `SurfacingChannel` — formats a `ScoredCandidate` into a
    channel-specific message and delivers it. Required methods: `name`,
    `format`, `send`.
  - `CalibrationLearner` — records operator responses
    (`acted` / `dismissed` / `more_like_this` / `less_like_this`) and
    adjusts per-source multipliers.

- `CandidateEvent`, `OperatorContext`, `ScoredCandidate`,
  `OperatorResponse`, `SurfacingUrgency` — the typed contracts every
  implementation shares.

- `RuleBasedRelevanceScorer` (`src/proactive-surfacing/relevance-scorer-rules.ts`) —
  deterministic scorer that reads `payload.baseRelevance` from each
  candidate, applies the calibration learner's per-source multiplier,
  and clamps the result to `[0, 1]`. Fast, predictable, no LLM cost.

- `TelegramSurfacingChannel` (`src/proactive-surfacing/surfacing-channel-telegram.ts`) —
  first `SurfacingChannel` implementation. Formats candidates as HTML
  Telegram messages with an inline CTA keyboard
  (`✓ Acted` / `▲ More` / `▼ Less` / `✕ Dismiss`). Callback data uses
  the `ps_<action>|<shortId>` format. Includes
  `parseSurfacingCallback(callbackData)` for the symmetric receive-side
  parser.

- `ProactiveSurfacingPipeline` (`src/proactive-surfacing/proactive-surfacing-pipeline.ts`) —
  the orchestrator. `run(operatorContext, since)` fetches candidates
  from every registered `TriggerSource`, drops expired ones, scores
  them, applies the score threshold, picks the channel
  (`operatorContext.preferredChannels` → fallback `"telegram"`), and
  delivers. `recordResponse(...)` routes operator feedback into the
  `CalibrationLearner`.

- `MemoryCalibrationLearner` (`src/proactive-surfacing/calibration-learner-memory.ts`) —
  in-memory `CalibrationLearner` stub. Tracks `(operatorId, sourceName)`
  multipliers; nudges them up on `more_like_this` / `acted` and down on
  `less_like_this` / `dismissed`. Prisma-backed implementation is
  deferred to a later batch — the in-memory store is sufficient for
  tests and single-process deployments.

- `DummyTriggerSource` — canned-event source for tests and local dev.

### Package surface

- New subpath export: `@bw-advisory/agent-kit/proactive-surfacing`.
- Top-level re-export as `ProactiveSurfacing` namespace.

### Changed

- `package.json` — `0.7.0` → `0.10.0` (0.8.0 and 0.9.0 reserved during
  planning, never released — see note at top of changelog).

### Why this matters

Every BW Advisory consumer (Command Centre, FrankiAI, the future Sentinel
AI specialists) has a "morning brief" / "things you should know" surface.
Before F0.10, each consumer built that surface from scratch. F0.10 turns
it into a primitive: define a `TriggerSource` for your data domain, and
the pipeline takes care of scoring, threshold filtering, channel
delivery, and operator-feedback capture. Batches 3 and 4 add the LLM
scorer, debouncer, dashboard channel, and per-operator preferences on
top of these interfaces.

## [0.7.0] — 2026-05-22

### Added — Personality Framework

- `src/personality/` — new module codifying an agent's voice, tone,
  persona, and system prompt as a structured, composable definition.
  Same Archer (or Franki, or any commercial customer's agent) across
  every surface, with appropriate register per channel. The sixth
  foundational pattern after Memory (0.1), Telegram (0.2), Tools (0.3),
  Photo (0.4), Voice (0.5), and Approval (0.6) — and the one that
  closes out the foundational kit.
- `PersonalityDefinition` — typed identity (name / role / about),
  tone (register / energy / humor / directness), style preferences
  (sentence length, lists allowed, emoji policy, addressing,
  self-reference), vocabulary (preferred / avoid / signature phrases),
  behavioural rules (do / don't), per-channel overrides, and semver
  versioning. The shape consumers feed in once and reference everywhere.
- `personalityDefinitionSchema` — zod validator for boundary cases
  (DB load, customer-onboarding form). Inline-constructed personalities
  can use the TypeScript interface directly.
- `buildSystemPrompt` — deterministic composition of personality +
  channel + optional task + optional context + optional recent
  calibrations into a single system prompt string ready for Claude.
  Same inputs always produce the same output, which makes the builder
  testable and prompt-cache-friendly across consecutive turns.
- `applyChannelOverride` — per-surface merge. Same base personality
  renders differently on Telegram (short sentences, sparing emojis),
  email (formal register, paragraph form, no lists), dashboard
  (markdown, lists allowed), and voice (spoken-language only, no
  markdown). List-shaped fields (`doList`, `dontList`, `preferredPhrasing`,
  `avoidPhrasing`, `signaturePhrases`) concatenate-and-deduplicate; scalar
  fields are shallow-overridden. The merged result is "flat" — the
  `channels` block is stripped so accidental re-merging is impossible.
- `applyCalibrationsToPersonality` — runtime tone nudges from AgentMemory
  `CALIBRATION` records. The live link between Phase 5.5b calibration
  capture and how the agent actually sounds in its next message.
  Calibrations are applied at composition time only — they never
  permanently mutate the stored personality. Each dimension has an
  ordered scale (e.g. register goes formal → professional → casual → warm);
  `direction: "less"` / `"softer"` moves toward the low-index end,
  `"more"` / `"sharper"` toward the high-index end. Movement is clamped
  at both ends. Calibrations are processed chronologically — later
  corrections override earlier ones along the same dimension.
- `CalibrationRecord` — structural type for the AgentMemory rows the
  builder consumes. Re-declared rather than imported from `./memory` so
  the personality module stays decoupled from the memory module's
  runtime — consumers can feed in real records or hand-crafted records
  in tests.
- `makeMockPersonality` — generic "Sam" persona for tests and local
  development. Intentionally NOT Archer-shaped; agent-kit does not ship
  any consumer's actual voice content.
- Channel-specific guidance is rendered into the system prompt
  automatically — voice replies are reminded "no markdown, no lists,
  no bullet points; the response will be read aloud", email replies
  are reminded to sign off appropriately for the register, etc.
- 31 personality-suite tests covering: zod validation (semver, register
  enum, round-trip), channel override merge (telegram, email, empty
  override, missing override, list concatenation/dedup, base
  immutability, channels stripped from result), calibration nudges
  (low + high clamps, chronological ordering, verbosity → sentenceLength
  alias, emojiPolicy via softer/sharper, base immutability), and
  builder determinism, channel reflection, capabilities-hint gating,
  task-last positioning, optional section omission, and layered
  calibration rendering.

### Changed

- `src/index.ts` — `Personality` namespace re-exported from root.
- `tsup.config.ts` — added `personality/index` entry.
- `package.json` — `0.6.0` → `0.7.0`; `./personality` subpath export
  added.

### Documentation

- `README.md` — new "Personality framework" section with a code example
  showing how a consumer wires up its own `PersonalityDefinition` and
  calls `buildSystemPrompt`. Includes a callout for per-customer
  personalities — each commercial customer defines their own during
  onboarding; agent-kit provides the shape and the composition path.
- `ROADMAP.md` — F0.7 marked shipped; F0.8 (Archer refactor to consume
  agent-kit) marked next.

### Why this matters

agent-kit now provides the structured way to keep an agent's voice
consistent across every surface — same Archer on Telegram, in
dashboard chat, in email drafts, and in voice replies, with appropriate
register per channel. Without it, voice drifts between surfaces and
between releases. The calibration link makes the personality reactive —
"too formal" and "too verbose" propagate into the next message
automatically. F0.7 closes the foundational agent-kit set; F0.8 is
the moment the kit gets consumed in anger by Archer Command Centre.

## [0.6.0] — 2026-05-22

### Added — Approval Flow framework

- `src/approval/` — new module codifying the PENDING `AgentProposal`
  lifecycle that powers every Yes / Edit / Discard interaction across
  Archer today, and that FrankiAI and future agents will inherit for
  free. The fifth foundational pattern after Telegram (0.2), Tools
  (0.3), Photo (0.4), and Voice (0.5).
- `ProposalRecord<TPayload>` — typed shape carrying the action
  discriminator (`type`), status, source (`AGENT_INITIATED` /
  `WATCHITEM_FIRED` / `USER_REQUESTED`), user-facing title, optional
  rationale, structured payload, and lifecycle timestamps
  (`createdAt` / `snoozedUntil` / `completedAt` / `dismissedAt`).
  `originalPayload` is set the first time `editProposal` mutates the
  payload, preserving the agent's first attempt for diffs + audit.
- `ProposalStatus` enum — `PENDING` / `SNOOZED` / `COMPLETED` /
  `DISMISSED`.
- `ProposalStorage<TPayload>` — pluggable persistence contract.
  `create`, `get`, `update`, `listPending(agentId, userId)` (excludes
  snoozed proposals whose `snoozedUntil` is still in the future), and
  `listByStatus`. Real adapters (Prisma model in Archer, equivalent in
  FrankiAI when F0.9 lands) live in the consuming repos; agent-kit
  ships only `MemoryProposalStorage` for tests and local dev.
- Lifecycle handlers — `createProposal`, `approveProposal`,
  `snoozeProposal`, `completeProposal`, `dismissProposal`,
  `editProposal`. Each handler validates state, mutates storage, and
  writes a structured audit event. Executor errors inside
  `approveProposal` are caught and surfaced in the return value so the
  proposal stays `PENDING` and the user can retry.
- `editProposal` — Haiku-merged user corrections. Takes the user's
  free-text correction, calls Haiku 4.5 with the original payload, and
  produces a merged payload. Preserves the agent's first attempt in
  `originalPayload` across multiple edits. Strips Markdown code fences
  around Haiku's JSON output. Throws a clear error if Haiku returns
  non-JSON text.
- `ProposalAuditLogger` + `ProposalAuditEvent` — structured audit log
  written on every state transition (`CREATED`, `APPROVED`, `EDITED`,
  `SNOOZED`, `COMPLETED`, `DISMISSED`, `EXECUTOR_FAILED`). Consumers
  wire this to `writeEpisode` from `@bw-advisory/agent-kit/memory` or
  any equivalent sink. Audit failures never block the lifecycle path —
  they're logged via `ctx.log.error` and swallowed.
- `MemoryProposalAuditLogger` — in-memory implementation for tests.
- Telegram approval UI helpers — `buildApprovalMessage(proposal, opts)`
  renders a title + rationale + Yes / Edit / Discard inline keyboard;
  `parseApprovalCallback(callbackData)` is the symmetric receive-side
  parser. Reuses the existing `createApprovalKeyboard` from the F0.2
  telegram module so callback_data formats stay consistent.
- `ProposalStateError` — distinguishable from generic `Error` so
  callers can map "already approved" / "not found" / "in wrong state"
  to a friendly UI response.

### Package surface

- New subpath export: `@bw-advisory/agent-kit/approval`.
- Top-level entry re-exports the approval module as the `Approval`
  namespace alongside `Telegram`, `Tools`, `Photo`, and `Voice`.

### Internal

- Nineteen unit tests covering: PENDING create + listPending isolation
  by agent/user, approve runs executor and writes COMPLETED audit,
  executor failure leaves PENDING and writes EXECUTOR_FAILED, snooze
  filters listPending until time advances, complete without executor,
  dismiss with reason, edit merges via Haiku and preserves
  originalPayload across multiple edits, code-fence stripping, non-JSON
  Haiku output throws clearly, approval message renders the keyboard,
  callback parser accepts all three actions and rejects malformed
  input, listByStatus orders newest-first and respects the limit.

## [0.5.0] — 2026-05-22

### Added — Voice Ingress framework

- `src/voice/` — new module exposing the generic ingest → persist → STT
  → typed-Transcript pattern. The fourth foundational pattern after
  Telegram (0.2), Tools (0.3), and Photo (0.4). Consumed by Archer
  (Telegram voice memos for transactions / inbox dictation) and
  FrankiAI (voice queries about wealth / family scheduling).
- `VoiceSource` — channel-agnostic ingestion shape mirroring
  `PhotoSource`. `channel` + `externalId` + `fetchBytes()` + optional
  `mimeType` / `durationSec` / `caption`. Telegram voice, browser
  upload, and future call-recording ingresses all implement this in
  their own repos.
- `VoiceStorage` — pluggable durable-storage interface. `persist(source,
  opts) → { url, key }`. Real adapters (Google Cloud Storage, Vercel
  Blob, S3) live in the consuming repos; agent-kit ships only the
  in-memory adapter (`MemoryVoiceStorage`). Same shape as `PhotoStorage`
  so a single backing-store implementation can satisfy both interfaces.
- `STTProvider` — provider abstraction with a `name` and `transcribe()`.
  Real providers (Google Cloud Speech, Whisper, Anthropic voice when
  GA) implement this in the consuming repos so the kit stays free of
  heavy SDK dependencies — same approach the photo module takes for
  the Anthropic vision client.
- `MockSTTProvider` — scriptable deterministic provider for tests and
  local development. Accepts a fixed `result` or a `queue` of results,
  records every call, and returns a sensible empty-transcript default
  when no script is supplied. Doubly useful: removes the need to burn
  real STT quota during local dev.
- `Transcript` — return shape from STT. `text` + `segments[]` +
  `duration` + `language` + overall `confidence` + `audioUrl` +
  `audioKey`. `TranscriptSegment` carries `start` / `end` / `text` /
  `confidence` and an optional `speaker` label when diarization is on.
- `processVoice({ source, storage, stt, language, enableSpeakerDiarization,
  expectedSpeakers, ctx })` — orchestrator entry point. Fetches bytes
  once, then runs `storage.persist` and `stt.transcribe` in parallel.
  Returns a `Transcript` with `audioUrl` and `audioKey` filled in.
  STT errors bubble so the caller can wrap with a resilience layer.
- `DEFAULT_LANGUAGE` (`'en-AU'`) and `DEFAULT_MIME` (`'audio/ogg'`)
  exported as constants. The orchestrator defaults to both when the
  caller / source doesn't specify.

### Package surface

- New subpath export: `@bw-advisory/agent-kit/voice`.
- Top-level entry re-exports the voice module as the `Voice` namespace
  alongside `Telegram`, `Tools`, and `Photo`.

### Internal

- Sixteen unit tests covering: single-fetch bytes guarantee, parallel
  persist + STT, transcript shape passthrough, language default + override,
  diarization passthrough, MIME default + override, error bubbling,
  empty-audio handling, MemoryVoiceStorage round-trip, key-shape
  with/without `userId`, MockSTTProvider fixed/queue/call-recording
  behaviour. Full suite: 94 tests pass.

## [0.4.0] — 2026-05-22

### Added — Photo Orchestrator framework

- `src/photo/` — new module exposing the generic ingest → persist →
  vision-extract → typed-result pattern. The third foundational pattern
  after Telegram (0.2) and Tools (0.3). Consumed by Archer (receipts,
  business cards, contracts) and FrankiAI (wealth statements, school
  documents).
- `PhotoSource` — channel-agnostic ingestion shape. `channel` + `externalId`
  + `fetchBytes()` + optional `mimeType` / `caption`. Telegram, email,
  upload, and web ingresses all implement this in their own repos.
- `PhotoStorage` — pluggable durable-storage interface. `persist(source,
  opts) → { url, key }`. Real adapters (Cloudinary, Vercel Blob, S3) live
  in the consuming repos; agent-kit ships only the in-memory adapter.
- `MemoryPhotoStorage` — in-memory `PhotoStorage` implementation for
  tests and local development.
- `extractWithVision({ client, model, imageBytes, mimeType, schema,
  systemPrompt, userPrompt, maxRetries, ... })` — hands a photo to Claude
  vision with a forced single-tool call whose `input_schema` is the
  consumer's Zod schema converted to JSON Schema. Validates the
  `tool_use` input against Zod. Retries with a clarifying prompt on
  validation failure, up to `maxRetries` (default 2). Returns
  strongly-typed data + confidence score (or `null` + reason on failure).
- `processPhoto({ source, storage, vision, schema, systemPrompt,
  userPrompt, ctx })` — orchestrator entry point. Fetches bytes once,
  then runs `storage.persist` and `extractWithVision` in parallel.
  Returns a `PhotoExtractionResult<T>` carrying `data`, `confidence`,
  `photoUrl`, `storageKey`, and an optional `partial` block on failure.
- Confidence pattern: schemas opting in by including
  `confidence: z.number().min(0).max(1)` surface that value on the
  result. Consumers use it to gate "auto-log vs ask for confirmation."
  Schemas without a `confidence` field default to `1` on success and `0`
  on failure.

### Package surface

- New subpath export: `@bw-advisory/agent-kit/photo`.
- Top-level entry re-exports the photo module as the `Photo` namespace
  alongside `Telegram` and `Tools`.

### Internal

- Eleven unit tests covering: single-fetch bytes guarantee, parallel
  persist + vision, typed-data success, retry-on-validation-failure,
  null-after-maxRetries, confidence surfacing, MIME passthrough,
  forced `tool_choice`, MemoryPhotoStorage round-trip, and key-shape
  with/without `userId`. Full suite: 78 tests pass.

## [0.3.0] — 2026-05-22

### Added — Tool Dispatcher framework

- `src/tools/` — new module exposing the typed-tool framework that lets an
  agent declare tools, hand them to Claude as tool-use definitions, and
  execute Claude's calls with validated input.
- `ToolDefinition<TInput, TOutput>` — declarative shape: name,
  description, Zod input schema, credential scope, async handler.
- `ToolRegistry` — registers tools against an agent scope, enforces
  credential-scope matching at registration time, exports the registered
  tools in the shape Anthropic SDK's `messages.create({ tools })` expects.
- `dispatchToolUse(registry, toolUseBlock, ctx)` — executes a single
  Claude `tool_use` block against the registry. Validates input, runs the
  handler, returns a `tool_result` block. Catches handler exceptions and
  returns them as `is_error: true` tool_result blocks so Claude can
  recover gracefully on the next turn.
- `runWithTools({ client, model, system, messages, registry, ctx, ... })`
  — high-level multi-turn helper that wraps a Claude conversation with
  tool use. Loops until `stop_reason !== "tool_use"` or `maxIterations`
  is hit. Tool calls within a single turn are dispatched in parallel.
- Structured telemetry hooks via the `AgentMemoryWriter` interface —
  consumers wire `toolInvocationStart` / `toolInvocationSuccess` /
  `toolInvocationError` to their AgentMemory write path (typically by
  wrapping `writeEpisode` from `@bw-advisory/agent-kit/memory`).
- `ToolScopeMismatchError` exported for callers that want to catch and
  inspect scope-violation throws.

### Added — Security documentation

- `SECURITY.md` — documents the credential-scope principle that the
  ToolRegistry enforces. Headline: "Business agents must never hold
  credentials for personal financial accounts." Explains why enforcement
  is at registration time, what the rule does not protect against, and
  how to report vulnerabilities.

### Added — Dependencies

- `zod ^3.25.x` — runtime input validation for tool definitions.
- `zod-to-json-schema ^3.25.x` — converts Zod schemas to the JSON Schema
  dialect Anthropic's tool-use endpoint accepts.

### Package surface

- New subpath export: `@bw-advisory/agent-kit/tools`.
- Top-level entry re-exports the tools module as the `Tools` namespace
  alongside the existing `Telegram` namespace.

### Internal

- Eight unit tests covering registry scope enforcement, dispatcher input
  validation + error paths, multi-turn loop termination, and
  maxIterations bail-out. All 67 tests across the suite pass.

## [0.2.0] — 2026-05-XX

### Added — Telegram dispatcher framework

- Webhook signature validation, typed Bot API client, router, HTML
  formatting helpers, inline keyboard builders, PendingProposal pattern
  for the Yes / Edit / Discard approval flow.

## [0.1.0] — 2026-05-XX

### Added — Scaffolding + AgentMemory

- Six-discriminator AgentMemory schema (EPISODE / PREFERENCE / BELIEF /
  WORKFLOW / WATCHITEM / CALIBRATION).
- Typed write + read helpers operating against the consumer's
  PrismaClient.
- `src/memory/schema.prisma` shipped as text — consumers copy the enum
  and model into their own schema and run migrations against their own
  database.
