---
name: connector:build
phase: 2
description: Phase 2 of 3 - Build the connector configuration from research data
triggers:
  - /connector:build
  - build connector
  - create connector configuration
---

# Phase 2: Build Connector

## Overview

Generate the connector configuration from research data, set up authentication, and create actions. Deployment is not part of this flow.

## Prerequisites

Build needs research: `run.json` recording the research phase as `complete` (or `exhausted`
with a gap list), and the artifact at
`.connector-dev/{{provider}}/research/{{provider}}_research.json`.

## Phase State Check

```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/state/run.js summary --provider {{provider}}
```

**If research is not recorded as complete, do it — do not stop and hand back a suggestion.**
Load `research-provider`, drive it to its gate, record the transition, then continue into
build in this same turn:

```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/state/run.js set-phase \
  --provider {{provider}} --phase research --status <complete|exhausted> [--unmet R4,R6]
```

Initialise the run first if `summary` reports no run at all:

```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/state/run.js init \
  --provider {{provider}} --request "<the request, verbatim>" --target-phase build
```

An unmet prerequisite that the run can satisfy itself is not a reason to stop. Telling the
customer to issue a second command strands an unattended run at a dead end, and the Stop
hook refuses the turn anyway while a phase is still owed.

`run.json` is written only by `scripts/state/run.js` — never edit it by hand.

## Skills Loaded

**Always, before writing any YAML — not lazily:**

- **Load skill: skills/connector-patterns** → `references/naming-conventions.md` (N1–N12,
  never violated), `references/authentication.md` for the auth block, `SKILL.md` for actions.

  This one is not conditional. The rest of this list is organised by what you are building;
  `connector-patterns` is what tells you whether the shape you wrote actually works. The
  wrong shapes — `- key:` on an input, `in: header`, a bare `values:` enum, an unquoted
  `{{…}}` — are all valid YAML that passes `stackone validate` and does nothing at runtime,
  so nothing downstream will tell you. Load it up front or build blind.

## Skills Loaded (Lazy)

**Load based on detected auth type:**

If OAuth 2.0:
- **Read `skills/build-connector/references/auth-oauth2.md`**
- **Read `skills/build-connector/references/auth-oauth2-code-exchange.md`** (if code flow)
- **Read `skills/build-connector/references/auth-oauth2-client-credentials.md`** (if client creds)

If API Key:
- **Read `skills/build-connector/references/auth-custom.md`**

If SOAP:
- **Read `skills/build-connector/references/auth-soap.md`**

**For configuration:**
- **Read `skills/build-connector/references/build-connector-config.md`**
- **Read `skills/build-connector/references/config-ratelimit.md`**

**For actions (skill chain per action):**

```
action-config          ← entrypoint: action structure, actionType, result pattern
  └── input-config     ← define what inputs the action accepts (types, enums, objects, arrays)
  └── step-function-request          ← HTTP step for REST endpoints
      step-function-upload-download  ← HTTP step for multipart upload or binary download
  └── args-config      ← wire inputs into the provider API call (query, body, headers)
```

- **Read `skills/build-connector/references/action-config.md`**
- **Read `skills/build-connector/references/input-config.md`**
- **Read `skills/build-connector/references/step-function-request.md`** (for REST endpoints)
- **Read `skills/build-connector/references/step-function-upload-download.md`** (for multipart upload or binary download)
- **Read `skills/build-connector/references/args-config.md`**

## Execution Flow

### Step 0: Capture Build State (connector_path)

Before any file is created, record the connector path that all downstream skills will use.

**`connector_path`** — folder OR file where the connector YAML lives / will live. Folder is preferred (works with `stackone validate <dir>` regardless of filename). See `docs/PATHS.md` §The `connector_path` rule.

> **No profile is asked for.** This flow authors YAML locally and never deploys, so a StackOne CLI profile is not needed at any point.

#### Determine connector path

