@oxy-hq/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +163 -0
  2. package/dist/main.mjs +6688 -0
  3. package/dist/main.mjs.map +1 -0
  4. package/dist/mcp-VdontkUr.mjs +316 -0
  5. package/dist/mcp-VdontkUr.mjs.map +1 -0
  6. package/json-schemas/agent-test.json +104 -0
  7. package/json-schemas/agentic.json +406 -0
  8. package/json-schemas/app.json +1884 -0
  9. package/json-schemas/config.json +2215 -0
  10. package/json-schemas/flow-test.json +124 -0
  11. package/json-schemas/global-semantics.json +782 -0
  12. package/json-schemas/workflow.json +1907 -0
  13. package/package.json +65 -0
  14. package/skills/oxy-airway-pipelines/SKILL.md +338 -0
  15. package/skills/oxy-automations/SKILL.md +67 -0
  16. package/skills/oxy-cli/SKILL.md +134 -0
  17. package/skills/oxy-custom-apps/SKILL.md +330 -0
  18. package/skills/oxy-data-architecture/SKILL.md +225 -0
  19. package/skills/oxy-semantic-modeling/SKILL.md +216 -0
  20. package/template/.gitattributes +2 -0
  21. package/template/.github/scripts/ci-publish.sh +77 -0
  22. package/template/.github/scripts/discover-apps.sh +175 -0
  23. package/template/.github/scripts/publish-env.sh +55 -0
  24. package/template/.github/workflows/publish.yaml +653 -0
  25. package/template/.github/workflows/validate.yaml +264 -0
  26. package/template/.nvmrc +1 -0
  27. package/template/.oxyc-managed +297 -0
  28. package/template/README.md +414 -0
  29. package/template/_gitignore +32 -0
  30. package/template/apps/.gitkeep +0 -0
  31. package/template/config.yml +53 -0
  32. package/template/memory/.gitkeep +0 -0
  33. package/template/package.json +17 -0
  34. package/template/pipelines/.gitkeep +0 -0
  35. package/template/pnpm-workspace.yaml +32 -0
  36. package/template/scripts/dev.sh +443 -0
  37. package/template/semantics/topics/.gitkeep +0 -0
  38. package/template/semantics/views/.gitkeep +0 -0
  39. package/template/workflows/.gitkeep +0 -0
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@oxy-hq/cli",
3
+ "version": "0.1.0",
4
+ "description": "oxyc \u2014 the Oxy CLI: a gh-api-style client for the Oxy HTTP API, plus the customer-account tooling that scopes a session to one customer.",
5
+ "type": "module",
6
+ "bin": {
7
+ "oxyc": "./dist/main.mjs"
8
+ },
9
+ "files": [
10
+ "README.md",
11
+ "dist",
12
+ "json-schemas",
13
+ "skills",
14
+ "template"
15
+ ],
16
+ "publishConfig": {
17
+ "registry": "https://registry.npmjs.org",
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/oxy-hq/oxygen-internal.git",
23
+ "directory": "sdk/cli"
24
+ },
25
+ "scripts": {
26
+ "codegen": "node scripts/sync-schemas.mjs && node scripts/embed-assets.mjs",
27
+ "prebuild": "pnpm run codegen",
28
+ "build": "tsdown",
29
+ "build:watch": "tsdown --watch",
30
+ "build:binary": "pnpm run build && node scripts/build-binaries.mjs",
31
+ "lint": "biome check src",
32
+ "prepublishOnly": "pnpm run build",
33
+ "pretest": "pnpm run build",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "pretypecheck": "pnpm run codegen",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "keywords": [
40
+ "oxy",
41
+ "cli",
42
+ "api",
43
+ "agent"
44
+ ],
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "@clack/prompts": "^0.11.0",
48
+ "@modelcontextprotocol/sdk": "^1.30.0",
49
+ "ajv": "^8.20.0",
50
+ "ajv-formats": "^3.0.1",
51
+ "commander": "^14.0.2",
52
+ "picocolors": "^1.1.1",
53
+ "yaml": "^2.8.1",
54
+ "zod": "^3.25.28 || ^4"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^26.2.0",
58
+ "tsdown": "^0.22.14",
59
+ "typescript": "^7.0.2",
60
+ "vitest": "^4.1.11"
61
+ },
62
+ "engines": {
63
+ "node": ">=20"
64
+ }
65
+ }
@@ -0,0 +1,338 @@
1
+ ---
2
+ name: oxy-airway-pipelines
3
+ description: Use when writing, reviewing, or debugging an Oxy Airway ELT pipeline (`.airway.yml`) — sources, the `kind` values Oxy actually wires, credentials, reset and retry — or when one misbehaves: "unsupported source kind", a pipeline that parses then fails at run time, credentials that do not resolve, a *pipeline* run landing `completed_with_errors` (an *automation* run with that status is `oxy-automations`), a schema that will not change, or duplicate rows. Airway has no published docs; this skill is the reference.
4
+ ---
5
+
6
+ # Oxy Airway pipelines
7
+
8
+ **Unlike every other Oxy subsystem, Airway has no published documentation** — no
9
+ `airway.mdx`, no `.airway.yml` reference page, no JSON schema. So this skill
10
+ carries content instead of pointing at a doc, which also makes it the
11
+ fastest-rotting thing in this set. Treat it as a map, not the territory.
12
+
13
+ ## Version anchor, and what is actually authoritative
14
+
15
+ Everything below was read on 2026-08-17/18 against:
16
+
17
+ | | |
18
+ |---|---|
19
+ | `oxy-internal` workspace version | **0.5.126** (`Cargo.toml:522`) |
20
+ | `airway` engine dependency | git tag **0.1.30** (declared in `Cargo.toml`, the `airway = { git = …, tag = … }` line — `:509` today) resolving to rev `7c1737af9e69e0f9ad4e7a26a7969e4d1309538a` (the rev lives **only** in `Cargo.lock`, `[[package]] name = "airway"` — `:731` today; `Cargo.toml` never names it) |
21
+ | Real corpus | Poke House's 19 `oxy/pipelines/*.airway.yml` |
22
+
23
+ **Bumped since.** On 2026-08-19 `Cargo.lock` reads airway **0.1.32**
24
+ (`55e2794490ebcfffdf69a9693a6044efe313d702`). Everything below was read at
25
+ 0.1.30 and wants re-checking against the live `source_factory.rs`. One error has
26
+ since been corrected in place (the `replacing` buffer's location, under
27
+ duplicate rows) — note that it was **not** caused by the bump: it was wrong at
28
+ 0.1.30 too. A stale claim here is at least as likely to be a misreading as it is
29
+ to be drift; check which before blaming the pin.
30
+
31
+ Airway is an **external engine fetched by git tag**, not a vendored copy —
32
+ `crates/agentic/airway` is only Oxy's wiring around it. So a dependency bump can
33
+ change which `kind`s work, and how they behave, with no oxy-internal commit at
34
+ all. Re-check `Cargo.lock` before trusting any list here.
35
+
36
+ **Two files are authoritative. When they disagree with this skill, they win:**
37
+
38
+ - **`crates/agentic/airway/src/config.rs`** — the parser. Which keys exist,
39
+ which are required, what the defaults are.
40
+ - **`crates/agentic/airway/src/source_factory.rs`** — the dispatch. Which
41
+ `kind` values Oxy can actually build. (Changing that crate rather than using
42
+ it? Read its `CLAUDE.md` first.)
43
+
44
+ **How the "no published doc" claim was established, so you can redo it:** the
45
+ live docs sitemap — <https://www.oxygen-hq.com/docs/sitemap.xml> — carries 146
46
+ URLs, **zero** mentioning airway, and `docs/build/data-infrastructure/` covers
47
+ three of the four similarly-named engines (Airform, airlayer, Airhouse) but not
48
+ this one. The single docs page whose URL says "pipeline",
49
+ <https://www.oxygen-hq.com/docs/guide/build/agents/pipeline>, is a same-word
50
+ false positive documenting the **agentic reasoning FSM**, not ELT. Do not send
51
+ anyone there for Airway.
52
+
53
+ ## Anatomy
54
+
55
+ `AirwayPipelineSpec` (`config.rs`) is `#[serde(deny_unknown_fields)]`, so an
56
+ unrecognised top-level key is a **hard parse error**, not a silent no-op. That
57
+ is the friendly half; the traps below are the rest.
58
+
59
+ | Key | Required | Default | Notes |
60
+ |---|---|---|---|
61
+ | `name` | **yes** | — | validated non-empty |
62
+ | `source` | **yes** | — | `{kind: String, config: <free-form>}` |
63
+ | `destination` | **yes** | — | `{database, dataset_name, schema_separator?}` |
64
+ | `description` | no | — | free text; `>` block scalar is house style |
65
+ | `resources` | no | `[]` = all | subset of the source's advertised resources |
66
+ | `concurrency` | no | **`1`** | validated, capped at `MAX_CONCURRENCY = 16` |
67
+ | `allow_concurrent_runs` | no | `false` | single-flight; see run semantics below |
68
+ | `streaming` | no | `true` | falls back to the bulk path transparently |
69
+ | `channel_capacity` | no | `2 * concurrency` | streaming path only |
70
+
71
+ The corpus's smallest file (`quickbooks_financials.airway.yml`; IDs redacted and
72
+ comments stripped — the real file's comments cover `database:` resolving
73
+ credentials at run time via `airhouse_managed` ephemeral creds, plus commented
74
+ `resources:` / `streaming:` exemplars):
75
+
76
+ ```yaml
77
+ name: quickbooks_financials
78
+ source:
79
+ kind: quickbooks
80
+ config:
81
+ client_id: <literal> # not a secret; see credentials below
82
+ client_secret_var: QB_CLIENT_SECRET
83
+ refresh_token_var: QB_REFRESH_TOKEN
84
+ realm_id: "<literal>"
85
+ destination:
86
+ database: pokehouse
87
+ dataset_name: quickbooks_financials
88
+ concurrency: 1
89
+ ```
90
+
91
+ **All 19 real files use that `destination: {database, dataset_name}` reference
92
+ form**, resolved by the `agentic-pipeline` executor at run time. `config.rs` also
93
+ accepts an `Inline{kind, config}` destination, but its own doc comment calls that
94
+ a test-fixture / post-resolution shape — no real file uses it. Field presence
95
+ across the 19: `name`/`source`/`destination` 19, `concurrency` 14, `description`
96
+ 12, `schema_separator` 1, and `resources` / `allow_concurrent_runs` / `streaming`
97
+ / `channel_capacity` **0** — those four are off the beaten path.
98
+
99
+ **The extension is `.airway.yml`, and it is load-bearing.** All 19 use it; none
100
+ uses a bare `.yml`. The compile-boundary walker
101
+ (`crates/oxy-compile/src/walker.rs:170`) globs literally `**/*.airway.yml` to
102
+ produce `FileKind::AirwayPipeline` — a `pipelines/foo.yml` is not a broken
103
+ pipeline, it is *invisible to compilation*, with no error to explain it.
104
+
105
+ ## The wired `kind` set — the trap that costs the most time
106
+
107
+ **Oxy wires a strict subset of the engine's sources.** The upstream airway crate
108
+ registers roughly **40** source modules; `source_factory.rs` gives **14** of them
109
+ a concrete arm. YAML has no `kind` enum — `SourceConfig.kind` is a plain `String`
110
+ — so `kind: shopify` parses perfectly and only then fails at *run time*, in the
111
+ `other =>` fallback arm of the dispatch `match` inside
112
+ `source_factory.rs`'s `build_source_connector_inner`, with
113
+ ``unsupported source kind `shopify`. Wire it up in
114
+ agentic_airway::source_factory::build_source_connector — …`` (cited by symbol,
115
+ not line: that arm is the *last* one in the table, so every source added above
116
+ it shifts it. Two did on 2026-08-17 alone — `ubereats` (#2937) and `netsuite`
117
+ (#2939).)
118
+
119
+ The 14 wired kinds, and how hard the real corpus exercises each:
120
+
121
+ | `kind` | Real pipelines using it |
122
+ |---|---|
123
+ | `http_file` | 5 (census LODES + gazetteer pulls) |
124
+ | `quickbooks` | 5 (see below — not one pattern) |
125
+ | `rest_api` | 3 (`census_acs`, `nces_schools`, `yelp_fusion`) |
126
+ | `clickhouse` | 2 (`clickhouse_ingest`, `clickhouse_mirror`) |
127
+ | `besttime` | 1 |
128
+ | `overpass` | 1 (`osm_augmentation`) |
129
+ | `overture` | 1 (`overture_places`) |
130
+ | `toast` | 1 (`restaurant_analytics`) |
131
+ | `filesystem`, `sql_database`, `postgres_cdc`, `weather`, `netsuite`, `ubereats` | 0 — wired, never exercised here |
132
+
133
+ Registered upstream but **not** wired here, so unavailable today: `shopify`,
134
+ `stripe`, `github`, `hubspot`, `salesforce`, `slack`, `jira`, `notion`,
135
+ `zendesk`, `google_ads`, `google_analytics`, `google_sheets`, `facebook_ads`,
136
+ `airtable`, `mongodb`, `kafka`, `kinesis`, and more. **Reading airway's own
137
+ source list and concluding a `kind` works here is the most expensive mistake
138
+ this file format offers.** Check the live `match` arms instead.
139
+
140
+ **`rest_api` is the generic escape hatch.** Per `source_factory.rs`'s own doc
141
+ comment, the vendor-specific helpers (`shopify`, `github`, `stripe`, …) are all
142
+ built on `RestApiSource` upstream, so most can be expressed directly as a
143
+ `rest_api` config; per-vendor sugar is deferred until a real consumer asks. A
144
+ source with no dedicated arm is a `rest_api` config, not a blocker.
145
+
146
+ Destinations are narrower still — `destination_factory.rs` wires exactly three:
147
+ `memory` (test fixture), `airhouse` (production; where all 19 real pipelines
148
+ land), and `postgres`.
149
+
150
+ ### `quickbooks` is not a normal match arm
151
+
152
+ It is dispatched by an **early return before the dispatch table**
153
+ (`source_factory.rs:229`), ahead of the `match` at line 236, because it is the
154
+ one source needing plumbing the host must supply: the `RefreshTokenSink` and
155
+ `AccessTokenSource` traits (`source_factory.rs:61` / `:106`), threaded in as
156
+ `QuickBooksTokens`. Two mutually exclusive custody modes fall out of that, and
157
+ `build_quickbooks` **fails closed** if a config mixes them:
158
+
159
+ - **Rotating custody** — `client_secret_var` + `refresh_token_var`: the pipeline
160
+ refreshes against Intuit itself and writes the new token back through the sink.
161
+ - **Read-only custody** — `access_token_var` alone: some *other* component owns
162
+ rotation, and the value is re-read per request rather than substituted once,
163
+ since a 60-minute token pinned at the start of a long backfill expires mid-run.
164
+
165
+ **Why that check exists, and why it matters far beyond this file:** Intuit voids
166
+ the old refresh token whenever it issues a new one, so two components rotating
167
+ the same grant deadlock into `invalid_grant` — each invalidating the other's —
168
+ until a human re-authorises by hand. Exactly one component may ever hold rotating
169
+ custody; `access_token_var` is how you keep a pipeline out of that role. The
170
+ source comment says it outright: a second refresher "would fork this grant's
171
+ rotation chain."
172
+
173
+ Hence the five real QuickBooks pipelines are **not one pattern**. The base
174
+ `quickbooks_financials.airway.yml` is rotating; the four per-location files
175
+ (`_clovis`, `_eastbay`, `_lakeoswego`, `_santarosa`) are read-only, fenced from
176
+ Intuit's token endpoint because a scheduled Oxy Function owns rotation. Copying
177
+ "the QuickBooks pipeline" as a template for a new company means first knowing
178
+ which custody mode that company's secret was provisioned under.
179
+
180
+ ## Credentials live in the secret manager, never in the YAML
181
+
182
+ The convention is a `*_var` suffix naming a secret **key**, never a value;
183
+ `crates/agentic/pipeline/src/executor/mod.rs` (`resolve_airway_source_secrets`)
184
+ holds the per-kind `(field, var_key)` table.
185
+
186
+ - Flat configs. **The per-kind sets are disjoint — read them per row, never as
187
+ a shared menu:**
188
+
189
+ | `kind` | `*_var` keys it manages |
190
+ |---|---|
191
+ | `toast` | `client_secret_var`, `client_id_var` |
192
+ | `quickbooks` | `client_secret_var`, `refresh_token_var` (plus `access_token_var` — see below, resolved differently) |
193
+ | `netsuite` | `private_key_var` |
194
+ | `clickhouse` | `password_var` |
195
+ | `weather`, `besttime` | `api_key_var` |
196
+
197
+ A `kind` with no arm in that `match` carries no managed credentials at all.
198
+ Borrowing across rows fails, and not gently: put `client_id_var` on a
199
+ quickbooks config and the executor leaves it in place (its quickbooks arm
200
+ strips only the two it owns), so it reaches `QuickBooksParams` — which is
201
+ itself `#[serde(deny_unknown_fields)]` and has no such field. Unlike the
202
+ top-level `deny_unknown_fields` at anatomy above, this one bites when the
203
+ connector is **built**, i.e. at run time: a spec that parsed cleanly dies on
204
+ `invalid quickbooks config: unknown field`. Same for `refresh_token_var` on a
205
+ toast config.
206
+ - **`rest_api` nests differently** — auth lives under `config.auth`, handled by
207
+ a separate `resolve_rest_api_auth_secrets`, as `token_var` (bearer) or
208
+ `key_var` (`api_key` header/query). Real, from `census_acs.airway.yml`:
209
+
210
+ ```yaml
211
+ auth:
212
+ type: api_key_query
213
+ key_var: CENSUS_API_KEY
214
+ param: key
215
+ ```
216
+
217
+ The executor resolves `*_var` → literal and then **strips the `_var` key**, so
218
+ the connector factory (and airway itself) only sees the resolved value under the
219
+ plain name — `client_secret_var` becomes `client_secret`. Hence **`password_var`
220
+ is not an accepted field on `ClickHouseParams`**, nor any other pair: the `_var`
221
+ form is valid *only* at the YAML layer.
222
+
223
+ ## Run semantics
224
+
225
+ - **Partial failures are their own status.** A run that loaded some resources
226
+ and failed others records as **`completed_with_errors`**, not as a failure.
227
+ This is airway-engine behaviour Oxy inherits — it lives in the pinned engine
228
+ (`src/pipeline/mod.rs:743,1316,2993`, `src/airstack/mod.rs:313,352`), not in
229
+ the wiring crate — so a dashboard or alert that only matches "failed" reports
230
+ it as clean success.
231
+ - **Schema migration is additive-only, structurally.** `schema/evolution.rs`
232
+ (`diff_schemas` / `diff_tables`) emits only four changes: `TableAdded`,
233
+ `ColumnAdded`, `VariantColumnCreated` (a type conflict creates a *new* variant
234
+ column, leaving the old one alone), `WriteDispositionChanged` — no "column
235
+ dropped", no "type changed in place". The enum cannot represent a narrowing
236
+ change, so **a pipeline sitting on a wrong schema never self-heals, however
237
+ many times you rerun it.**
238
+ - **Reset schema is the explicit escape**, and a real mechanism:
239
+ `crates/agentic/airway/src/reset.rs` drops the pipeline's destination tables
240
+ and deletes its `airway_pipeline_state` row, so the next run re-infers a fresh
241
+ schema. Exposed as an HTTP route (`crates/agentic/http/src/routes/airway.rs`)
242
+ and a UI button (`web-app/src/components/airway/ResetSchemaButton.tsx`).
243
+ Destructive on purpose.
244
+ - **A retry resumes from a cursor; it does not re-pull history.** There are two
245
+ cursors, and confusing them wastes time. The **pipeline-global** one is
246
+ `airway_pipeline_state` (`state_store.rs`), keyed by `pipeline_name`: ordinary
247
+ incremental runs advance it, ordinary retries reuse it, and Reset is the only
248
+ sanctioned way to wipe it. Two runs racing that single row is half of why
249
+ `allow_concurrent_runs` exists — see below for the other half. The **per-run**
250
+ one is `airway_run_extensions.resume_state` (beside `retry_count`), used only
251
+ by **backfill** runs; retry there is *reset-in-place*, re-driving the same
252
+ `run_id` rather than cloning a new run (Toast:
253
+ `with_resumable_backfill_window`).
254
+
255
+ ### Duplicate rows in a `replacing` table
256
+
257
+ `write_disposition: replacing` never writes the served table directly — it
258
+ appends to a buffer that a merge-on-read fold rebuilds latest-wins, chosen to
259
+ avoid the O(target) `MERGE INTO` that OOMs the data plane (the
260
+ `WriteDispositionLabel::Replacing` doc comment in `source_factory.rs`). **That
261
+ fold runs twice over: inline at the end of every successful load, and again on
262
+ the scheduled airhouse vacuum, from identical SQL.** Either can be the one that
263
+ promotes a given row, which is why the failure below is about overlap rather
264
+ than about the vacuum alone.
265
+
266
+ **That doc comment names the buffer `<table>_raw`, and it is wrong** — the
267
+ buffer is a sibling *schema*, `<schema>_raw.<table>`. So was an earlier version
268
+ of this skill; the correction is not a version bump, since the migration
269
+ predates every airway rev oxy has pinned, including 0.1.30. `oxy-data-architecture`
270
+ carries the location, the stale comments, and where the buffer sits in the wider
271
+ picture. That shape is where duplicates come from:
272
+ each run ends with a merge-on-read fold, and **two folds whose snapshots overlap
273
+ each purge against a base the other has not committed yet, so both versions of a
274
+ changed row survive** (`extension/migration.rs:432`). Measured, not
275
+ hypothetical — pokehouse, 2026-08-05: 34 excess rows in `toast_pos.orders`, 104
276
+ in `order_selections`, **every duplicate pair spanning two `_aw_load_id`s**
277
+ (`migration.rs:435`). That span is the diagnostic, so check it *before* auditing
278
+ your own config: a pair straddling two load ids is this bug, not yours — and if
279
+ it doesn't straddle, this section does not explain your duplicates.
280
+
281
+ The guard is the single-flight lease `airway_pipeline_leases` — at most one
282
+ active run per `(workspace_id, pipeline_name)`, taken by one atomic
283
+ `INSERT … ON CONFLICT DO UPDATE … WHERE expires_at < now()` so the database
284
+ resolves the race rather than application code (`extension/pipeline_lease.rs:1`,
285
+ `:16`). A process-local mutex was rejected explicitly: Oxy runs multiple
286
+ `oxy-serve`/`oxy-worker` replicas, and the engine's own `COMPACTION_GATE` rests
287
+ on a single-writer assumption that stopped holding once the data plane scaled
288
+ past one replica (`pipeline_lease.rs:7`). Two consequences follow.
289
+ **`allow_concurrent_runs: true` turns the guard off** — `config.rs:44` says to
290
+ set it only on a pipeline owning no cursor and writing no `replacing` table,
291
+ since on a `replacing` pipeline it re-admits exactly this bug. And **a
292
+ dead-lettered run never releases its lease**: the 6-hour `LEASE_TTL_SECS` is a
293
+ crash backstop, not a reclaim policy, and the generic reaper cannot touch the
294
+ table without leaking a domain table into `agentic-runtime`
295
+ (`pipeline_lease.rs:73`, `:104`) — so a pipeline stuck "busy" with nothing
296
+ running is that gap, and wants the manual release, not a six-hour wait.
297
+
298
+ The lease stops *new* overlapping folds; being an admission guard, it cannot
299
+ repair rows that already landed. Those need Reset schema.
300
+
301
+ ## Traps
302
+
303
+ **The `$schema` header several real pipelines carry is dead, and its failure is
304
+ silent.** Four of the 19 (`quickbooks_financials_clovis`, `_eastbay`,
305
+ `_lakeoswego`, `_santarosa`) open with a `# yaml-language-server: $schema=`
306
+ comment pointing at
307
+ `raw.githubusercontent.com/oxy-hq/oxy/refs/heads/main/json-schemas/pipeline.json`
308
+ (scheme omitted deliberately — the link is dead and this repo's link check fails
309
+ any non-200). Curled: **404**. `oxy-hq/oxy` was renamed to `oxy-hq/oxygen` and
310
+ the path 404s there too; that repo's `json-schemas/` holds 8 files and **no
311
+ `pipeline.json` — no schema for `.airway.yml` exists at all.** Start from a real
312
+ pipeline and you inherit an editor integration that validates nothing, warns
313
+ about nothing, and looks like it works. Delete the line. (The other 15 carry no
314
+ pointer at all, so IDE validation is inconsistent across the corpus even before
315
+ the link being dead.)
316
+
317
+ **Omitting `concurrency` means `1`, not "unlimited".** The default is forced
318
+ sequential and nothing announces it: the 5 files that omit it (the census
319
+ `http_file` pulls) run serially, while the 14 that set it use 2–4. A pipeline
320
+ that just forgets the key is not misconfigured — it is quietly slow.
321
+
322
+ **Two `clickhouse` source footguns, both silent.** (a) `ClickHouseParams`' own
323
+ doc comment admits that `port: 8443` with no `secure:` "silently uses plaintext
324
+ on a TLS port… left as-is deliberately" — tolerable only because the wizard emits
325
+ both keys together, and these are hand-edited files. State `secure:` explicitly
326
+ and match it to the port, as `clickhouse_mirror.airway.yml` does (`8123`/`false`).
327
+ (b) Socket timeouts belong in `settings:`, never a SQL `SETTINGS` clause:
328
+ ClickHouse fixes the response socket's timeout at request setup, *before* parsing
329
+ the query body, so an inline `SETTINGS http_send_timeout=…` is accepted and
330
+ ignored — exactly what someone reaches for when tuning a slow mirror.
331
+
332
+ **A `kind` claim is only as good as the pin it was checked against, and in-tree
333
+ comments do not self-update.** `overture_places.airway.yml` still carries a
334
+ header saying the connector has not landed and the airway rev must be pinned to
335
+ a build registering `kind: overture` — which at 0.1.30 it does, so the comment
336
+ has been stale for a while with nothing to flag it. Read that as the standing
337
+ warning about this whole document: verify against `source_factory.rs` at the rev
338
+ you are running.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: oxy-automations
3
+ description: Use when writing, debugging, or reviewing an Oxy automation (a `.automation.yml` / legacy `.procedure.yml` workflow — tasks, loops, variables, schedules) in a customer workspace, or when an automation *run* misbehaves: an `http_request` task that works locally and fails in cloud, a file that is silently not an automation, or an automation run landing `completed_with_errors`. Covers the file-naming trap, task-type gotchas, and where the authoritative reference lives.
4
+ ---
5
+
6
+ # Oxy automations
7
+
8
+ **The reference is upstream and maintained — read it rather than guessing:**
9
+ <https://www.oxygen-hq.com/docs/guide/build/automations>
10
+ (`index`, `loops`, `task-types`, `variables`).
11
+
12
+ This skill deliberately does **not** restate the schema. It carries the things
13
+ that reference does not tell you, and the mistakes that cost time.
14
+
15
+ ## The file-naming trap
16
+
17
+ The canonical extension is **`.automation.yml`**.
18
+
19
+ - `.procedure.yml` is the **legacy** name. It is still accepted, so a workspace
20
+ full of it keeps working and nothing warns you — which is exactly why it
21
+ spreads. Poke House's workspace has 18 automations, and every one of them is
22
+ still `.procedure.yml`.
23
+ - `.workflow.yml` is **no longer a recognised file kind at all**. A file with
24
+ that name is silently not an automation.
25
+ - `oxy migrate-automations` exists to rename legacy files. It is the only thing
26
+ that reads them for that purpose.
27
+
28
+ **Write new files as `.automation.yml`.** Do not copy the extension from a
29
+ neighbouring file in the workspace you are editing — that is how the legacy name
30
+ propagated in the first place.
31
+
32
+ ## Naming, and why the code disagrees with the docs
33
+
34
+ "Automation" is the current name for what used to be called a Procedure, and
35
+ before that a Workflow. The Rust types still say `Workflow*` / `Procedure*`, and
36
+ `type: workflow` plus the `agentic_workflow_state` table are **wire and storage
37
+ contracts** — they are not stale naming to be tidied up. Seeing `workflow` in the
38
+ code or in a payload does not mean you are looking at something deprecated.
39
+
40
+ Routes follow the same split: `/automations/:id` is canonical;
41
+ `/workflows/:id`, `/procedures`, and `/agentic-workflows` are aliases.
42
+
43
+ ## Task-type gotchas
44
+
45
+ - **`http_request` is HTTPS-only and blocks private egress.** Requests to
46
+ localhost, cloud-metadata endpoints, and private IP ranges are refused unless
47
+ that task opts in with `allow_hosts`. A task that works locally and fails in
48
+ cloud is usually this, not a network problem.
49
+ - **Partial failures are their own state.** A run where some steps failed records
50
+ as `completed_with_errors`, not as a failed run. Code and dashboards that only
51
+ check for "failed" will report it as a success.
52
+
53
+ ## Before you write one
54
+
55
+ Check whether the work belongs in an automation at all. Scheduled data movement
56
+ is usually an Airway pipeline (`.airway.yml`); a question answered from the
57
+ semantic layer is usually an agent or a Data App. Automations are for
58
+ multi-step orchestration you want to run on a schedule or trigger.
59
+
60
+ **But "usually a pipeline" is not "never an automation", and the seam is
61
+ undocumented.** There is a `type: airway` task that runs a pipeline as a step —
62
+ absent from the task-types page — and a step completes only once the pipeline's
63
+ end-of-load fold has committed. That is what makes the standard
64
+ ingest-then-aggregate automation safe: `type: airway` followed by
65
+ `type: execute_sql` building a rollup table. Reach for it whenever a dashboard
66
+ would otherwise aggregate at query time. **`oxy-data-architecture`** owns that
67
+ pattern and the guarantee behind it.
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: oxy-cli
3
+ description: Use when you need data out of an Oxy deployment — querying a customer's warehouse or semantic layer, reading threads/runs/apps/orgs, checking why an endpoint 4xxs, or reproducing a customer-reported bug against real data. Also when you need to know which endpoints exist or what body one takes. Triggers on "query the customer's data", "what does this API return", "debug this in production/dev", "which endpoint", "check the thread/run/app", "oxyc", "oxy api".
4
+ ---
5
+
6
+ # Getting data out of Oxy with `oxyc`
7
+
8
+ `oxyc` is an authenticated `gh api`-shaped client. Prefer it over `curl` — it
9
+ resolves the token, picks the right credential for the surface, and can tell
10
+ you what endpoints exist.
11
+
12
+ ```bash
13
+ # Not published to npm yet, so `npx @oxy-hq/cli` 404s. From a checkout:
14
+ node <repo>/sdk/cli/dist/main.mjs <command>
15
+ ```
16
+
17
+ ## The loop that works
18
+
19
+ **Never guess a path.** Three commands, in this order:
20
+
21
+ ```bash
22
+ oxyc routes sql # 1. what exists, and what it does
23
+ oxyc schema {workspace}/sql/query # 2. the body it expects
24
+ oxyc api {workspace}/sql/query \
25
+ -f 'sql=select 1' -f database=<name> --md # 3. call it
26
+ ```
27
+
28
+ To get data out of a customer's warehouse, that is the whole path:
29
+
30
+ ```bash
31
+ oxyc api orgs --md # org ids
32
+ oxyc api {org}/workspaces --md # workspace ids
33
+ oxyc api {workspace}/databases --jq '.[].name' # connection names
34
+ oxyc api {workspace}/sql/query -f 'sql=…' -f database=… --md
35
+ ```
36
+
37
+ `oxyc routes <filter>` matches on method, path, surface and description, so
38
+ `oxyc routes query`, `oxyc routes admin`, `oxyc routes semantic` all work. Run
39
+ it with no filter only when you genuinely want all ~670 — it is a lot of
40
+ context.
41
+
42
+ ## Placeholders — do not hunt for ids
43
+
44
+ `{org}`, `{workspace}`, `{project}`, `{customer}` and `{me}` fill themselves
45
+ from the customer repo you are standing in, or from `--org` / `--workspace` /
46
+ `--project`.
47
+
48
+ ```bash
49
+ oxyc api {org}/workspaces --md # find a workspace id
50
+ oxyc api {workspace}/agents
51
+ ```
52
+
53
+ An unresolved placeholder errors and names the flag that would fill it. It is
54
+ never sent to the server as a literal.
55
+
56
+ ## Keep the context small
57
+
58
+ - `--jq '<expr>'` on the server-side shape, **always**, before you look at a
59
+ large response. Dumping a full thread list into context is the common waste.
60
+ - `--md` turns an array of objects into a markdown table — far fewer tokens
61
+ than the same rows as JSON, which repeats every field name per row.
62
+ - `--cache 5m` when you are walking the same endpoints repeatedly.
63
+ - `--paginate` merges every page into one document. It is a heuristic on this
64
+ API (pagination is not uniform); if a result looks short, pass
65
+ `--paginate-key <field>`.
66
+
67
+ ## Beyond the API
68
+
69
+ oxyc validate # check the workspace YAML — no network, no token
70
+ oxyc proxy --env dev # local app dev against cloud data
71
+ oxyc guide # this page, to paste into a context file
72
+
73
+ `oxyc validate` is the one command that works entirely offline. It checks
74
+ `config.yml`, `.automation.yml`, `.agentic.yml`, `.app.yml` and
75
+ `.agent.test.yml` against the schemas the Rust config types generate. It is
76
+ STRUCTURAL only — `oxy validate` also resolves `databases:` and `llm.ref`, and
77
+ wins where the two disagree.
78
+
79
+ If your runtime speaks MCP, `oxyc mcp` serves the same API surface as four
80
+ tools (`oxy_routes`, `oxy_schema`, `oxy_request`, `oxy_whoami`) instead.
81
+
82
+ ## Branch on the exit code, not the text
83
+
84
+ | | |
85
+ | --- | --- |
86
+ | `0` | fine |
87
+ | `2` | you called it wrong — fix the command, do not retry |
88
+ | `4` | not authenticated → `oxyc login --env <env>`, or the token expired |
89
+ | `5` | 404 — check the path with `oxyc routes`. In an **admin** surface a 404 can be a scope boundary, not a missing row. |
90
+ | `6` | the request was malformed — check `oxyc schema` |
91
+ | `7` | 5xx / timeout / network — **retryable** |
92
+ | `8` | refused: the operation would have destroyed something |
93
+
94
+ ## Traps specific to this API
95
+
96
+ - **`200` with a body of `null` can mean an expired session**, not "no such
97
+ thing" — `/api/user` does exactly that. `oxyc` warns on stderr when it sees
98
+ one; `oxyc whoami` tells the two apart.
99
+ - **`oxyc schema` covers the data plane, not everything.** The document is
100
+ curated for exactly the endpoints you need to build a body for — SQL, semantic
101
+ query, and the lookups that resolve ids. A blank schema means *undocumented*,
102
+ not nonexistent: `oxyc routes <path>` confirms the endpoint is real and shows
103
+ what the handler says it does.
104
+ - **`/sql/query` does not return an object by default.** It returns arrays of
105
+ strings, **header row first** — `[["id","name"],["1","ada"]]`. `--md` renders
106
+ that as a table; `--jq '.[1:]'` skips the header. Only `result_format:
107
+ "parquet"` returns an object, and only that one carries `truncated`.
108
+ - **A listed route can still 404** if it is `ide-only` or `worker-only`; those
109
+ are hidden by default and shown by `oxyc routes --all`.
110
+ - **Pick the environment deliberately.** `--env local|dev|staging|production`
111
+ (default production), or paste a URL: `--env https://poke-house.oxygen-hq.com`
112
+ targets that deployment *and* sets `{org}`.
113
+ - **Staff hitting a tenant surface need an assume-role session.** A 403 there
114
+ usually means no active session, not a mis-modeled role — check with
115
+ `oxyc assume status`, which answers "none" without failing.
116
+
117
+ ```bash
118
+ oxyc assume start --org <slug|uuid|url> -r "why" # 60 min, not renewable
119
+ oxyc assume status # what is live, minutes left
120
+ oxyc assume end # or --all
121
+ oxyc login --login-env dev,staging # log into several at once
122
+ ```
123
+
124
+ The session hangs off your **account**, not this terminal — your browser is in
125
+ there too, and `end` gets you out of both.
126
+
127
+ ## Do not
128
+
129
+ - Do not `curl` the API by hand — you will get the credential wrong for
130
+ `/external/api/**`, which takes `X-API-Key` rather than a bearer.
131
+ - Do not run a mutating request (`-X POST/PATCH/DELETE`) against **production**
132
+ on your own initiative. Read freely; ask before you write.
133
+ - Do not paste a token into a command line or a file. `oxyc` reads it from the
134
+ login cache; `oxyc token` exists if something really needs it.