Integrations
Many teams first capture requests in an external work system — GitHub Issues, Jira, Azure DevOps, Linear. Kaddo’s Integration Adapter Foundation connects to those systems through a single, provider-agnostic boundary and normalizes what it finds into a neutral model Kaddo can consume.
The rule that governs everything here:
External tools can originate work and contribute context. Kaddo normalizes that information, preserves its traceability, and keeps its own Work Item as the source of truth for development.
External Work System ↓Integration Adapter (normalizes provider data) ↓Normalized External Work Item ↓Import Preview (read-only) ↓Human Confirmation ↓Kaddo Core (creates a canonical Draft) ↓Canonical Work Item ← the source of truthIntegration Adapters vs Agent Adapters
Kaddo uses the word adapter in two unrelated places. Keep them distinct:
| Projects to | Examples | |
|---|---|---|
| Agent Adapters | agent-native files | AGENTS.md, CLAUDE.md |
| Integration Adapters | external work systems | GitHub, Jira, Azure DevOps, Linear |
This page is about Integration Adapters.
Architecture
@kaddo/core (domain: Work Items, Knowledge, Graph) ▲ │ normalized model │ @kaddo/integrations (contract · registry · models · config · errors) ▲ ┌───────────┼───────────┐ ▼ ▼ ▼ GitHub Jira Azure DevOps (future concrete adapters)@kaddo/coreis provider-agnostic. It never imports a vendor SDK and never knows GitHub fields, Jira issue types or Linear states.@kaddo/integrationsowns the adapter contract, registry, normalized models, configuration and secret-reference model, error/status model, import-preview mapping and a reference adapter. It holds no Work Item domain rules, Graph semantics or lifecycle rules — those stay in Core.- The integration service (in the CLI/Admin layer) is the only place external reads cross into Core, and the only place an import materializes a Work Item.
Adapter contract
interface IntegrationAdapter { readonly id: string readonly metadata: IntegrationAdapterMetadata readonly capabilities: IntegrationCapabilities verifyConnection(context): Promise<ConnectionResult> listWorkItems(request): Promise<ExternalWorkItemPage> // paginated getWorkItem(request): Promise<ExternalWorkItem | null>}Adapters are not assumed equivalent. Each declares its capabilities so the UI can ask “what
can this adapter do?” instead of assuming everything is supported. The VS-102 baseline requires
verifyConnection, list, read and import; write, statusSync, comments and webhooks are
future capabilities.
Providers are resolved through a registry — never a hardcoded switch (provider). Adding a new
provider is a registration, not a change to Core, Admin or MCP.
Normalized model
Every provider item becomes a neutral ExternalWorkItem — what Kaddo needs, not a faithful copy of
the provider model. Provider-specific detail may ride along in rawMetadata but never dominates.
Each item has a stable external identity — integration + externalId — used for duplicate
detection and linking. It never relies on the visible title.
Configuration & secrets
Declare integrations in .kaddo/integrations.yml. Configuration carries only how to find a
credential, never the credential itself:
integrations: - id: github-dotear adapter: github enabled: true config: owner: trycatch-tv repository: dotear credentials: token_env: GITHUB_TOKEN # a reference — resolved at runtime only secrets: apiKey: github-dotear.apiKey # VS-103: resolved via SecretProviderAn inline secret (token: ghp_…) is rejected by validation. Secrets are resolved from the
environment only for the duration of a call and never appear in Work Items, Knowledge, the Graph,
context packs, the Admin API, MCP output, logs or telemetry.
Secret management (VS-103)
Kaddo supports two mechanisms for managing secrets:
- Environment variables (VS-102): credentials reference an env var via
token_env: VAR_NAME. - SecretProvider (VS-103): secrets are stored in
.kaddo/.secrets.json(gitignored, never committed) via a pluggableSecretProviderinterface. The YAML stores only a logical reference (e.g.github-dotear.apiKey), never the value.
A CompositeResolver tries the local SecretProvider first, then environment variables. This means both mechanisms work together — local secrets take priority, env vars serve as fallback.
Admin shows secrets as “Configured” or “Not configured” — values are never sent back to the
browser after storage. Adapters declare what secrets they need via secretSchema metadata but never
know where or how secrets are stored.
Admin management
Kaddo Admin provides full CRUD management of integrations — no need to edit YAML by hand:
- Create — choose an adapter type, fill dynamic forms driven by the adapter’s
configSchemaandsecretSchema, and save. - Edit — update configuration or replace secrets on an existing integration.
- Delete — remove an integration and its stored secrets.
- Enable / Disable — toggle an integration without deleting its configuration.
- Verify — test the connection with a single click.
Dynamic forms are rendered from adapter metadata — no hardcoded provider-specific forms. The YAML
file remains the single source of truth: Admin reads and writes .kaddo/integrations.yml directly,
and CLI and Admin always see the same state.
Status & connection
Two axes that must never be conflated: an integration connection status and a Work Item
lifecycle status. verifyConnection() distinguishes “adapter installed” from “integration
actually usable”:
configured · available · unavailable · unauthorized · invalid-config · disabledErrors are normalized (INTEGRATION_UNAUTHORIZED, INTEGRATION_RATE_LIMITED,
INTEGRATION_TIMEOUT, INTEGRATION_UNAVAILABLE, …). Raw provider messages are never surfaced.
Import semantics
Reading is not importing. Viewing EXT-001 does not create a Work Item — import is an explicit,
human-confirmed action:
-
Preview (read-only) shows the source, the captured intent and “No project files have been modified yet.”
-
Confirm. You choose the Kaddo Work Item type — it is never inferred from the external type.
-
Import reuses Core’s
createWorkItem, producing a Draft (regardless of the external status). The external origin is recorded as provenance, not as the truth:source:type: externalprovider: githubintegration: github-dotearid: "231"url: https://github.com/trycatch-tv/dotear/issues/231
Re-importing the same external identity does not create a duplicate — Kaddo returns the existing Work Item. After import, refinement and impact analysis happen on the Kaddo Work Item, and it keeps working even if the external provider becomes unavailable.
CLI
kaddo integrations list # configured integrations + capabilitieskaddo integrations status # verify each and report connection statuskaddo integrations verify <id> # verify one integrationkaddo integrations work-items <id> # list external items (paginated)kaddo integrations work-item <id> <ext-id> # read one external itemkaddo integrations import <id> <ext-id> --type <feature|fix|…> # preview → confirm → DraftRead-only commands support --json. Import always previews and asks for confirmation before Core
creates anything.
MCP
@kaddo/mcp exposes read-only tools — kaddo_integrations_list, kaddo_integrations_status,
kaddo_integrations_work_items, kaddo_integrations_work_item. Reading an external item through MCP
never materializes a Kaddo Work Item; import stays a human-confirmed action.
Reference (mock) adapter
Kaddo ships a deterministic, offline mock adapter that exercises the whole contract — registry,
connection, listing, pagination, read, normalization and error simulation — with no network or
credentials. It is the reference a custom adapter can be checked against.
integrations: - id: mock-work-source adapter: mock enabled: true config: simulate: available # or unauthorized · rate-limited · unavailable · timeoutWriting a custom adapter
- Implement the
IntegrationAdaptercontract. - Declare your capabilities honestly.
- Normalize provider data into
ExternalWorkItem(keep extras inrawMetadata). - Register the adapter in the registry.
- Validate configuration; reference secrets by environment variable, never store them.
- Never write Kaddo artifacts directly — return normalized data and let the integration service and Core own materialization.
External Work Item Discovery & Filtering
VS-104 adds discovery — the ability to query all enabled integrations and see their external work items in a unified view, without importing any of them. This is the “browse before you buy” layer.
Discovery
discoverExternalWorkItems queries every enabled integration that supports list in parallel.
Partial failures are isolated: if one integration errors, the others still return their results.
kaddo integrations discover # discover items from all integrationskaddo integrations discover --types Bug,Feature # filter by typekaddo integrations discover --search billing # text searchAdmin exposes the External Items view, which shows items grouped by integration with type badges, status indicators, labels, and assignees. Each item has an Import action (the same human-confirmed flow from VS-102) and an Open link to the provider’s URL.
Load More — the discovery endpoint supports per-integration cursor-based pagination. When an
integration reports hasMore, the UI shows a Load more button below that integration’s section.
Clicking it fetches the next page using the integration’s cursor and appends the results.
Integration Filters vs UI Filters
Filters come in two flavors:
| Persisted in YAML | Applies to | |
|---|---|---|
| Integration Filters | Yes — .kaddo/integrations.yml | Every query to this integration |
| UI Filters | No — temporary, client-side only | The current discovery session |
Integration filters set the scope of what Kaddo queries from a provider (e.g. “only bugs from the
backend label”). UI filters narrow that further at runtime (e.g. “only the ones assigned to Alice”).
Both share the same ExternalWorkItemFilters shape:
integrations: - id: github-dotear adapter: github enabled: true filters: statuses: - Open - In Progress labels: - backendThe service merges them before calling the adapter — overlay fields (UI) take precedence over base fields (integration) when present.
Filter Capabilities
Each adapter declares which filter fields it supports via filterCapabilities in its metadata. Admin
uses this to render only the filter controls the adapter can actually handle — unsupported filters
are not shown, not silently ignored.
Provider-Driven Integration Setup
VS-103A introduces a provider-driven creation flow: instead of manually filling adapter-specific fields, Admin generates the entire form from adapter metadata — no hardcoded provider logic.
Provider Catalog
The creation flow starts with a visual grid of available providers, sourced directly from the Integration Registry. Each tile shows the adapter’s icon, display name, description and capabilities. Selecting a provider moves to the configuration step.
Adding a new adapter to the registry automatically makes it appear in the catalog — no Admin changes needed.
Schema-based forms
Each adapter declares a configSchema and secretSchema in its metadata. Admin renders a dynamic
form from these schemas at runtime. Supported field types:
| Type | Renders as |
|---|---|
string | Text input |
url | URL input (validated) |
password | Masked input |
number | Number input |
boolean | Checkbox |
select | Dropdown from options |
multi-select | Toggle buttons from options |
Each field carries required, label, description, placeholder, options and defaultValue.
Validation runs against the schema before saving — required-field checks, URL format, type coercion,
option membership.
Verify on create
The creation flow includes a Verify & Save step: after filling configuration and secrets, the
integration is created, secrets are set, and verifyConnection runs automatically. If verification
fails, the integration is removed and the error is shown — so no broken integrations linger.
Provider icons
Adapters declare an icon field in metadata. Admin maps this to a visual icon via ProviderIcon —
a simple emoji-based mapping that is extensible without external assets.
Multiple instances
The same adapter can back multiple integrations — each with independent configuration, secrets and connection status. For example, two separate GitHub integrations pointing at different repositories.
Adapter unavailable
If an adapter is no longer registered but its configuration persists, Admin shows the integration with an “Adapter unavailable” label and hides the Verify button. Configuration is preserved — the adapter can be re-registered later without data loss.
External Work Item Import & Refinement Handoff
VS-105 adds a human-initiated import action that converts a discovered External Work Item into a native Kaddo Draft Work Item — preserving full provenance and an original snapshot of the external state at import time.
Import pipeline
- Disabled check — if the integration is disabled, the import is rejected immediately.
- Concurrent lock — an in-process lock prevents two simultaneous imports of the same item (guards against the TOCTOU race between duplicate check and Work Item creation).
- User selects an External Item in the discovery view and chooses a Kaddo Work Item type.
- Kaddo re-reads the item from the adapter for freshness.
- Duplicate check runs against
integrationId#externalId— if already imported, the existing Work Item is returned (idempotent import, no duplicate created). - A Draft Work Item is created through the standard
createWorkItemCore boundary.
The pipeline is provider-neutral: once an item is normalized to ExternalWorkItem, Core has no
knowledge of the original provider.
Original snapshot
At import time, the external item’s title, description, type, status, labels, assignee and timestamps
are captured as an original_snapshot in the Work Item frontmatter. This snapshot is immutable — it
records what the external item looked like when it was imported, regardless of later changes on either
side.
Provenance
Each imported Work Item carries full traceability in its source metadata:
| Field | Value |
|---|---|
type | external |
provider | Adapter id (e.g. mock, github) |
integration | Kaddo integration id |
id | External item id |
url | Link back to the external item |
imported_at | ISO timestamp of import |
external_updated_at | External item’s last update at import time |
Imported badge in discovery
After importing an item, the discovery view shows an Imported badge with a link to the Kaddo Work Item. The Import button is hidden for already-imported items.
Rich provenance in Work Item detail
The Work Item detail view shows an External provenance section (provider, external ID, integration, timestamps, and an “Open in provider” link) and an Original snapshot section (title, description, type, status, labels, assignee).
Refinement handoff
Imported Work Items enter the standard Kaddo refinement pipeline. The snapshot and provenance survive refinement — no provider-specific refinement logic exists.
Resilience
- Deleting the source integration does not affect imported Work Items.
- If the adapter becomes unavailable, imported Work Items remain fully functional.
- Import is a one-time snapshot, not continuous sync.
Jira Adapter (VS-106)
The first production adapter connects Kaddo to Jira Cloud using email + API Token (Basic Auth). It is fully provider-neutral from Kaddo’s perspective — Admin, Core and the refinement pipeline need no Jira-specific logic.
Configuration
integrations: - id: my-jira adapter: jira enabled: true config: baseUrl: https://mycompany.atlassian.net email: user@example.com secrets: apiToken: my-jira.apiToken| Field | Schema type | Required | Description |
|---|---|---|---|
baseUrl | url | Yes | Jira Cloud instance URL |
email | string | Yes | Atlassian account email |
apiToken | password (secret) | Yes | Jira API Token (never stored in YAML) |
Capabilities
| Capability | Supported |
|---|---|
list | Yes |
read | Yes |
import | Yes |
write | No (future) |
JQL filter generation
The adapter translates the normalized ExternalWorkItemFilters into JQL automatically:
| Filter field | JQL clause |
|---|---|
projects | project IN (...) |
types | issuetype IN (...) |
statuses | status IN (...) |
labels | labels IN (...) |
assignees | assignee IN (...) |
updatedAfter | updated >= "..." |
search | (summary ~ "..." OR description ~ "...") |
providerQuery | Raw JQL appended directly |
Values are properly escaped. When no filters are provided, the adapter defaults to
updated >= -30d ORDER BY updated DESC (the Jira enhanced search endpoint requires bounded JQL).
The providerQuery capability is labeled “JQL” in Admin, so users know they can write raw Jira
Query Language when the normalized filters are not enough.
ADF to Markdown
Jira Cloud stores descriptions in Atlassian Document Format (ADF) — a rich JSON structure. The adapter converts ADF to Markdown at normalization time so the rest of Kaddo works with plain text.
Supported ADF nodes: doc, paragraph, heading (levels 1–6), bulletList, orderedList,
listItem, blockquote, codeBlock (with language), rule, hardBreak, text with marks
(strong, em, code, strike). Media nodes are safely skipped.
Error normalization
| HTTP status | Integration error code |
|---|---|
| 400 (JQL) | INVALID_QUERY |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 410 | UNAVAILABLE |
| 429 | RATE_LIMITED |
| 500+ | UNAVAILABLE |
| AbortError | TIMEOUT |
A 400 response whose body contains JQL/query/field keywords is mapped to INVALID_QUERY so the UI
can show a specific message about filter syntax. Other 400 errors fall through to PROVIDER_ERROR.
Projects filter
VS-106 adds a projects field to the normalized filter model (ExternalWorkItemFilters). This is
a common concept across providers (Jira has projects, GitHub has repos, Azure DevOps has projects)
and avoids forcing users to resort to providerQuery for basic project scoping.
External Content Import (VS-109)
VS-109 adds a second import path — raw text or Markdown — independent of any Integration Adapter.
Content from ChatGPT, Claude, a wiki, a Notion export, or a plain .md file enters Kaddo through the
same Core boundary as an adapter import, producing a canonical Draft Work Item with full provenance.
The critical invariant: import never executes a Work Item. The content is data — parsed, normalized and registered — but implementation requires a separate, explicit action.
Raw content (text / Markdown / Kaddo-format Markdown) ↓detectFormat() (kaddo-frontmatter | markdown-frontmatter | markdown | plain-text) ↓parseContent() (extracts title, type candidate, sections, candidate fields) ↓normalizeImportFields() (validates type, builds provenance, captures original snapshot) ↓computeContentHash() (SHA-256 of trimmed input) ↓findDuplicateByHash() (walks existing WIs — returns existing WI if same hash) ↓createWorkItem() (Core — always Draft, always new ID) ↓buildRefinementHandoff() (agent-agnostic refinement instructions)Supported formats
| Format | Detected when |
|---|---|
kaddo-frontmatter | YAML frontmatter with type: work-item or id: WI-NNN |
markdown-frontmatter | YAML frontmatter without Kaddo markers |
markdown | ## headings, no frontmatter |
plain-text | Everything else |
Lifecycle-controlled fields
External content may carry fields like status: completed or id: WI-001. Kaddo discards these
lifecycle-controlled fields during normalization — they never override the internal lifecycle:
id, status, phase, knowledge_level, refined_by, implemented_by, closed_by, ready_at,
generated_by, template_version, implementation_status, validation_status, release_status.
The imported Work Item is always created as a Draft with a Kaddo-generated ID.
Duplicate detection
A SHA-256 hash of the trimmed input is stored in source.source_hash. Before creating a Work Item,
the pipeline walks all existing Work Items and checks their source_hash. If a match is found, the
existing Work Item is returned — no duplicate is created.
Provenance
Each content-imported Work Item carries traceability in its source metadata:
| Field | Value |
|---|---|
type | chat (MCP/conversation) or external (CLI/Admin) |
imported_at | Date of import |
source_format | Detected format (kaddo-frontmatter, markdown, etc.) |
source_hash | SHA-256 of the original content |
An original_snapshot captures the external title, description, type and status at import time.
CLI
kaddo work-item import ./feature.md # import from a filekaddo work-item import --text "Add CloudWatch alert" -y # import from inline text, skip confirmationkaddo work-item import ./spec.md --type bugfix # override the detected typeThe command previews the detected format, title and type before asking for confirmation. Pass -y or
--yes to skip the prompt.
MCP
The kaddo_work_item_import tool accepts raw content and returns the created Work Item with
executed: false and an optional refinement handoff:
{ "content": "## Add CloudWatch alert\n\nMonitor API latency p99 and alert when > 500ms.", "type": "feature", "source": "chat"}The response includes workItemId, path, status: "draft", sourceFormat, and
refinementHandoff with the recommended agent and skill for refinement.
Safety boundary
- Content is data, never instructions — the parser extracts structure, never executes commands.
- Input is capped at 100 KB.
- The result always includes
executed: false— a signal to any consumer that the Work Item has been registered but not acted upon.
Out of scope (built on this foundation later)
Bidirectional sync, polling, webhooks, status/comment/attachment sync, pushing or updating external issues, and external OAuth UI are not part of the foundation. They build on top of it — without redesigning the integration model.