> **This step works in ANY directory.** No monorepo, marketplace checkout, or pre-existing structure is required. If the target folder does not exist, create it. Do NOT stop the phase because "no repo detected" — that is not a blocker.

Detect the project layout to pick a sensible default:

```bash
# Monorepo layout if a src/configs/ directory already exists; otherwise standalone.
[ -d "src/configs" ] && echo "monorepo" || echo "standalone"
```

| Detected layout | Use |
|-------------|------------------|
| Monorepo (`src/configs/` exists) | `src/configs/{{provider}}/` |
| Standalone (no `src/configs/`) | `{{provider}}/` (relative to cwd) |

**Do not ask.** Detect the layout and use the matching path. Create the folder if it does
not exist yet — `mkdir -p` is fine.

**The folder name is the provider token: lowercase letters and digits, run together.** A
multi-word provider never takes a hyphen or underscore — `src/configs/azureblobstorage/`,
never `src/configs/azure-blob-storage/`. Every file inside it carries that same token as its
prefix, and the prefix is parsed to match `$ref` entries, so a separator here propagates
into a wiring failure that never names the folder as the cause.

A run must reach a connector without a human in the loop, and this question has a correct
answer that is derivable in one `test -d`. Honour an explicit path if the customer named
one in their request; otherwise take the default silently and record it in
`build-state.yaml`, which is what anything downstream reads anyway.

**Write `.connector-dev/{{provider}}/build-state.yaml`:**

> ⛔ **State files go under `.connector-dev/{{provider}}/`, NEVER inside `connector_path`.**
>
> Every state/artifact file (`build-state.yaml`, `auth-evidence.json`, gate reports) belongs under `.connector-dev/{{provider}}/`. The `connector_path` folder (`whois/`, `src/configs/whois/`, etc.) holds ONLY the connector YAML files (`*.connector.s1.yaml`, `*.s1.partial.yaml`). Do NOT put `build-state.yaml` or gate output inside `connector_path`.

Use **exactly** this schema — no extra fields:

```yaml
build:
  provider: {{provider}}                # required — string, matches folder name
  connector_path: {{connector_path}}    # required — RELATIVE path (e.g. "whois/" or "src/configs/whois/"); NEVER absolute
  started_at: {{iso_timestamp}}         # required — ISO 8601 timestamp
```

