@1aboveio/skills 0.17.0 → 0.18.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.
package/README.md CHANGED
@@ -68,7 +68,7 @@ planned same-named skill. The coordinator still owns group and new-member consen
68
68
  - `engineering-workflow` — Engineering Workflow: e2e-test, ensure-coverage, implement-and-pr, resolve-issues, resolve-release, review-pr, smoke, engineering-runtime
69
69
  - `delivery-infrastructure` — Delivery Infrastructure: cloud-build, cloud-debug, cloud-deploy, docker, google-cloud, mergify, podman
70
70
  - `backend` — Backend: app-debug, python-backend
71
- - `data-science` — Data Science: airflow-dag-develop, pyspark
71
+ - `data-science` — Data Science: airflow, pyspark
72
72
  - `payment` — Payment: fraud-analysis, payment-analysis
73
73
  - `fullstack` — Fullstack: better-auth, monorepo, nextjs-fullstack, prisma-setup, shadcn, zod-v4
74
74
 
@@ -123,7 +123,7 @@ npx skills@1.5.22 remove --global --agent universal claude-code --skill cloud-bu
123
123
  # Backend
124
124
  npx skills@1.5.22 remove --global --agent universal claude-code --skill app-debug python-backend
125
125
  # Data Science
126
- npx skills@1.5.22 remove --global --agent universal claude-code --skill airflow-dag-develop pyspark
126
+ npx skills@1.5.22 remove --global --agent universal claude-code --skill airflow pyspark
127
127
  # Payment
128
128
  npx skills@1.5.22 remove --global --agent universal claude-code --skill fraud-analysis payment-analysis
129
129
  # Fullstack
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1aboveio/skills",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Install the 1AboveIO first-party skill groups through the native Skills CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "id": "first-party",
9
9
  "type": "first-party",
10
10
  "package": "@1aboveio/skills",
11
- "version": "0.17.0",
11
+ "version": "0.18.0",
12
12
  "updatePolicy": "pinned-npm-version",
