Add AIP-to-user-stories skill for generating recipe playbooks from AIPs#65776
Add AIP-to-user-stories skill for generating recipe playbooks from AIPs#65776Lee-W merged 1 commit intoapache:mainfrom
Conversation
|
You need to rebase though :) |
c'est la vie 🤷♂️ |
2c6dcf2 to
4f5ab98
Compare
4f5ab98 to
03b5705
Compare
03b5705 to
f33ec3d
Compare
|
Added an example for "pre mode" using AIP-93. @jroachgolf84 @gyli - would appreciate your comments on the output's usefulness. |
jroachgolf84
left a comment
There was a problem hiding this comment.
I like it, I think it makes sense!
f33ec3d to
c35fd47
Compare
Adds a Claude Code skill that generates verified recipe playbooks from Airflow Improvement Proposals (AIPs). Supports two modes: post-implementation (generates verified recipes from actual codebase code and PRs) and pre-implementation (generates speculative user stories to help AIP authors validate design before coding). Supporting changes: - Update .gitignore to track .claude/skills/ while ignoring other .claude/ content - Add CODEOWNERS entry for .claude/skills/ - Widen license header detection window to cover files with frontmatter - Narrow pre-commit license-check excludes (instructions and skill files now carry headers, so the broad excludes are no longer needed) - Add Apache license header to code-review.instructions.md - Exclude .claude/skills from blacken-docs (Markdown code fences are not standalone Python files)
c35fd47 to
c033ed8
Compare
Backport failed to create: v3-2-test. View the failure log Run detailsNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
You can attempt to backport this manually by running: cherry_picker 0960ad2 v3-2-testThis should apply the commit to the v3-2-test branch and leave the commit in conflict state marking After you have resolved the conflicts, you can continue the backport process by running: cherry_picker --continueIf you don't have cherry-picker installed, see the installation guide. |
…#66275) The `insert-license` agentic-markdown hook on v3-2-test cannot detect license headers placed below YAML frontmatter, so it prepends a second license at the top of the file. That break also confuses markdownlint: the `---` line of the frontmatter, after a blank line, gets parsed as a setext-H2 underline, and every later `# ...` heading then fails MD003. This started failing on v3-2-test once #66169 landed `.github/skills/prepare-providers-documentation/SKILL.md`, which is not on the existing per-file exclude list. The same issue was solved on `main` in #65776 (commit 0960ad2) by passing `--detect-license-in-X-top-lines '30'` to the SHORT_LICENSE hook so it recognises an existing license inside the first 30 lines of the file. Backport only the `.pre-commit-config.yaml` change from that PR — not the new `aip-user-stories` skill that shipped alongside it — and add the matching license header inside `.github/instructions/code-review.instructions.md` (so it does not regress once `.github/instructions/` is no longer blanket-excluded).
Summary
Adds a Claude Code skill (
/aip-user-stories) that generates recipe playbooks from Airflow Improvement Proposals (AIPs). The skill has two modes:Changes
.claude/skills/aip-user-stories/SKILL.md— full skill definition with 6-phase workflow for both modes, including AIP parsing, PR discovery, codebase analysis, recipe proposal, generation, and assembly..claude/skills/aip-user-stories/references/playbook-template.md— output template with sections for prerequisites, overview, recipes (post mode), and user stories (pre mode)..gitignore: Changed.claude/exclusion to.claude/*with!.claude/skills/so the skill files are tracked in git while other Claude Code artifacts remain ignored..github/CODEOWNERS: Added/.claude/skills/entry with the same reviewers as other agentic instruction files..pre-commit-config.yaml:--detect-license-in-X-top-lines 10to the short-license hook so it finds the Apache header in files with YAML frontmatter (likeSKILL.md).code-review.instructions.mdand GitHub skill files now carry their own license headers, so the broad excludes are no longer needed..claude/skillstoblacken-docsexclude (Markdown code fences in skill files are illustrative, not standalone Python)..github/instructions/code-review.instructions.md: Added Apache License header (was previously excluded from the license check instead).Appendix
Example output for AIP-93 (pre mode)
AIP-93 Asset Watermarks — Playbook
Prerequisites
DbTaskStateBackendusing the metadata database). Configurable via[task_state] backendinairflow.cfg.apache-airflow-providers-amazon.apache-airflow-providers-postgres).Overview
Incremental processing is the most common pattern in data orchestration, yet Airflow has never offered first-class support for persisting state — watermarks, cursors, or checkpoints — across executions. DAG authors using traditional operators resort to XCom or Variables, both of which are poor fits: XCom is scoped to a single DAG run and cleared on retry, while Variables are global singletons with no asset awareness.
For event-driven scheduling via Asset Watchers (AIP-82), the problem is worse.
BaseEventTriggerinstances running in the Triggerer have no built-in mechanism to persist state between invocations. Attempts to store state inside trigger instances or use Variables from async trigger code have proven fragile and unreliable.AIP-93 proposes making
BaseEventTriggerAsset-aware and leveraging AIP-103's state management infrastructure to give triggers and watchers a clean, scoped interface for storing and retrieving watermarks. The core pattern is: on each run, read the last watermark, scan for changes since that watermark, update the watermark, and yield events. This AIP also introduces a decorator-based authoring experience for Asset watchers, lowering the barrier for the community to build and distribute event-driven integrations.Relationship to AIP-103: AIP-103 provides the persistence layer —
AssetScope,BaseTaskStateBackend, and the/state/asset/{asset_id}/{key}Execution API endpoints. AIP-93 builds on top of this to define the patterns and APIs for using that layer within Asset Watchers and Event Triggers.User Stories
1. Persisting a Timestamp Watermark Across Trigger Runs
Goal: Store and retrieve a simple timestamp watermark so an Event Trigger can resume scanning from where it last left off, rather than re-processing everything from the beginning.
This story illustrates the fundamental watermark pattern. The trigger reads a previously stored timestamp via
self.asset_state.aget(), scans only for changes after that point, and writes the new watermark back viaself.asset_state.aset(). Theasset_stateobject is automatically scoped to the Asset this trigger is watching — no manual key namespacing needed.Open Design Questions:
asset_stateget injected into the trigger? CurrentlyBaseEventTrigger(inairflow-core/src/airflow/triggers/base.py) has noasset_stateattribute. Does the Triggerer inject it when the trigger is associated with anAssetWatcher, or does the trigger need to explicitly request it? What happens if the same trigger class is used outside of an AssetWatcher context?TriggerEventis lost? If the Triggerer crashes betweenaset()andyield TriggerEvent(...), the watermark advances but no event is emitted. On restart, the data in that window is silently skipped. Should watermark updates be transactional with event submission?strfor values. Should AIP-93 define a convention (ISO 8601 for timestamps, JSON for structured values), or leave it to trigger authors?2. Using Asset State as a General Key-Value Store in Triggers
Goal: Store arbitrary structured state — not just timestamps — across trigger invocations, such as cursor positions, page tokens, or sets of previously seen IDs.
This story demonstrates using multiple state keys for different aspects of the same incremental process. The cursor tracks pagination position while the seen IDs set provides deduplication. Both are persisted and scoped to the same Asset.
Open Design Questions:
DbTaskStateBackendstores values as strings in the metadata DB. For a growing set of seen IDs, this could become arbitrarily large. Should AIP-93 recommend or enforce limits? Should large state values use a different backend (e.g., object storage)?page_cursorandseen_idsrequires two separate round-trips through the Execution API. Aget_many(keys)/set_many(mapping)API would reduce latency, especially for triggers managing multiple state keys.strvalues. Should triggers be responsible for JSON serialization, or should the SDK provide typed helpers (e.g.,asset_state.get_json("seen_ids"),asset_state.set_json("seen_ids", data))?AssetWatcherfrom an Asset definition, is the associated state garbage-collected, orphaned, or retained for potential re-attachment?3. Watching an S3 Bucket for New Files
Goal: Incrementally detect new objects in an S3 bucket by persisting a timestamp watermark, avoiding a full bucket scan on every trigger invocation.
This story shows the most common real-world use case for asset watermarks: monitoring cloud object storage. The trigger uses a timestamp watermark to avoid rescanning the entire bucket prefix on every poll. The DAG that schedules on this Asset receives the list of new keys via the
TriggerEventpayload.Open Design Questions:
list_keysimmediately. If the watermark advances past the object'sLastModifiedtimestamp before it becomes visible, the object is permanently skipped. Should triggers implement a configurable "lookback window" that overlaps the previous scan by a safety margin?TriggerEventwith a huge payload. Should there be a mechanism for batched events, or is pagination the trigger author's responsibility?catchup=True? If a DAG has been paused and accumulates many events, does each event create a separate DAG run, or are they coalesced? How does this interact with the watermark — does each DAG run see a different watermark window?4. Watching a SQL Table for New/Updated Rows
Goal: Detect new or modified rows in a relational database table by tracking a high-water mark on an
updated_atcolumn.This story mirrors the S3 pattern but for relational databases. The watermark column (typically
updated_ator an auto-incrementing ID) determines which rows are "new." The trigger reads the last watermark from asset state, queries for rows beyond it, and advances the watermark to the maximum value in the result set.Open Design Questions:
BaseEventTriggeroffer parameterized query helpers?updated_atcatches inserts and updates but misses deletes. Should AIP-93 address soft-delete patterns (e.g., anis_deletedflag) or is that out of scope?last_watermarkisNone, causing a full table scan. For large tables this could be catastrophic. Should there be a convention for setting an initial watermark (e.g., "only look at rows from the last 24 hours")?datetime.now()), which avoids clock skew. Should this pattern be documented as a best practice, or should AIP-93 enforce it?5. Building a Custom Asset Watcher for Any Data Source
Goal: Apply the watermark pattern to a non-standard data source (e.g., a REST API, message queue, or custom service) using the same state management primitives.
This story generalizes the watermark pattern beyond cloud storage and SQL. It uses an event ID as the watermark (rather than a timestamp), demonstrates multiple state keys for different concerns (cursor vs. statistics), and shows the pattern applied to an HTTP API.
Open Design Questions:
WatermarkTriggerbase class could reduce boilerplate and enforce best practices. But would this over-constrain trigger authors who need different patterns?aset()call fails after a successful scan, the trigger might yield an event without persisting the watermark, causing duplicate processing on the next run. Should there be retry/rollback semantics?await self.asset_state.list_keys())? This would help with debugging and building generic monitoring tools.BaseEventTrigger.serialize()returns the classpath and kwargs. The watermark state is NOT part of kwargs (it's in the state backend). Is this separation clear enough, or will trigger authors accidentally try to store state in kwargs?6. Decorator-Based Asset Watcher Authoring
Goal: Define Asset watching logic using a simple decorator instead of writing a full
BaseEventTriggersubclass, lowering the barrier to entry for building event-driven integrations.This story addresses one of the AIP's explicit goals: a more intuitive, decorator-based authoring experience. Instead of subclassing
BaseEventTrigger, implementingserialize(), and managing the async generator protocol, the user writes a simple function that receivesasset_stateand returns a payload (orNoneto retry).Open Design Questions:
BaseEventTrigger.serialize()must return a classpath and kwargs for reconstruction by the Triggerer. A decorated function isn't a class. Does the decorator generate a synthetic class, or does it use a genericFunctionTriggerthat stores the function reference and kwargs?bucketandprefixare passed when attaching the watcher. These need to survive serialization. Should they be stored in the trigger kwargs, or in the asset state?async def run(self) -> AsyncIterator[TriggerEvent]withyield. The decorator replaces this with a simpler return-value protocol. Can decorated watchers still useyieldfor streaming multiple events in one invocation?cleanup()andon_kill()? These lifecycle methods exist onBaseTrigger. If the decorated function is the entire trigger, how does the user define cleanup logic? Additional decorators (@watch_s3_bucket.on_kill)? Optional callback parameters?7. Asset-Aware Event Triggers
Goal: Make
BaseEventTriggeraware of which Asset it is watching, so the trigger can automatically scope its state and behavior to the correct asset without manual configuration.This story demonstrates the key architectural change AIP-93 proposes: making triggers aware of the asset they're monitoring. Currently,
BaseEventTrigger(inairflow-core/src/airflow/triggers/base.py) has no knowledge of which asset it serves — the connection is external, viaAssetWatcherModel. With asset-awareness, a single trigger class can adapt its behavior based on the asset URI, andasset_stateis automatically scoped to the correct asset.Open Design Questions:
self.assetexpose? Is it a fullAssetobject (withname,uri,group,extra,watchers) or a lightweight reference? Loading the full object might require a database query. Should it be lazy-loaded?AssetWatcherModelhas a composite primary key(asset_id, trigger_id). If the same trigger instance is assigned to multiple assets, whichself.assetdoes it see? Is a trigger always 1:1 with an asset in the watcher context?BaseEventTrigger.hash()uses classpath and kwargs to identify unique triggers. Two watchers usingGenericFileWatchTrigger()with the samepoke_intervalbut different assets would hash to the same value. Should the asset identity be part of the trigger hash?self.asset.extrabe writable? Assetextrametadata is currently user-defined at DAG parse time. If a trigger could update it (e.g., adding alast_file_countmetric), it would blur the line between asset metadata and asset state. Shouldextraremain read-only with all mutable state going throughasset_state?8. Partitioned Asset Watermarks
Goal: Track independent watermarks per partition of a partitioned Asset, so each partition's incremental processing state is isolated.
This story combines AIP-93 watermarks with Airflow's existing asset partition infrastructure (
PartitionedAssetTimetableintask-sdk/src/airflow/sdk/definitions/timetables/assets.py). Each date partition (e.g.,dt=2025-01-15/) gets its own watermark stored under a namespaced key (watermark:2025-01-15). When new files land in a partition, only that partition's watermark advances, and the resulting DAG run is scoped to that partition.Open Design Questions:
AssetScopeor a newPartitionScope? AIP-103 definesAssetScope(asset_id=str)but no partition-level scoping. The pattern above manually namespaces keys withwatermark:{partition_key}. Should AIP-103 be extended withAssetPartitionScope(asset_id, partition_key)for first-class partition support?TriggerEventmap to partitioned DAG runs? The currentTrigger.submit_event()callsAssetManager.register_asset_change()without a partition key. How does the event'spartition_keypropagate from the trigger through toAssetEvent.partition_keyand ultimately toAssetPartitionDagRun?dt=2025-01-20/is created for the first time, there's no watermark for it. The trigger would do a full scan of that partition. Is this the desired behavior, or should new partitions inherit a default watermark (e.g., "partition creation time")?Example output for AIP-76 (post mode)
AIP-76 Asset Partitions — Playbook
Prerequisites
airflow db migrate).airflow.sdk. No provider packages are needed for basic partition functionality.Overview
AIP-76 introduces asset partitions to Airflow, enabling data processing at finer granularity than a DAG's schedule. Before partitions, a DAG run represented a single unit of work for an entire dataset. With partitions, each DAG run can target a specific slice — an hour's worth of data, a region, or a combination of both — and downstream DAGs trigger only when matching partitions from all required upstream assets are available.
The implementation has two sides: producers emit partitioned asset events using
CronPartitionTimetable, and consumers react to those events usingPartitionedAssetTimetable. Between them, partition mappers transform partition keys so that upstream and downstream granularities can differ (e.g., hourly events roll up into daily partitions). The scheduler tracks partition key alignment across multiple upstream assets and only creates a downstream DAG run when all required partitions match.Partitioned events are a separate scheduling path from regular asset-triggered DAGs. A
PartitionedAssetTimetableconsumer ignores non-partitioned events, and a regularAssetTriggeredTimetableconsumer ignores partitioned events.Recipes
1. Produce Hourly Partitioned Events with CronPartitionTimetable
Goal: Set up a producer DAG that emits asset events tagged with a partition key on every cron tick.
Verified — from
airflow-core/src/airflow/example_dags/example_asset_partition.pyCronPartitionTimetableworks likeCronTriggerTimetablebut additionally stamps each DAG run — and the asset events it emits — with a partition key derived from the run's scheduled time. The defaultkey_formatis"%Y-%m-%dT%H:%M:%S", so a run scheduled at2026-03-10 09:00 UTCproduces partition key"2026-03-10T09:00:00".You can customize the key format and offset:
2. Produce Partitioned Events with the @asset Decorator
Goal: Use the compact
@assetdecorator to define a single-task partitioned producer without an explicit DAG block.Verified — from
airflow-core/src/airflow/example_dags/example_asset_partition.pyThe
@assetdecorator creates both a DAG and a single task in one step. The resulting asset can be referenced by name or URI in downstreamPartitionedAssetTimetableconsumers, just like any other asset.3. Consume a Single Partitioned Asset
Goal: Set up a downstream DAG that triggers when a partitioned event arrives from one upstream asset.
Verified — from
airflow-core/src/airflow/example_dags/example_asset_partition.pyWhen no
default_partition_mapperis specified,IdentityMapperis used — the downstream partition key is the same as the upstream key. The consumer DAG gets one DAG run per unique partition key emitted by the upstream asset. Non-partitioned events from the same asset are ignored.4. Consume Multiple Partitioned Assets with Aligned Keys
Goal: Trigger a downstream DAG only when all required upstream assets have emitted events with a matching partition key.
Verified — from
airflow-core/src/airflow/example_dags/example_asset_partition.pyThe
&operator creates anAssetAllcondition — all three assets must have a matching partition key before the downstream DAG triggers. TheStartOfHourMappernormalizes each upstream key (e.g.,"2026-03-10T09:15:00") to hour granularity ("2026-03-10T09"), so events from different producers that fall within the same hour are aligned.If Team A emits
"2026-03-10T09:00:00"and Team B emits"2026-03-10T09:15:00",StartOfHourMappermaps both to"2026-03-10T09". The downstream DAG won't run until Team C also emits an event that maps to the same key.5. Chain Partitioned Assets Across Multiple Levels
Goal: Propagate partition keys through a multi-level pipeline: producer → consumer → consumer.
Adapted from
airflow-core/src/airflow/example_dags/example_asset_partition.pyEach level in the chain is an independent partition-aware DAG.
team_a_player_statsproduces events with full-timestamp keys.combined_player_statsmaps those to hourly keys and emits new partition events.compute_player_oddspicks up those hourly keys with the defaultIdentityMapper. The partition key flows through the chain, with each level optionally transforming the granularity.6. Aggregate Hourly Partitions to Daily with Temporal Mappers
Goal: Roll up hourly upstream partition keys to daily granularity so the downstream DAG triggers once per day, after any hourly event within that day.
Adapted from
airflow-core/src/airflow/example_dags/example_asset_partition.pyStartOfDayMappertruncates timestamp-based keys to"%Y-%m-%d"format. An upstream event with key"2026-03-10T09:15:00"maps to downstream key"2026-03-10". The downstream DAG run is created when the first hourly event for that day arrives (since a single upstream asset is required). If you need all 24 hourly partitions before triggering, that requires rollup support which is tracked for future implementation.Available temporal mappers and their output formats:
StartOfHourMapper%Y-%m-%dT%H2026-03-10T09StartOfDayMapper%Y-%m-%d2026-03-10StartOfWeekMapper%Y-%m-%d (W%V)2026-03-09 (W11)StartOfMonthMapper%Y-%m2026-03StartOfQuarterMapper%Y-Q{quarter}2026-Q1StartOfYearMapper%Y2026All temporal mappers accept optional
input_formatandoutput_formatparameters to override the defaults:7. Map Composite Partition Keys with ProductMapper
Goal: Handle multi-dimensional partition keys (e.g.,
"region|timestamp") by applying a different mapper to each segment.Verified — from
airflow-core/src/airflow/example_dags/example_asset_partition.pyProductMappersplits the incoming key by a delimiter ("|"by default), applies one mapper per segment, and rejoins the results. For input key"us|2026-03-10T09:00:00":"us") →IdentityMapper→"us""2026-03-10T09:00:00") →StartOfDayMapper→"2026-03-10""us|2026-03-10"ProductMapperrequires at least two mappers (positional-only) and accepts a custom delimiter:8. Restrict Partition Keys with AllowedKeyMapper
Goal: Accept only specific partition keys (e.g., region codes) and reject all others.
Verified — from
airflow-core/src/airflow/example_dags/example_asset_partition.pyAllowedKeyMappervalidates that upstream partition keys belong to a fixed set. If the upstream emits"us", the downstream triggers with key"us". If it emits"latam"(not in the list), the event is silently ignored — no downstream DAG run is created for that partition.This is useful for segment-based partitioning where partition keys are categorical values rather than timestamps.
9. Configure Per-Asset Mapper Overrides
Goal: Apply different partition mappers to different upstream assets in the same consumer DAG.
Adapted from
airflow-core/docs/authoring-and-scheduling/assets.rstThe
default_partition_mapperapplies to all upstream assets unless overridden inpartition_mapper_config. Here,hourly_salesevents are mapped withStartOfDayMapper(truncating hourly keys to daily), whiledaily_targetsevents are passed through unchanged withIdentityMapper.The downstream DAG triggers only when both assets produce a matching partition key after their respective mappers are applied. If
hourly_salesemits"2026-03-10T09:00:00"(mapped to"2026-03-10") anddaily_targetsemits"2026-03-10"(unchanged), the keys match and the DAG run is created.You can also use
Asset.ref()to reference assets by name or URI when the fullAssetobject isn't available:Caution with mismatched mappers: If mappers produce incompatible key formats, the downstream DAG will never trigger. For example, mapping one asset with
StartOfYearMapper(output:"2026") and another withStartOfHourMapper(output:"2026-03-10T09") means the keys will never align.10. Read partition_key Inside a Running Task
Goal: Access the resolved partition key at task execution time to drive partition-specific logic.
Verified — from
airflow-core/src/airflow/example_dags/example_asset_partition.pyThe
dag_run.partition_keyattribute is available on theDagRuninstance passed to every task. For partitioned DAG runs, it contains the resolved downstream partition key (after mapper transformation). For non-partitioned DAG runs, it isNone.11. Trigger a Partitioned DAG Run via REST API
Goal: Manually materialize a specific partition by triggering a DAG run with an explicit partition key.
Verified — from
airflow-core/docs/authoring-and-scheduling/assets.rstThe REST API accepts
partition_keyin the request body when triggering a DAG run. The partition key is stored directly on the DAG run and is accessible viadag_run.partition_keyin tasks. This bypasses the normal partition mapper pipeline — the key you provide is used as-is.You can also trigger a partitioned DAG run from the Airflow UI via the "Trigger DAG" dialog, which includes a
partition_keyfield.Not Yet Implemented
The following AIP-76 features are proposed but not present in the current codebase:
PartitionAtRuntime— Dynamic partition keys determined during execution based on runtime data (e.g., querying a database for active customers). Tracked in #44146.PartitionByInterval/PartitionBySequence/PartitionByProduct— The AIP proposes these as high-level partition definition classes on assets. The implementation instead usesCronPartitionTimetablefor producing partitions andPartitionMappersubclasses for consuming them.PartitionKeysubclasses — The AIP proposes rich, typed partition keys (e.g., aPosition(x, y, z)class). The implementation uses plain string keys.Manifest / ADR
Definition: AIP-to-User-Stories Playbook Skill
1. Intent & Context
/aip-user-stories) that auto-detects its mode from inputs: (1) post-implementation (PR URLs provided) generates verified recipe/how-to guides, and (2) pre-implementation (no PR URLs) generates user stories with speculative code to help AIP authors validate their design.2. Approach
Architecture: Single skill directory at
/home/USER/repositories/airflow/.claude/skills/aip-user-stories/containing:SKILL.md— main skill prompt with phase-based execution flowreferences/playbook-template.md— output template for both modes (recipes for playbook, user stories for assist)Two modes, auto-detected from inputs:
Invocation:
/aip-user-stories <AIP-URL> [<PR-URL>...] [<file>...]Post-implementation mode (PRs provided):
https://prefix; everything else is a file path.ghCLI, read local example/source/test files..claude/aip-{number}.md.Pre-implementation mode (no PRs):
# PROPOSED API — not yet implemented), and open design questions the AIP should address..claude/aip-{number}.md.Execution Order:
Risk Areas:
Trade-offs:
3. Global Invariants (The Constitution)
[INV-G1] The skill prompt must not contain ambiguous instructions, vague language, or implicit expectations. No prescriptive HOW (step-by-step tool instructions), no arbitrary limits, no weak language ("try to", "maybe"). No AI-typical vocabulary (delve, tapestry, landscape, leverage, harness, navigate, seamless, robust, transformative, etc.) in its own prose or output instructions. | Verify: prompt quality review + vocabulary check
[INV-G2] The skill directory must have SKILL.md + references/playbook-template.md. Template in companion file, not inlined. | Verify: directory structure check
[INV-G3] SKILL.md must have a description field that follows the What + When + Triggers pattern (trigger specification, not human-readable summary). Must include trigger terms for both modes (pre and post). | Verify: description check
[INV-G4] In post mode, code blocks use three tiers: (1) Verified — pattern exists verbatim in codebase; clean code, no markers. (2) Adapted — combines verified components in a new way (e.g., swapping one mapper for another); clean code, brief note below the block: "Adapted from [source file]". (3) Unverified — no codebase evidence; uses placeholder:
# TODO: Implement [description]/# See: [reference]/.... Verification sources: source code, example DAGs, and test files. | Verify: three-tier instruction[INV-G5] In pre mode, ALL code blocks must be marked as speculative with
# PROPOSED API — not yet implemented. No code should appear to be verified or production-ready. | Verify: pre mode code marking[INV-G6] In post mode, the skill must follow implementation truth, not AIP spec. When AIP proposes APIs that differ from implementation, the playbook follows the implementation. | Verify: source of truth instruction
[INV-G7] SKILL.md must include a Gotchas section with at least 3 specific, actionable failure modes grounded in this skill's domain. | Verify: gotchas check
[INV-G8] The playbook template (references/playbook-template.md) must include: prerequisites/version section, brief conceptual overview section, and recipe/story sections. Must NOT include API reference, migration guide, or troubleshooting sections. | Verify: template structure
[INV-G9] The skill must write output to
.claude/aip-{number}.mdwhere{number}is the AIP number extracted from the URL or provided by the user. | Verify: output path instruction[INV-G10] In pre mode, each user story must include open design questions probing: (a) API ergonomics — easy to use correctly, hard to misuse? (b) edge cases — unusual inputs or configurations? (c) compatibility — interaction with existing Airflow patterns (e.g., catchup, backfill, dynamic task mapping)? (d) implementation feasibility — constraints the AIP hasn't addressed? Generic questions like "what about error handling?" don't count. | Verify: design questions instruction
4. Process Guidance (Non-Verifiable)
.claude/aip-{number}.mdalready exists, ask user before overwriting.https://github.com/.../pull/...URLs are in the arguments, use post mode. Otherwise, pre mode. No flags needed.5. Known Assumptions
.claude/skills/) | Impact if wrong: post mode code verification fails; pre mode unaffected.ghCLI is available and authenticated for fetching PR data. | Default: yes | Impact if wrong: PR data cannot be fetched in post mode; pre mode unaffected.6. Deliverables (The Work)
Deliverable 1: SKILL.md — Main Skill Prompt
The skill file at
/home/USER/repositories/airflow/.claude/skills/aip-user-stories/SKILL.mdcontaining the skill prompt and both mode definitions. References the template inreferences/playbook-template.md.Acceptance Criteria:
[AC-1.1] SKILL.md has valid YAML frontmatter with
name: aip-user-storiesand a trigger-pattern description covering both modes. | Verify: frontmatter check[AC-1.2] SKILL.md defines argument parsing:
<AIP-URL> [<PR-URL>...] [<file>...]. PR URLs auto-detected as GitHub pull request URLs. Mode auto-detected: PR URLs present → post, absent → pre. No arguments prints usage. | Verify: argument parsing[AC-1.3] SKILL.md defines the six execution phases for post mode (Parse, Fetch, Analyze, Propose, Generate, Assemble) with clear inputs/outputs. | Verify: post mode phases
[AC-1.4] SKILL.md defines the six execution phases for pre mode (Parse, Fetch, Analyze, Propose, Generate, Assemble) with appropriate differences: no code verification, speculative code, design questions. | Verify: pre mode phases
[AC-1.5] Propose phase presents items as a numbered list with title + one-liner, grouped by concept, and waits for user approval. | Verify: proposal format
[AC-1.6] Handles unimplemented AIP features (post mode): asks user whether to include with placeholder code or skip. | Verify: unimplemented features
[AC-1.7] Error handling: fail with clear message for inaccessible URLs. For AIP content, URL fetch and pasted content are equally valid input paths — present both options rather than trying URL first and falling back. | Verify: error handling
[AC-1.8] Gotchas section with at least 3 specific, actionable failure modes. | Verify: gotchas quality
[AC-1.9] Version detection: post mode greps the PR diff for
versionadded::directives; if not found, asks the user. Pre mode asks the user for target version. | Verify: version detection[AC-1.10] Multiple PR handling (post mode): latest PR with code changes takes priority; docs-only PRs inform context. | Verify: multi-PR
[AC-1.11] SKILL.md references the playbook template in references/playbook-template.md and instructs the LLM to follow it for output structure in both modes. | Verify: template reference
Deliverable 2: Playbook Template
Reference template at
/home/USER/repositories/airflow/.claude/skills/aip-user-stories/references/playbook-template.md.Acceptance Criteria:
[AC-2.1] Template includes a prerequisites section with placeholders for Airflow version and required packages. | Verify: prerequisites section
[AC-2.2] Template includes a conceptual overview section (2-3 paragraphs from AIP motivation). | Verify: overview section
[AC-2.3] Template defines recipe structure (post mode): title, goal, per-recipe prerequisites, code block(s), explanation. | Verify: recipe structure
[AC-2.4] Template defines user story structure (pre mode): title, goal, speculative code (marked as proposed), open design questions. | Verify: story structure
[AC-2.5] Template does NOT include API reference, migration guide, or troubleshooting sections. | Verify: scope check
Deliverable 3: Validation with AIP-76
Manual review that the skill would produce coherent output for AIP-76 inputs.
Acceptance Criteria:
[AC-3.1] In post mode (AIP-76 + PR docs: asset partition #63262 + example_asset_partition.py), the skill would identify recipe candidates covering: CronPartitionTimetable, PartitionedAssetTimetable, temporal mappers, ProductMapper, AllowedKeyMapper. | Verify: manual review
[AC-3.2] In post mode, PartitionAtRuntime (from AIP-76) would be flagged as unimplemented and the user asked how to handle it. | Verify: manual review
[AC-3.3] In pre mode (AIP-76 only, no PRs), the skill would generate user stories using AIP's proposed API (PartitionByInterval, PartitionBySequence, PartitionAtRuntime) with speculative code and design questions. | Verify: manual review
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Opus 4.6 following the guidelines
{pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.