**FORBIDDEN fields** (do NOT add these — they are not part of the schema):
- ❌ `provider_display_name` (belongs in the connector YAML, not build-state)
- ❌ `mode` (`standalone` vs `monorepo` — derivable from `connector_path`, don't duplicate)
- ❌ `initial_test_action` (not a build-state concern)
- ❌ `phase` (tracked by `.connector-dev/{{provider}}/run.json`, written only by `scripts/state/run.js`)
- ❌ `created` (use `started_at`)

Absolute paths for `connector_path` are also forbidden — always relative to the cwd where the build ran.

Every build skill (`build-connector-config`, `build-actions`) reads `connector_path` from this file.

If `build-state.yaml` already exists (resumed build), read it instead of re-prompting.

---

### Step 1: Load Research Data and Scope

```bash
# The single research artifact — auth mechanisms, endpoints, parameters, pagination, rate limits
Read .connector-dev/{{provider}}/research/{{provider}}_research.json

# The resolved scope — which resources and auth mechanisms are in scope
node ${CLAUDE_PLUGIN_ROOT}/scripts/state/run.js read --provider {{provider}} | jq '.scope'
```

Build only what `scope` selected. Endpoints in `scope.dropped[]` are deliberate exclusions — do not build them, do not remove them from the record. `scope.auth.resolved` is the auth build list; `scope.auth.dropped` is deliberately left out.

### Step 2: Generate Connector Config

```
Read references/build-connector-config.md

Create: {{connector_path}}/{{provider}}.connector.s1.yaml
(connector_path from build-state.yaml — e.g. src/configs/{{provider}}/ in a monorepo or {{provider}}/ in a standalone project)

Contents:
- Provider metadata
- Display name
- Base URL
- Authentication configuration
- Rate limiting (if applicable)
```

### Step 3: Configure Authentication (Loop 1 — auth to green first)

Based on the mechanisms in `authMechanisms[]` of the research artifact, filtered by `scope.auth`:

**OAuth 2.0:**
```
Read references/auth-oauth2.md

Configure:
- Authorization URL
- Token URL
- Scopes
- Refresh configuration
```

**API Key:**
```
Read references/auth-custom.md

Configure:
- Header name
- Key placement (header/query)
```

Record the provider auth documentation you read in `.connector-dev/{{provider}}/build/auth-evidence.json` as you go — check A10 fails without it.

Then loop the auth gate until it exits 0. **Do not start writing actions before it does** — a wrong auth block breaks every action at once:

```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/gates/auth.js \
  --provider {{provider}} --connector-path "$CONNECTOR_PATH"

# post-write: the environment and baseUrl rules
node ${CLAUDE_PLUGIN_ROOT}/scripts/gates/conventions.js \
  --provider {{provider}} --connector-path "$CONNECTOR_PATH" --only N7,N8,N10
```

### Step 4: Build Actions (Loop 2)

#### Step 4a — Read Resource Research Data

For each in-scope resource, read its slice of the research artifact before building any YAML:

```bash
# All endpoints for one resource, from the single research artifact
jq --arg r "{{resource}}" '[.endpoints[] | select(.resource == $r)]' \
  .connector-dev/{{provider}}/research/{{provider}}_research.json
```

Extract from the research data:
- All endpoints for the resource (HTTP method, URL, description)
- Every parameter per endpoint — path, query, and body (required AND optional)
- Nested objects, enums, and array types
- Auth scopes required per endpoint

#### Step 4b — Create Partial File per Resource

Structure of a partial file — actions are written as a flat YAML list, each starting with `- actionId:`:

```yaml
- actionId: list_{{resource}}
  label: List {{Resource}}
  categories:
    - category
  actionType: custom
  description: ...
  details: ...
  tags:
    - {{Resource}}
  inputs:
    ...
  steps:
    ...
  result:
    data: $.steps.{{stepId}}.output.data

- actionId: get_{{resource}}
  ...
```

Add a `$ref` to each partial in the connector YAML under `actions`:

```yaml
actions:
  $ref: ./{{provider}}.{{resource}}.s1.partial.yaml
```

#### Step 4c — Implement Actions (per resource)

For each action in a resource, follow the skill chain:

**Entrypoint**: read `references/action-config.md` — defines the full action structure (`actionId`, `label`, `categories`, `actionType: custom`, `description`, `details`, `tags`, `resources`, `inputs`, `steps`, `result`).

Then for each part of the action:

| Part | Skill to load |
| :--- | :--- |
| Define inputs (what the action accepts) | `input-config` |
| Build the HTTP step | `step-function-request` for REST; `step-function-upload-download` for multipart upload or binary download |
| Wire inputs into args (what is sent to provider) | `args-config` |

**Action order within each resource**: LIST → GET → CREATE → UPDATE → DELETE

**Key rules**:
- Read ALL parameters from research data — required AND optional
- Every action uses `actionType: custom`
- Exactly one step per action
- Result is always:
  ```yaml
  result:
    data: $.steps.{{stepId}}.output.data
  ```
- Optional inputs always have `condition: '{{present(inputs.field)}}'` in args
- Path params go in URL via `${inputs.field}` — never in args

#### Step 4d — Verify Coverage

After implementing all actions for a resource, verify:
- Every in-scope endpoint from the research data has a corresponding action
- Every parameter from the research data is mapped as an input
- No parameters are skipped — include all optional ones

Repeat Steps 4a–4d for each resource.

### Step 5: Validate Configuration and Run the Build Gates

> **Path note:** `stackone validate` takes a filesystem path — a folder containing a `.s1.yaml` file (preferred) or the file itself. Resolve `$CONNECTOR_PATH` from `.connector-dev/{{provider}}/build-state.yaml` per `docs/PATHS.md §Connector-path resolution`.

```bash
# Resolve $CONNECTOR_PATH from build-state.yaml (see docs/PATHS.md §Connector-path resolution)
CONNECTOR_PATH=$(yq -r '.build.connector_path // ""' ".connector-dev/{{provider}}/build-state.yaml" 2>/dev/null)
[ -z "$CONNECTOR_PATH" ] && CONNECTOR_PATH=$([ -d "src/configs" ] && echo "src/configs/{{provider}}/" || echo "{{provider}}/")

# Validate connector YAML
stackone validate "$CONNECTOR_PATH"

# Actions gate — --cli-ok carries the stackone validate verdict; B1 fails when omitted
node ${CLAUDE_PLUGIN_ROOT}/scripts/gates/build.js \
  --provider {{provider}} --connector-path "$CONNECTOR_PATH" --cli-ok <true|false>

# Naming and content rules across the whole connector — N1 to N22
node ${CLAUDE_PLUGIN_ROOT}/scripts/gates/conventions.js \
  --provider {{provider}} --connector-path "$CONNECTOR_PATH"
```

The loop, as everywhere in this flow:

```
run the gate
  exit 0  →  move on
  exit 1  →  fix what the gate reported, run it again
  exit 2  →  the gate could not evaluate; that is a bug, fix it and rerun
```

**Stop looping when two consecutive rounds resolve nothing.** Mark the phase `exhausted`, record what remains, and move on — a connector plus an honest gap list beats no connector.

After a targeted fix, re-check only what changed:

```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/gates/build.js --provider {{provider}} --scope candidates
```

### Step 6: Complete Phase

Record the transition — `run.json` is written only by `scripts/state/run.js`:

```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/state/run.js set-phase \
  --provider {{provider}} --phase build --status complete

# or, when the loop stopped converging:
node ${CLAUDE_PLUGIN_ROOT}/scripts/state/run.js set-phase \
  --provider {{provider}} --phase build --status exhausted --unmet B3,B7
```

## Output

### Success

One narrated line, printed verbatim from the narrator so the counts match the YAML:

```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/state/narrate.js \
  --provider {{provider}} --event built --connector-path {{connector_path}}
```

```
✓ Built the connector — 41 actions, 318 parameters
```

Then the one caveat that changes what the customer should do next:

```
⚠️ Not yet deliverable — built, but never checked against {{provider}}'s live docs.
Next: /connector:validate {{provider}}
```

That warning stays. `run.json` keeps `outcome: null` until validate passes by design — a
built connector that was never re-checked against the provider's live docs is how a
hallucinated endpoint or a stale base URL reaches a customer, and neither leaves any trace
in the build artifacts that produced it.

### Failure
```
❌ Phase 2 Failed: Build Connector

Error: Connector validation failed
Reason: Invalid OAuth configuration

Details:
- Missing token_url in auth configuration

Suggestion: Check authMechanisms[] in {{provider}}_research.json for required fields
```

## Flags

- `--resources <list>` - Build only specified resources (comma-separated)
- `--force` - Overwrite existing connector config

## Checkpoints

Phase 2 creates these checkpoints:
1. `connector-created` - YAML generated
2. `auth-configured` - Authentication set up, `auth.js` exits 0
3. `actions-built` - Partial files created
4. `validated` - `stackone validate`, `build.js` and `conventions.js` all exit 0

Resume from any checkpoint by re-issuing the original request.

**Checkpoint 4 ends the *build phase*, not the connector.** The connector is deliverable
only after Phase 3 (`/connector:validate`) re-derives it from the provider's live
documentation and `gates/validate.js` exits 0. Never present a connector as finished from
this command alone.