13
13
  "members": [
14
14
  {
@@ -114,8 +114,8 @@
114
114
  "licenseResponsibility": "1aboveio-mit"
115
115
  },
116
116
  {
117
- "installName": "airflow-dag-develop",
118
- "path": "skills/data-science/airflow-dag-develop",
117
+ "installName": "airflow",
118
+ "path": "skills/data-science/airflow",
119
119
  "role": "skill",
120
120
  "licenseResponsibility": "1aboveio-mit"
121
121
  },
@@ -196,7 +196,7 @@
196
196
  "sourceId": "first-party",
197
197
  "sourceType": "first-party",
198
198
  "package": "@1aboveio/skills",
199
- "version": "0.17.0",
199
+ "version": "0.18.0",
200
200
  "installPath": null,
201
201
  "members": [
202
202
  "harness-runtime",
@@ -230,7 +230,7 @@
230
230
  "sourceId": "first-party",
231
231
  "sourceType": "first-party",
232
232
  "package": "@1aboveio/skills",
233
- "version": "0.17.0",
233
+ "version": "0.18.0",
234
234
  "installPath": null,
235
235
  "members": [
236
236
  "harness-runtime",
@@ -274,7 +274,7 @@
274
274
  "sourceId": "first-party",
275
275
  "sourceType": "first-party",
276
276
  "package": "@1aboveio/skills",
277
- "version": "0.17.0",
277
+ "version": "0.18.0",
278
278
  "installPath": null,
279
279
  "members": [
280
280
  "harness-runtime",
@@ -307,7 +307,7 @@
307
307
  "sourceId": "first-party",
308
308
  "sourceType": "first-party",
309
309
  "package": "@1aboveio/skills",
310
- "version": "0.17.0",
310
+ "version": "0.18.0",
311
311
  "installPath": null,
312
312
  "members": [
313
313
  "harness-runtime",
@@ -350,7 +350,7 @@
350
350
  "sourceId": "first-party",
351
351
  "sourceType": "first-party",
352
352
  "package": "@1aboveio/skills",
353
- "version": "0.17.0",
353
+ "version": "0.18.0",
354
354
  "installPath": null,
355
355
  "members": [
356
356
  "harness-runtime",
@@ -378,7 +378,7 @@
378
378
  "sourceId": "first-party",
379
379
  "sourceType": "first-party",
380
380
  "package": "@1aboveio/skills",
381
- "version": "0.17.0",
381
+ "version": "0.18.0",
382
382
  "installPath": null,
383
383
  "members": [
384
384
  "harness-runtime",
@@ -416,17 +416,17 @@
416
416
  "sourceId": "first-party",
417
417
  "sourceType": "first-party",
418
418
  "package": "@1aboveio/skills",
419
- "version": "0.17.0",
419
+ "version": "0.18.0",
420
420
  "installPath": null,
421
421
  "members": [
422
422
  "harness-runtime",
423
- "airflow-dag-develop",
423
+ "airflow",
424
424
  "pyspark"
425
425
  ],
426
426
  "commands": [
427
427
  {
428
428
  "transport": "local",
429
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime airflow-dag-develop pyspark"
429
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime airflow pyspark"
430
430
  }
431
431
  ],
432
432
  "onFailure": {
@@ -444,17 +444,17 @@
444
444
  "sourceId": "first-party",
445
445
  "sourceType": "first-party",
446
446
  "package": "@1aboveio/skills",
447
- "version": "0.17.0",
447
+ "version": "0.18.0",
448
448
  "installPath": null,
449
449
  "members": [
450
450
  "harness-runtime",
451
- "airflow-dag-develop",
451
+ "airflow",
452
452
  "pyspark"
453
453
  ],
454
454
  "commands": [
455
455
  {
456
456
  "transport": "local",
457
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime airflow-dag-develop pyspark"
457
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime airflow pyspark"
458
458
  }
459
459
  ],
460
460
  "onFailure": {
@@ -465,7 +465,7 @@
465
465
  ]
466
466
  },
467
467
  "uninstall": {
468
- "command": "npx skills@1.5.22 remove --global --agent universal claude-code --skill airflow-dag-develop pyspark",
468
+ "command": "npx skills@1.5.22 remove --global --agent universal claude-code --skill airflow pyspark",
469
469
  "removesDependencies": false
470
470
  }
471
471
  },
@@ -482,7 +482,7 @@
482
482
  "sourceId": "first-party",
483
483
  "sourceType": "first-party",
484
484
  "package": "@1aboveio/skills",
485
- "version": "0.17.0",
485
+ "version": "0.18.0",
486
486
  "installPath": null,
487
487
  "members": [
488
488
  "harness-runtime",
@@ -510,7 +510,7 @@
510
510
  "sourceId": "first-party",
511
511
  "sourceType": "first-party",
512
512
  "package": "@1aboveio/skills",
513
- "version": "0.17.0",
513
+ "version": "0.18.0",
514
514
  "installPath": null,
515
515
  "members": [
516
516
  "harness-runtime",
@@ -548,7 +548,7 @@
548
548
  "sourceId": "first-party",
549
549
  "sourceType": "first-party",
550
550
  "package": "@1aboveio/skills",
551
- "version": "0.17.0",
551
+ "version": "0.18.0",
552
552
  "installPath": null,
553
553
  "members": [
554
554
  "harness-runtime",
@@ -580,7 +580,7 @@
580
580
  "sourceId": "first-party",
581
581
  "sourceType": "first-party",
582
582
  "package": "@1aboveio/skills",
583
- "version": "0.17.0",
583
+ "version": "0.18.0",
584
584
  "installPath": null,
585
585
  "members": [
586
586
  "harness-runtime",
@@ -620,7 +620,7 @@
620
620
  "sourceId": "first-party",
621
621
  "sourceType": "first-party",
622
622
  "package": "@1aboveio/skills",
623
- "version": "0.17.0",
623
+ "version": "0.18.0",
624
624
  "installPath": null,
625
625
  "members": [
626
626
  "harness-runtime"
@@ -646,7 +646,7 @@
646
646
  "sourceId": "first-party",
647
647
  "sourceType": "first-party",
648
648
  "package": "@1aboveio/skills",
649
- "version": "0.17.0",
649
+ "version": "0.18.0",
650
650
  "installPath": null,
651
651
  "members": [
652
652
  "harness-runtime"
@@ -678,7 +678,7 @@
678
678
  "sourceId": "first-party",
679
679
  "sourceType": "first-party",
680
680
  "package": "@1aboveio/skills",
681
- "version": "0.17.0",
681
+ "version": "0.18.0",
682
682
  "installPath": null,
683
683
  "members": [
684
684
  "harness-runtime",
@@ -699,7 +699,7 @@
699
699
  "podman",
700
700
  "app-debug",
701
701
  "python-backend",
702
- "airflow-dag-develop",
702
+ "airflow",
703
703
  "pyspark",
704
704
  "fraud-analysis",
705
705
  "payment-analysis",
@@ -713,7 +713,7 @@
713
713
  "commands": [
714
714
  {
715
715
  "transport": "local",
716
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman app-debug python-backend airflow-dag-develop pyspark fraud-analysis payment-analysis better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
716
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman app-debug python-backend airflow pyspark fraud-analysis payment-analysis better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
717
717
  }
718
718
  ],
719
719
  "onFailure": {
@@ -731,7 +731,7 @@
731
731
  "sourceId": "first-party",
732
732
  "sourceType": "first-party",
733
733
  "package": "@1aboveio/skills",
734
- "version": "0.17.0",
734
+ "version": "0.18.0",
735
735
  "installPath": null,
736
736
  "members": [
737
737
  "harness-runtime",
@@ -752,7 +752,7 @@
752
752
  "podman",
753
753
  "app-debug",
754
754
  "python-backend",
755
- "airflow-dag-develop",
755
+ "airflow",
756
756
  "pyspark",
757
757
  "fraud-analysis",
758
758
  "payment-analysis",
@@ -766,7 +766,7 @@
766
766
  "commands": [
767
767
  {
768
768
  "transport": "local",
769
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman app-debug python-backend airflow-dag-develop pyspark fraud-analysis payment-analysis better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
769
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman app-debug python-backend airflow pyspark fraud-analysis payment-analysis better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
770
770
  }
771
771
  ],
772
772
  "onFailure": {
@@ -777,7 +777,7 @@
777
777
  ]
778
778
  },
779
779
  "uninstall": {
780
- "command": "npx skills@1.5.22 remove --global --agent universal claude-code --skill e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman app-debug python-backend airflow-dag-develop pyspark fraud-analysis payment-analysis better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 harness-runtime"
780
+ "command": "npx skills@1.5.22 remove --global --agent universal claude-code --skill e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman app-debug python-backend airflow pyspark fraud-analysis payment-analysis better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 harness-runtime"
781
781
  }
782
782
  }
783
783
  }
@@ -97,7 +97,7 @@ export const WORKFLOW_TRUSTED_SOURCES = deepFreeze({
97
97
  id: 'first-party',
98
98
  type: 'first-party',
99
99
  package: '@1aboveio/skills',
100
- version: '0.17.0',
100
+ version: '0.18.0',
101
101
  },
102
102
  matt: {
103
103
  id: 'matt-pocock',
@@ -24,7 +24,7 @@ So the watcher's memory *is* the verdict. Four pieces of state must survive acro
24
24
 
25
25
  1. **`seenQueued`** — has queue entry ever been observed? Every downstream transition depends on it. Set from the `queued` label **or** `mergify queue show`.
26
26
  2. **A re-read counter** (`missingActiveReads` / `absentStreak`) — one absence is suspicion, two consecutive *readable* absences settle `DEQUEUED`.
27
- 3. **Readable vs. absent** — a failed `gh`/`mergify` call is *no information* and must not advance that counter.
27
+ 3. **Readable vs. absent** — a failed `gh`/`mergify` call is *no information*: it must not advance that counter or overwrite that source's last readable contribution to the stall-state snapshot. During a partial outage, the still-readable sibling remains informative and genuine progress from it still resets the change clock.
28
28
  4. **A change clock** (`lastChangeMs`) — the stall bound is *no observable change*, not elapsed time.
29
29
 
30
30
  ### Why a per-poll predicate cannot satisfy this
@@ -160,7 +160,7 @@ When the reason is not machine-readable, quote the Mergify status comment, queue
160
160
  ## Watcher Requirements
161
161
 
162
162
  - Run as one background task or one script invocation; do not spend foreground turns polling.
163
- - Retry transient `gh`, GraphQL, and Mergify CLI failures. EOF, TLS handshake timeouts, and temporary API failures are not dequeue signals — an unreadable read is *no information*, not a "not queued" observation, and must not advance the counter that settles `DEQUEUED`.
163
+ - Retry transient `gh`, GraphQL, and Mergify CLI failures. EOF, TLS handshake timeouts, and temporary API failures are not dequeue signals — an unreadable read is *no information*, not a "not queued" observation, and must not advance the counter that settles `DEQUEUED`. Retain that source's last readable stall-state contribution instead; a readable sibling can still report genuine progress and reset the change clock.
164
164
  - Bound every subprocess read on its own timeout, in addition to the stall/ceiling bounds below — those are only evaluated after a poll returns, so an unbounded read can hang the watcher past both of them.
165
165
  - **Run the bundled script. Do not hand-roll a watcher** — see the progression section above for why the hand-rolled shape cannot hold the contract, whatever care goes into the code. (If you are reading a legacy shell watcher: avoid shell-reserved variable names such as `status`.)
166
166
  - Keep enqueue and watch separate. A watch observes only; it does not post `@mergifyio queue`, `@mergifyio dequeue`, or merge.
@@ -165,6 +165,50 @@ export function observationKey({ pr = {}, queue = {} } = {}) {
165
165
  ])
166
166
  }
167
167
 
168
+ function stallPrSnapshot(pr = {}) {
169
+ return {
170
+ state: pr.state ?? null,
171
+ mergedAt: pr.mergedAt ?? null,
172
+ headRefOid: pr.headRefOid ?? null,
173
+ labels: Array.isArray(pr.labels) ? [...pr.labels] : [],
174
+ failedChecks: Array.isArray(pr.failedChecks) ? pr.failedChecks.map(({ name, status }) => ({ name, status })) : [],
175
+ pendingChecks: Array.isArray(pr.pendingChecks) ? pr.pendingChecks.map(({ name, status }) => ({ name, status })) : [],
176
+ }
177
+ }
178
+
179
+ function stallQueueSnapshot(queue = {}) {
180
+ return {
181
+ queued: queue.queued ?? null,
182
+ queueState: queue.queueState ?? queue.state ?? null,
183
+ position: queue.position ?? null,
184
+ }
185
+ }
186
+
187
+ // Retain each source's last readable contribution independently. A failed read carries no
188
+ // replacement state, while a readable sibling can still move the effective stall snapshot.
189
+ export function nextStallObservation({ cache = {}, pr = {}, queue = {} } = {}) {
190
+ const previous = cache && typeof cache === 'object' ? cache : {}
191
+ const prReadable = pr.readable !== false
192
+ const queueReadable = queue.readable !== false
193
+ const nextPr = prReadable ? stallPrSnapshot(pr) : (previous.pr ?? null)
194
+ const nextQueue = queueReadable ? stallQueueSnapshot(queue) : (previous.queue ?? null)
195
+
196
+ if (!prReadable && !queueReadable) {
197
+ return {
198
+ cache: { pr: nextPr, queue: nextQueue, key: previous.key ?? null },
199
+ key: previous.key ?? null,
200
+ changed: false,
201
+ }
202
+ }
203
+
204
+ const key = observationKey({ pr: nextPr ?? {}, queue: nextQueue ?? {} })
205
+ return {
206
+ cache: { pr: nextPr, queue: nextQueue, key },
207
+ key,
208
+ changed: key !== (previous.key ?? null),
209
+ }
210
+ }
211
+
168
212
  // A failed subprocess read (`readable: false`) carries no information about queue membership
169
213
  // — it is not evidence the PR is still queued, and it is not evidence it left the queue
170
214
  // either. Only a clean read that reports absence should move the counter that eventually
@@ -609,7 +653,7 @@ export async function runCli(argv) {
609
653
  let seenQueued = args.seenQueued === true
610
654
  let everActive = false
611
655
  let missingActiveReads = 0
612
- let lastKey = null
656
+ let stallObservationCache = null
613
657
  let lastChangeMs = startedMs
614
658
  let observations = 0
615
659
  let phases = []
@@ -675,10 +719,10 @@ export async function runCli(argv) {
675
719
  return verdict.outcome === 'MERGED' ? 0 : 4
676
720
  }
677
721
 
678
- const key = observationKey({ pr, queue })
679
- const changed = key !== lastKey
722
+ const stallObservation = nextStallObservation({ cache: stallObservationCache, pr, queue })
723
+ stallObservationCache = stallObservation.cache
724
+ const changed = stallObservation.changed
680
725
  if (changed) {
681
- lastKey = key
682
726
  lastChangeMs = Date.now()
683
727
  }
684
728
 
@@ -0,0 +1,198 @@
1
+ ---
2
+ name: airflow
3
+ description: Airflow DAG architecture and conventions. Use when designing, writing, reviewing, testing, or debugging Airflow DAGs and tasks, including TaskGroups, data intervals, timezone-aware date parameters, CLI validation, environment-based scheduling, and pytest integrity tests.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Airflow DAG Development Patterns
8
+
9
+ ## 1. Organize Data Pipelines with TaskGroups
10
+
11
+ Use `TaskGroup` to make meaningful pipeline stages visible in the Airflow graph. For data warehouse pipelines, group tasks by warehouse layer such as `stg`, `ods`, `dwd`, `dws`, and `ads`. For other pipelines, use domain-specific groups such as `features`, `exports`, `quality_checks`, or `notifications`.
12
+
13
+ Keep groups aligned with real ownership or dependency boundaries; do not create a group solely to wrap one trivial task. Express dependencies between groups so the graph communicates the pipeline's high-level flow.
14
+
15
+ ```python
16
+ from airflow.decorators import dag
17
+ from airflow.utils.task_group import TaskGroup
18
+
19
+
20
+ @dag(...)
21
+ def customer_metrics():
22
+ with TaskGroup(group_id="stg") as stg:
23
+ load_stg_customers()
24
+ load_stg_orders()
25
+
26
+ with TaskGroup(group_id="dwd") as dwd:
27
+ build_dwd_customers()
28
+ build_dwd_orders()
29
+
30
+ with TaskGroup(group_id="ads") as ads:
31
+ build_ads_customer_metrics()
32
+
33
+ stg >> dwd >> ads
34
+
35
+
36
+ customer_metrics()
37
+ ```
38
+
39
+ ## 2. Use Timezone-Aware Data Intervals
40
+
41
+ Prefer Airflow's `data_interval_start` and `data_interval_end` as task date-range inputs. They are timezone-aware logical boundaries and behave correctly for scheduled runs, catchup, and daylight-saving transitions. Treat `data_interval_end` as an exclusive boundary.
42
+
43
+ Prepare one shared set of templated date variables for the whole DAG, converting the interval boundaries to the DAG's business timezone. Pass those variables to every task that reads or writes partitioned data. Keep the conversion in Jinja so it happens for each DAG run, not while the scheduler parses the DAG file.
44
+
45
+ ```python
46
+ import pendulum
47
+ from airflow.decorators import dag, task
48
+ from airflow.utils.task_group import TaskGroup
49
+
50
+ LOCAL_TIMEZONE = "Asia/Shanghai"
51
+
52
+ # These values are rendered separately for every DAG run. The end is exclusive.
53
+ DATE_RANGE = {
54
+ "start_date": (
55
+ f"{{{{ data_interval_start.in_timezone('{LOCAL_TIMEZONE}') | ds }}}}"
56
+ ),
57
+ "end_date": (
58
+ f"{{{{ data_interval_end.in_timezone('{LOCAL_TIMEZONE}') | ds }}}}"
59
+ ),
60
+ }
61
+
62
+
63
+ @dag(
64
+ schedule="0 2 * * *",
65
+ start_date=pendulum.datetime(2024, 1, 1, tz=LOCAL_TIMEZONE),
66
+ catchup=True,
67
+ )
68
+ def daily_warehouse():
69
+ @task
70
+ def load_partition(start_date: str, end_date: str) -> None:
71
+ # Read records in the half-open range [start_date, end_date).
72
+ ...
73
+
74
+ with TaskGroup(group_id="stg"):
75
+ load_partition(**DATE_RANGE)
76
+
77
+
78
+ daily_warehouse()
79
+ ```
80
+
81
+ Use timestamps instead of dates when a task needs sub-day precision:
82
+
83
+ ```python
84
+ INTERVAL_RANGE = {
85
+ "interval_start": "{{ data_interval_start.in_timezone('Asia/Shanghai').to_iso8601_string() }}",
86
+ "interval_end": "{{ data_interval_end.in_timezone('Asia/Shanghai').to_iso8601_string() }}",
87
+ }
88
+ ```
89
+
90
+ ## 3. Operations: Wait for DAG Auto-Reload
91
+
92
+ Airflow periodically scans the DAG directory and automatically reloads changed DAG files. The refresh cadence depends on the deployment's scheduler and DAG-processing configuration, and commonly takes a few minutes.
93
+
94
+ After updating a DAG file, do not restart the Airflow services by default. Watch for the DAG to refresh in the Airflow UI or CLI after the configured scan interval. Investigate import errors or deployment synchronization only if the update does not appear after a reasonable interval.
95
+
96
+ ## 4. Quick DAG Validation with Airflow CLI
97
+
98
+ Use these commands for fast feedback during development:
99
+
100
+ ```bash
101
+ # List import errors (fastest check - catches syntax and import issues)
102
+ airflow dags list-import-errors
103
+
104
+ # Full lint check (comprehensive - catches best practice violations)
105
+ airflow dags lint
106
+ ```
107
+
108
+ **When to use:**
109
+ - `list-import-errors`: During active development for quick syntax/import validation
110
+ - `lint`: Before commits to catch style and best practice issues
111
+
112
+ ## 5. Environment-Based Schedule Override
113
+
114
+ Disable schedules in local development to prevent accidental runs:
115
+
116
+ ```python
117
+ import os
118
+ from datetime import datetime
119
+ from airflow import DAG
120
+
121
+ def get_schedule(production_schedule: str) -> str | None:
122
+ """Return None for local dev, actual schedule for deployed environments.
123
+
124
+ Set AIRFLOW_ENV=local in .env for local development.
125
+ Cloud Run deployments set AIRFLOW_ENV=dev|staging|prod.
126
+ """
127
+ env = os.getenv("AIRFLOW_ENV", "local")
128
+ if env == "local":
129
+ return None
130
+ return production_schedule
131
+ ```
132
+
133
+ **Usage in DAG:**
134
+ ```python
135
+ with DAG(
136
+ dag_id="my_dag",
137
+ schedule=get_schedule("0 4 * * *"), # Daily 4 AM in prod, None locally
138
+ start_date=datetime(2024, 1, 1),
139
+ catchup=False,
140
+ ) as dag:
141
+ ...
142
+ ```
143
+
144
+ **Environment setup:**
145
+ - **Local:** Add `AIRFLOW_ENV=local` to `.env`
146
+ - **Cloud Run:** Set via cloudbuild.yaml substitutions (`_AIRFLOW_ENV: dev|staging|prod`)
147
+
148
+ ## 6. Pytest DAG Integrity Tests
149
+
150
+ Standard pytest patterns for validating DAG quality:
151
+
152
+ ```python
153
+ # tests/test_dag_integrity.py
154
+ import pytest
155
+ from airflow.models import DagBag
156
+
157
+ @pytest.fixture(scope="session")
158
+ def dagbag():
159
+ return DagBag(dag_folder="dags/", include_examples=False)
160
+
161
+ def test_no_import_errors(dagbag):
162
+ """Verify all DAGs load without import errors."""
163
+ assert not dagbag.import_errors, f"DAG import errors: {dagbag.import_errors}"
164
+
165
+ def test_dags_have_tags(dagbag):
166
+ """Verify all DAGs have at least one tag for organization."""
167
+ for dag_id, dag in dagbag.dags.items():
168
+ assert dag.tags, f"DAG {dag_id} has no tags"
169
+
170
+ def test_dags_have_owner(dagbag):
171
+ """Verify all DAGs have an owner in default_args."""
172
+ for dag_id, dag in dagbag.dags.items():
173
+ assert dag.default_args.get("owner"), f"DAG {dag_id} missing owner"
174
+
175
+ def test_no_cycles(dagbag):
176
+ """Verify DAGs don't have circular dependencies."""
177
+ for dag_id, dag in dagbag.dags.items():
178
+ # DagBag already validates this, but explicit test for clarity
179
+ assert dag.topo_sort(), f"DAG {dag_id} has cycles"
180
+ ```
181
+
182
+ **Run tests:**
183
+ ```bash
184
+ # Run all DAG tests
185
+ pytest tests/test_dag_integrity.py -v
186
+
187
+ # Run specific test
188
+ pytest tests/test_dag_integrity.py::test_no_import_errors -v
189
+ ```
190
+
191
+ ## Quick Reference
192
+
193
+ | Task | Command |
194
+ |------|---------|
195
+ | Check import errors | `airflow dags list-import-errors` |
196
+ | Lint DAGs | `airflow dags lint` |
197
+ | Run integrity tests | `pytest tests/test_dag_integrity.py -v` |
198
+ | Trigger manual run | `airflow dags trigger <dag_id>` |
@@ -344,6 +344,7 @@ class Etl(ABC):
344
344
  "hoodie.datasource.write.keygenerator.class": "org.apache.hudi.keygen.ComplexKeyGenerator",
345
345
  "hoodie.datasource.write.precombine.field": self.ts,
346
346
  "hoodie.datasource.write.operation": f"{operation}",
347
+ "hoodie.bulkinsert.sort.mode": "PARTITION_PATH_REPARTITION_AND_SORT",
347
348
  "hoodie.datasource.write.reconcile.schema": True,
348
349
  # Do not set hoodie.*.shuffle.parallelism or spark.sql.shuffle.partitions:
349
350
  # each shuffle task becomes a tiny Hudi file.
@@ -98,6 +98,9 @@ belong before the writer in the job process.
98
98
  `hoodie.bulkinsert.shuffle.parallelism`. A raised shuffle partition count
99
99
  becomes one small file per task on the write path. Size files with
100
100
  `HudiLayout` (default one ~128MB file per partition), not shuffle parallelism.
101
+ - Keep bulk inserts physically grouped and sorted by partition with
102
+ `hoodie.bulkinsert.sort.mode=PARTITION_PATH_REPARTITION_AND_SORT`; override it
103
+ through `extra_hudi_options` only when the job contract requires another layout.
101
104
 
102
105
  ### Repair small files
103
106
 
@@ -361,7 +361,7 @@
361
361
  "id": "first-party",
362
362
  "type": "first-party",
363
363
  "package": "@1aboveio/skills",
364
- "version": "0.17.0"
364
+ "version": "0.18.0"
365
365
  },
366
366
  "contentDigest": "eff6c7b5931bce5b2265a619bccddd371a89df6ce7bc2edc74f11d00060a71dd",
367
367
  "digestExcludes": []
@@ -374,7 +374,7 @@
374
374
  "id": "first-party",
375
375
  "type": "first-party",
376
376
  "package": "@1aboveio/skills",
377
- "version": "0.17.0"
377
+ "version": "0.18.0"
378
378
  },
379
379
  "contentDigest": "08230dc57a53d6526692b50138038abbd80abce1a067e01c52ad6acc182caf7e",
380
380
  "digestExcludes": []
@@ -387,7 +387,7 @@
387
387
  "id": "first-party",
388
388
  "type": "first-party",
389
389
  "package": "@1aboveio/skills",
390
- "version": "0.17.0"
390
+ "version": "0.18.0"
391
391
  },
392
392
  "contentDigest": "9eea7bfba348ddaea1b934c9a7fabdd9b101df8a146e1c93e947da4d335f1d98",
393
393
  "digestExcludes": []
@@ -400,7 +400,7 @@
400
400
  "id": "first-party",
401
401
  "type": "first-party",
402
402
  "package": "@1aboveio/skills",
403
- "version": "0.17.0"
403
+ "version": "0.18.0"
404
404
  },
405
405
  "contentDigest": "58b0556228a9271cf9e33727a05fc08a4fdfd29512ec0936b169f0a55d60e6ef",
406
406
  "digestExcludes": []
@@ -413,9 +413,9 @@
413
413
  "id": "first-party",
414
414
  "type": "first-party",
415
415
  "package": "@1aboveio/skills",
416
- "version": "0.17.0"
416
+ "version": "0.18.0"
417
417
  },
418
- "contentDigest": "0b2d8e0c4d0ae6336c733ff2102d6911a3bd4298394eddd9f40eefaeb884903c",
418
+ "contentDigest": "93241488e62da366d4ecef2a9188a7d0d89f2a5d6db78ad96f0f03bd05b43915",
419
419
  "digestExcludes": []
420
420
  },
421
421
  {
@@ -426,9 +426,9 @@
426
426
  "id": "first-party",
427
427
  "type": "first-party",
428
428
  "package": "@1aboveio/skills",
429
- "version": "0.17.0"
429
+ "version": "0.18.0"
430
430
  },
431
- "contentDigest": "b5af4dccf703362d4f41cac4fdff48305f652a00338d85975a2e5c35ec6bc61c",
431
+ "contentDigest": "0e770c0f20893330214a3e004dd56cad583707603e22c733e36e7276ef8a79ae",
432
432
  "digestExcludes": []
433
433
  },
434
434
  {
@@ -439,7 +439,7 @@
439
439
  "id": "first-party",
440
440
  "type": "first-party",
441
441
  "package": "@1aboveio/skills",
442
- "version": "0.17.0"
442
+ "version": "0.18.0"
443
443
  },
444
444
  "contentDigest": "f048fd00c69f2dc666fc7a3096cfeee6dfba73933bfb69602f3f0e26159798cc",
445
445
  "digestExcludes": []
@@ -452,7 +452,7 @@
452
452
  "id": "first-party",
453
453
  "type": "first-party",
454
454
  "package": "@1aboveio/skills",
455
- "version": "0.17.0"
455
+ "version": "0.18.0"
456
456
  },
457
457
  "contentDigest": "90a7c4e1ad6da1e632ea2c5259e4967ffebb84353adccbca8dfc5d6bc50b60c1",
458
458
  "digestExcludes": []
@@ -465,15 +465,15 @@
465
465
  "id": "first-party",
466
466
  "type": "first-party",
467
467
  "package": "@1aboveio/skills",
468
- "version": "0.17.0"
468
+ "version": "0.18.0"
469
469
  },
470
- "contentDigest": "4d2242d41c7c57e24119b7cd974a88dd97cf526a8f76e6825385060ff86095c8",
470
+ "contentDigest": "2a4193c1b18220520723e145abbdd742f0ea6a93bba57a50b488bf69f95219d3",
471
471
  "digestExcludes": [
472
472
  "coherence/workflow.json"
473
473
  ]
474
474
  }
475
475
  ],
476
- "releaseIdentity": "4a75f070c64c9f24bfbb250be0706a55f6fc3bf071d537200a0a5a91378a4959",
476
+ "releaseIdentity": "2368bd82f8a75798333be8a958c7ce11398839a3dcbe7ee4dbb68d621752a11a",
477
477
  "lifecycleAuthority": "native-skills-cli",
478
478
  "repairRecipe": {
479
479
  "id": "engineering-workflow-dependency-first",
@@ -531,7 +531,7 @@
531
531
  "sourceId": "first-party",
532
532
  "sourceType": "first-party",
533
533
  "package": "@1aboveio/skills",
534
- "version": "0.17.0",
534
+ "version": "0.18.0",
535
535
  "installPath": null,
536
536
  "members": [
537
537
  "harness-runtime",
@@ -547,7 +547,7 @@
547
547
  "commands": [
548
548
  {
549
549
  "transport": "npm",
550
- "command": "npx @1aboveio/skills@0.17.0 install --group engineering-workflow --yes"
550
+ "command": "npx @1aboveio/skills@0.18.0 install --group engineering-workflow --yes"
551
551
  }
552
552
  ],
553
553
  "onFailure": {
@@ -97,7 +97,7 @@ export const WORKFLOW_TRUSTED_SOURCES = deepFreeze({
97
97
  id: 'first-party',
98
98
  type: 'first-party',
99
99
  package: '@1aboveio/skills',
100
- version: '0.17.0',
100
+ version: '0.18.0',
101
101
  },
102
102
  matt: {
103
103
  id: 'matt-pocock',
@@ -237,7 +237,7 @@
237
237
  "id": "first-party",
238
238
  "type": "first-party",
239
239
  "package": "@1aboveio/skills",
240
- "version": "0.17.0"
240
+ "version": "0.18.0"
241
241
  }
242
242
  },
243
243
  {
@@ -248,7 +248,7 @@
248
248
  "id": "first-party",
249
249
  "type": "first-party",
250
250
  "package": "@1aboveio/skills",
251
- "version": "0.17.0"
251
+ "version": "0.18.0"
252
252
  }
253
253
  },
254
254
  {
@@ -259,7 +259,7 @@
259
259
  "id": "first-party",
260
260
  "type": "first-party",
261
261
  "package": "@1aboveio/skills",
262
- "version": "0.17.0"
262
+ "version": "0.18.0"
263
263
  }
264
264
  },
265
265
  {
@@ -270,7 +270,7 @@
270
270
  "id": "first-party",
271
271
  "type": "first-party",
272
272
  "package": "@1aboveio/skills",
273
- "version": "0.17.0"
273
+ "version": "0.18.0"
274
274
  }
275
275
  },
276
276
  {
@@ -281,7 +281,7 @@
281
281
  "id": "first-party",
282
282
  "type": "first-party",
283
283
  "package": "@1aboveio/skills",
284
- "version": "0.17.0"
284
+ "version": "0.18.0"
285
285
  }
286
286
  },
287
287
  {
@@ -292,7 +292,7 @@
292
292
  "id": "first-party",
293
293
  "type": "first-party",
294
294
  "package": "@1aboveio/skills",
295
- "version": "0.17.0"
295
+ "version": "0.18.0"
296
296
  }
297
297
  },
298
298
  {
@@ -303,7 +303,7 @@
303
303
  "id": "first-party",
304
304
  "type": "first-party",
305
305
  "package": "@1aboveio/skills",
306
- "version": "0.17.0"
306
+ "version": "0.18.0"
307
307
  }
308
308
  },
309
309
  {
@@ -314,7 +314,7 @@
314
314
  "id": "first-party",
315
315
  "type": "first-party",
316
316
  "package": "@1aboveio/skills",
317
- "version": "0.17.0"
317
+ "version": "0.18.0"
318
318
  }
319
319
  },
320
320
  {
@@ -325,7 +325,7 @@
325
325
  "id": "first-party",
326
326
  "type": "first-party",
327
327
  "package": "@1aboveio/skills",
328
- "version": "0.17.0"
328
+ "version": "0.18.0"
329
329
  }
330
330
  }
331
331
  ],
@@ -426,7 +426,7 @@
426
426
  "sourceId": "first-party",
427
427
  "sourceType": "first-party",
428
428
  "package": "@1aboveio/skills",
429
- "version": "0.17.0",
429
+ "version": "0.18.0",
430
430
  "installPath": null,
431
431
  "members": [
432
432
  "harness-runtime",
@@ -442,7 +442,7 @@
442
442
  "commands": [
443
443
  {
444
444
  "transport": "npm",
445
- "command": "npx @1aboveio/skills@0.17.0 install --group engineering-workflow --yes"
445
+ "command": "npx @1aboveio/skills@0.18.0 install --group engineering-workflow --yes"
446
446
  }
447
447
  ],
448
448
  "onFailure": {
@@ -3412,7 +3412,7 @@ export const WORKFLOW_PREFLIGHT_COMMANDS = Object.freeze([
3412
3412
 
3413
3413
  const WORKFLOW_VERIFIER_URL = new URL('../../engineering-runtime/scripts/workflow-coherence.mjs', import.meta.url)
3414
3414
  const WORKFLOW_FALLBACK_POLICY_URL = new URL('../generated/workflow-repair-policy.json', import.meta.url)
3415
- export const WORKFLOW_TRUSTED_FALLBACK_POLICY_SHA256 = 'a6d80d500e43b660220f71f9764e5e6c905bf1955d63f8733d115b6c1a601448'
3415
+ export const WORKFLOW_TRUSTED_FALLBACK_POLICY_SHA256 = 'c3d9fac9486de96cb064da61f2bfcb07d4ebc9935be804cfac0c0b9f743c7a30'
3416
3416
  const WORKFLOW_REPAIR_RECIPE_REFERENCE = Object.freeze({
3417
3417
  id: 'engineering-workflow-dependency-first',
3418
3418
  generatedFrom: 'skills/distribution/generated/recipes.json',
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: resolve-release
3
- description: "Release-lane orchestrator, from merge authorization to verified-in-production: assemble PRs into a release branch, cut version + CHANGELOG, promotion PR, deploy a candidate at 0% traffic, validate it, shift traffic, verify the live revision, tag and publish notes (or delete the candidate / roll back). Input is resolve-issues' handoff. NOT for driving issues to PRs (resolve-issues), pipeline debugging (cloud-build/cloud-deploy/cloud-debug), or broken production (app-debug)."
3
+ description: "Slash/explicit-only (/resolve-release). Release-lane orchestrator, from merge authorization to verified-in-production: assemble PRs into a release branch, cut version + CHANGELOG, promotion PR, deploy a candidate at 0% traffic, validate it, shift traffic, verify the live revision, tag and publish notes (or delete the candidate / roll back). Do not auto-select. Input is resolve-issues' handoff. NOT for driving issues to PRs (resolve-issues), pipeline debugging (cloud-build/cloud-deploy/cloud-debug), or broken production (app-debug)."
4
+ disable-model-invocation: true
4
5
  dependencies:
5
6
  - review-pr
6
7
  - e2e-test
@@ -0,0 +1,9 @@
1
+ interface:
2
+ display_name: "Resolve Release"
3
+ short_description: "Slash/explicit-only release lane: merge-ready PRs to verified production"
4
+
5
+ policy:
6
+ # Codex counterpart to SKILL.md disable-model-invocation: true
7
+ # (Claude Code / Pi). Keeps $resolve-release / explicit invoke; blocks
8
+ # description-based auto-selection.
9
+ allow_implicit_invocation: false
@@ -22,7 +22,7 @@ Use these skills by name when the Zod change is part of a larger stack-specific
22
22
  - `web-design-guidelines` for UI review after validation-driven form changes
23
23
  - `vercel-react-best-practices` for React/Next.js performance-sensitive form patterns
24
24
  - `app-debug` for runtime failures caused by validation or payload mismatches
25
- - `airflow-dag-develop` only when Zod-related config or payload contracts affect DAG tooling
25
+ - `airflow` only when Zod-related config or payload contracts affect DAG tooling
26
26
 
27
27
  ---
28
28
 
@@ -1,111 +0,0 @@
1
- ---
2
- name: airflow-dag-develop
3
- description: Airflow DAG architecture and conventions. Read when designing, creating, or debugging DAGs — covers CLI validation, environment-based scheduling, and pytest integrity tests.
4
- user-invocable: true
5
- ---
6
-
7
- # Airflow DAG Development Patterns
8
-
9
- ## 1. Quick DAG Validation with Airflow CLI
10
-
11
- Use these commands for fast feedback during development:
12
-
13
- ```bash
14
- # List import errors (fastest check - catches syntax and import issues)
15
- airflow dags list-import-errors
16
-
17
- # Full lint check (comprehensive - catches best practice violations)
18
- airflow dags lint
19
- ```
20
-
21
- **When to use:**
22
- - `list-import-errors`: During active development for quick syntax/import validation
23
- - `lint`: Before commits to catch style and best practice issues
24
-
25
- ## 2. Environment-Based Schedule Override
26
-
27
- Disable schedules in local development to prevent accidental runs:
28
-
29
- ```python
30
- import os
31
- from datetime import datetime
32
- from airflow import DAG
33
-
34
- def get_schedule(production_schedule: str) -> str | None:
35
- """Return None for local dev, actual schedule for deployed environments.
36
-
37
- Set AIRFLOW_ENV=local in .env for local development.
38
- Cloud Run deployments set AIRFLOW_ENV=dev|staging|prod.
39
- """
40
- env = os.getenv("AIRFLOW_ENV", "local")
41
- if env == "local":
42
- return None
43
- return production_schedule
44
- ```
45
-
46
- **Usage in DAG:**
47
- ```python
48
- with DAG(
49
- dag_id="my_dag",
50
- schedule=get_schedule("0 4 * * *"), # Daily 4 AM in prod, None locally
51
- start_date=datetime(2024, 1, 1),
52
- catchup=False,
53
- ) as dag:
54
- ...
55
- ```
56
-
57
- **Environment setup:**
58
- - **Local:** Add `AIRFLOW_ENV=local` to `.env`
59
- - **Cloud Run:** Set via cloudbuild.yaml substitutions (`_AIRFLOW_ENV: dev|staging|prod`)
60
-
61
- ## 3. Pytest DAG Integrity Tests
62
-
63
- Standard pytest patterns for validating DAG quality:
64
-
65
- ```python
66
- # tests/test_dag_integrity.py
67
- import pytest
68
- from airflow.models import DagBag
69
-
70
- @pytest.fixture(scope="session")
71
- def dagbag():
72
- return DagBag(dag_folder="dags/", include_examples=False)
73
-
74
- def test_no_import_errors(dagbag):
75
- """Verify all DAGs load without import errors."""
76
- assert not dagbag.import_errors, f"DAG import errors: {dagbag.import_errors}"
77
-
78
- def test_dags_have_tags(dagbag):
79
- """Verify all DAGs have at least one tag for organization."""
80
- for dag_id, dag in dagbag.dags.items():
81
- assert dag.tags, f"DAG {dag_id} has no tags"
82
-
83
- def test_dags_have_owner(dagbag):
84
- """Verify all DAGs have an owner in default_args."""
85
- for dag_id, dag in dagbag.dags.items():
86
- assert dag.default_args.get("owner"), f"DAG {dag_id} missing owner"
87
-
88
- def test_no_cycles(dagbag):
89
- """Verify DAGs don't have circular dependencies."""
90
- for dag_id, dag in dagbag.dags.items():
91
- # DagBag already validates this, but explicit test for clarity
92
- assert dag.topo_sort(), f"DAG {dag_id} has cycles"
93
- ```
94
-
95
- **Run tests:**
96
- ```bash
97
- # Run all DAG tests
98
- pytest tests/test_dag_integrity.py -v
99
-
100
- # Run specific test
101
- pytest tests/test_dag_integrity.py::test_no_import_errors -v
102
- ```
103
-
104
- ## Quick Reference
105
-
106
- | Task | Command |
107
- |------|---------|
108
- | Check import errors | `airflow dags list-import-errors` |
109
- | Lint DAGs | `airflow dags lint` |
110
- | Run integrity tests | `pytest tests/test_dag_integrity.py -v` |
111
- | Trigger manual run | `airflow dags trigger <dag_id>` |