jobwright 0.0.1__tar.gz
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.
- jobwright-0.0.1/.claude-plugin/marketplace.json +12 -0
- jobwright-0.0.1/.claude-plugin/plugin.json +12 -0
- jobwright-0.0.1/.github/workflows/ci.yml +36 -0
- jobwright-0.0.1/.github/workflows/publish.yml +32 -0
- jobwright-0.0.1/.gitignore +23 -0
- jobwright-0.0.1/CHANGELOG.md +38 -0
- jobwright-0.0.1/CONTRIBUTING.md +31 -0
- jobwright-0.0.1/LICENSE +21 -0
- jobwright-0.0.1/PKG-INFO +110 -0
- jobwright-0.0.1/README.md +80 -0
- jobwright-0.0.1/adapters/platform/airflow.md +45 -0
- jobwright-0.0.1/adapters/platform/databricks.md +72 -0
- jobwright-0.0.1/adapters/platform/dbt.md +40 -0
- jobwright-0.0.1/adapters/platform/snowflake_tasks.md +51 -0
- jobwright-0.0.1/bin/selftest.sh +29 -0
- jobwright-0.0.1/docs/PUBLISHING.md +54 -0
- jobwright-0.0.1/examples/sample-airflow/dags/demo_dag.py +8 -0
- jobwright-0.0.1/examples/sample-airflow/jobwright.config.yaml +18 -0
- jobwright-0.0.1/examples/sample-databricks/databricks/job_definitions/prod/JOB-1_Demo_Report.json +3 -0
- jobwright-0.0.1/examples/sample-databricks/jobs/JOB-1_Demo_Report/claude.md +5 -0
- jobwright-0.0.1/examples/sample-databricks/jobs/JOB-1_Demo_Report/demo.py +11 -0
- jobwright-0.0.1/examples/sample-databricks/jobs/JOBS.md +14 -0
- jobwright-0.0.1/examples/sample-databricks/jobs/OBJECTS.md +13 -0
- jobwright-0.0.1/examples/sample-databricks/jobwright.config.yaml +48 -0
- jobwright-0.0.1/hooks/deploy_safety.py +242 -0
- jobwright-0.0.1/hooks/hooks.json +28 -0
- jobwright-0.0.1/hooks/regenerate_jobs_index.py +104 -0
- jobwright-0.0.1/hooks/session_start.sh +24 -0
- jobwright-0.0.1/jobwright/__init__.py +8 -0
- jobwright-0.0.1/jobwright/_templates/job/claude.md.j2 +25 -0
- jobwright-0.0.1/jobwright/_templates/job/job_definition.json.j2 +18 -0
- jobwright-0.0.1/jobwright/_templates/job/notebook_header.py.j2 +17 -0
- jobwright-0.0.1/jobwright/_templates/job/task_definition.sql.j2 +9 -0
- jobwright-0.0.1/jobwright/_templates/repo/AGENTS.md.j2 +43 -0
- jobwright-0.0.1/jobwright/cli.py +336 -0
- jobwright-0.0.1/jobwright/config.py +304 -0
- jobwright-0.0.1/jobwright/jobsindex.py +349 -0
- jobwright-0.0.1/jobwright/platforms/__init__.py +57 -0
- jobwright-0.0.1/jobwright/platforms/airflow.py +118 -0
- jobwright-0.0.1/jobwright/platforms/base.py +170 -0
- jobwright-0.0.1/jobwright/platforms/databricks.py +293 -0
- jobwright-0.0.1/jobwright/platforms/dbt.py +84 -0
- jobwright-0.0.1/jobwright/platforms/snowflake_tasks.py +151 -0
- jobwright-0.0.1/jobwright/policy.py +109 -0
- jobwright-0.0.1/jobwright/scaffolder.py +96 -0
- jobwright-0.0.1/jobwright/tools/__init__.py +5 -0
- jobwright-0.0.1/jobwright/tools/check_dependency_vulns.py +154 -0
- jobwright-0.0.1/jobwright/tools/check_notebook_syntax.py +71 -0
- jobwright-0.0.1/jobwright/tools/job_doc_lint.py +88 -0
- jobwright-0.0.1/jobwright/tools/schema_compliance.py +85 -0
- jobwright-0.0.1/jobwright/tools/validate_job.py +160 -0
- jobwright-0.0.1/jobwright/tools/validate_job_definitions.py +62 -0
- jobwright-0.0.1/jobwright.config.example.yaml +48 -0
- jobwright-0.0.1/pyproject.toml +67 -0
- jobwright-0.0.1/skills/architecture-audit/SKILL.md +27 -0
- jobwright-0.0.1/skills/build-jobs-index/SKILL.md +24 -0
- jobwright-0.0.1/skills/configure-workspace/SKILL.md +26 -0
- jobwright-0.0.1/skills/document-job/SKILL.md +23 -0
- jobwright-0.0.1/skills/onboard/SKILL.md +20 -0
- jobwright-0.0.1/skills/safe-deploy/SKILL.md +27 -0
- jobwright-0.0.1/skills/scaffold-job/SKILL.md +26 -0
- jobwright-0.0.1/skills/start-job/SKILL.md +20 -0
- jobwright-0.0.1/skills/triage-failure/SKILL.md +21 -0
- jobwright-0.0.1/skills/validate-job/SKILL.md +31 -0
- jobwright-0.0.1/tests/test_phase0.py +132 -0
- jobwright-0.0.1/tests/test_phase1.py +102 -0
- jobwright-0.0.1/tests/test_phase2.py +95 -0
- jobwright-0.0.1/tests/test_phase3.py +75 -0
- jobwright-0.0.1/tests/test_review_fixes.py +140 -0
- jobwright-0.0.1/tests/test_review_fixes_overall.py +91 -0
- jobwright-0.0.1/tests/test_review_fixes_p2.py +96 -0
- jobwright-0.0.1/tests/test_review_fixes_p3.py +107 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jobwright",
|
|
3
|
+
"owner": { "name": "Kyle Chalmers" },
|
|
4
|
+
"description": "jobwright — govern, validate, and safely ship data-orchestration jobs with Claude Code.",
|
|
5
|
+
"plugins": [
|
|
6
|
+
{
|
|
7
|
+
"name": "jobwright",
|
|
8
|
+
"source": "./",
|
|
9
|
+
"description": "A jobs index, architecture-compliance scanning, a live-vs-repo deploy-safety guard, and per-job validation for Databricks / Airflow / dbt / Snowflake Tasks job repos."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jobwright",
|
|
3
|
+
"displayName": "jobwright",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Govern, validate, and safely ship data-orchestration jobs (Databricks, Airflow, dbt, Snowflake Tasks) with Claude Code: a jobs index, architecture-compliance scanning, a live-vs-repo deploy-safety guard, and per-job validation.",
|
|
6
|
+
"author": { "name": "Kyle Chalmers" },
|
|
7
|
+
"homepage": "https://github.com/kyle-chalmers/jobwright",
|
|
8
|
+
"repository": "https://github.com/kyle-chalmers/jobwright",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": ["databricks", "airflow", "dbt", "snowflake", "orchestration", "governance", "data-engineering"],
|
|
11
|
+
"hooks": "./hooks/hooks.json"
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
27
|
+
uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install (editable, with dev extras)
|
|
32
|
+
run: pip install -e ".[dev]"
|
|
33
|
+
|
|
34
|
+
# bin/selftest.sh runs ruff + pytest + the adapter-contract + skill-leak checks.
|
|
35
|
+
- name: Selftest
|
|
36
|
+
run: bash bin/selftest.sh
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Manual trigger so cutting a GitHub release never auto-fails before PyPI is configured.
|
|
4
|
+
# After registering the PyPI Trusted Publisher (see docs/PUBLISHING.md), run this workflow
|
|
5
|
+
# from the Actions tab (or switch the trigger to `release: [published]`).
|
|
6
|
+
on:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write # OIDC for PyPI Trusted Publishing — no API token stored
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.11"
|
|
25
|
+
|
|
26
|
+
- name: Build sdist + wheel
|
|
27
|
+
run: |
|
|
28
|
+
pip install build
|
|
29
|
+
python -m build
|
|
30
|
+
|
|
31
|
+
- name: Publish to PyPI (Trusted Publishing)
|
|
32
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.env
|
|
11
|
+
|
|
12
|
+
# Tooling caches
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
|
|
17
|
+
# OS
|
|
18
|
+
.DS_Store
|
|
19
|
+
|
|
20
|
+
# Never commit a real, org-specific config at the repo root.
|
|
21
|
+
# Root-anchored so the example fixtures under examples/ ARE tracked.
|
|
22
|
+
# The documented template lives at jobwright.config.example.yaml.
|
|
23
|
+
/jobwright.config.yaml
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to jobwright are documented here. Format loosely follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); versions follow semver.
|
|
5
|
+
|
|
6
|
+
## [0.0.1] — 2026-06-27
|
|
7
|
+
|
|
8
|
+
First public alpha. Generalized from a production Databricks data-jobs repo and
|
|
9
|
+
stripped of all org-specific values.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Two-seam architecture** — a `platform` seam (orchestrator lifecycle) and a
|
|
14
|
+
`warehouse`/`architecture` seam (static schema policy), expressed in a typed,
|
|
15
|
+
validated `jobwright.config.yaml`.
|
|
16
|
+
- **Platform adapters** across all three deploy models via a `JobPlatformAdapter`
|
|
17
|
+
contract: Databricks Jobs (`api-reset`), Snowflake Tasks (`sql-ddl`), Apache
|
|
18
|
+
Airflow (`git-sync`), and dbt (`git-sync`). Each ships a markdown playbook.
|
|
19
|
+
- **Deploy-safety guard** (`PreToolUse` hook) — asks before destructive job/SQL
|
|
20
|
+
commands (e.g. `databricks jobs reset/update/delete`, `DROP TASK`, `dbt … --target
|
|
21
|
+
prod`, destructive warehouse SQL incl. SQL in `-f` files). Stdlib-only, fail-open,
|
|
22
|
+
zero-cost outside a jobwright repo; defends against shell-quote and full-path evasion.
|
|
23
|
+
- **Generic checks** (`check syntax | job-defs | deps | architecture | docs`) plus a
|
|
24
|
+
composite `validate-job` PASS/FAIL gate scoped to one job.
|
|
25
|
+
- **Deterministic jobs index** — `jobs-index` renders `JOBS.md` + `OBJECTS.md`
|
|
26
|
+
(recall-before-rebuild; surfaces deprecated-schema migration debt), with a `--check`
|
|
27
|
+
CI gate.
|
|
28
|
+
- **Scaffolder** — `new-job` creates a governed job folder (claude.md + notebook
|
|
29
|
+
header + a deploy-model-appropriate definition stub); `gen-agents` renders an
|
|
30
|
+
`AGENTS.md` rulebook from config.
|
|
31
|
+
- **Hooks** — SessionStart skill/catalog pointer + PostToolUse jobs-index regen.
|
|
32
|
+
- **10 tool-agnostic skills** (onboard, start-job, scaffold-job, document-job,
|
|
33
|
+
validate-job, architecture-audit, build-jobs-index, safe-deploy, triage-failure,
|
|
34
|
+
configure-workspace).
|
|
35
|
+
- **Claude Code plugin** manifests (`.claude-plugin/`), `bin/selftest.sh` (lint +
|
|
36
|
+
tests + adapter-contract + skill-leak checks), and a CI workflow.
|
|
37
|
+
|
|
38
|
+
[0.0.1]: https://github.com/kyle-chalmers/jobwright/releases/tag/v0.0.1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Contributing to jobwright
|
|
2
|
+
|
|
3
|
+
## Ground rules
|
|
4
|
+
|
|
5
|
+
1. **One implementation, many consumers.** Logic lives in the `jobwright` package. The CLI, the Claude Code skills, the hooks, and CI all call it — they never re-implement a check. A skill is a thin playbook that shells out to `jobwright ...`; it never embeds the check logic in prose.
|
|
6
|
+
2. **Skills call verbs, never tools.** Skills and commands use the abstract platform verbs (`diff_live_vs_repo`, `list_active_runs`, …) and the generic checks. They must not hardcode `databricks` / `airflow` / `dbt` by name — that belongs in an adapter. `bin/selftest.sh` greps for platform-name leaks.
|
|
7
|
+
3. **No org-specific values in code.** Schema names, profiles, account locators, channels — everything that varies by repo — goes in `jobwright.config.yaml`. The package ships only the *schema* and generic rules.
|
|
8
|
+
4. **Secrets never in the repo.** Not in config, not in tests, not in docs. Config holds names/profiles only.
|
|
9
|
+
|
|
10
|
+
## Adding a platform adapter
|
|
11
|
+
|
|
12
|
+
A platform adapter is two files:
|
|
13
|
+
|
|
14
|
+
- `jobwright/platforms/<kind>.py` — a `JobPlatformAdapter` subclass implementing the mandatory verbs (see `jobwright/platforms/base.py`, `MANDATORY_VERBS`). Set `kind`, `deploy_model`, and `destructive_patterns`. Register it in `jobwright/platforms/__init__.py`. **This is the single source of truth for `destructive_patterns`.**
|
|
15
|
+
- `adapters/platform/<kind>.md` — a markdown playbook: frontmatter (`seam`, `kind`, `transport`, `deploy_model`, `requires`, `auth`, `destructive_patterns`) + a `## verb:` section per verb. The `destructive_patterns` here must mirror the Python class; `bin/selftest.sh` checks they agree.
|
|
16
|
+
|
|
17
|
+
Verbs that don't apply to a platform (e.g. `get_live_definition` / `diff_live_vs_repo` on a `git-sync` platform where git is the source of truth) should raise `ManualFallback` with a message telling the human what to do instead.
|
|
18
|
+
|
|
19
|
+
## Dev setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
23
|
+
pip install -e ".[dev]"
|
|
24
|
+
ruff check . && ruff format --check .
|
|
25
|
+
pytest
|
|
26
|
+
bash bin/selftest.sh
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Provenance
|
|
30
|
+
|
|
31
|
+
jobwright was generalized from a production Databricks data-jobs repo. Every check and safety rule was earned against real jobs and real failures before being lifted here — and stripped of all org-specific values on the way out.
|
jobwright-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyle Chalmers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jobwright-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jobwright
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Open-source AI layer for governing, validating, and safely shipping data-orchestration jobs (Databricks, Airflow, dbt, Snowflake Tasks) with Claude Code
|
|
5
|
+
Project-URL: Homepage, https://github.com/kyle-chalmers/jobwright
|
|
6
|
+
Project-URL: Repository, https://github.com/kyle-chalmers/jobwright
|
|
7
|
+
Project-URL: Issues, https://github.com/kyle-chalmers/jobwright/issues
|
|
8
|
+
Author: Kyle Chalmers
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: airflow,ci,claude-code,data-engineering,databricks,dbt,governance,orchestration,snowflake
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: jinja2>=3.1
|
|
22
|
+
Requires-Dist: pyyaml>=6
|
|
23
|
+
Requires-Dist: rich>=13
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pre-commit>=4; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# jobwright
|
|
32
|
+
|
|
33
|
+
[](https://github.com/kyle-chalmers/jobwright/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/jobwright/)
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
[](https://github.com/astral-sh/ruff)
|
|
38
|
+
|
|
39
|
+
**An open-source AI layer for governing, validating, and safely shipping data-orchestration jobs with Claude Code.**
|
|
40
|
+
|
|
41
|
+
jobwright treats your *jobs* — Databricks Jobs, Airflow DAGs, dbt jobs, Snowflake Tasks — as deployable artifacts that deserve a governed lifecycle: a catalog you can recall before you rebuild, architecture-compliance scanning, a per-job validation gate, and a deploy-safety guard that prompts for confirmation before known destructive commands (so a stale-definition overwrite can't happen unattended).
|
|
42
|
+
|
|
43
|
+
It is the third in a family of Claude Code "AI layer" kits:
|
|
44
|
+
|
|
45
|
+
| Kit | Domain |
|
|
46
|
+
|---|---|
|
|
47
|
+
| [ticketwright](https://github.com/kyle-chalmers/ticketwright) | ticket-driven data work (plan → implement → validate) |
|
|
48
|
+
| [streamsnow](https://github.com/kyle-chalmers/streamsnow) | Streamlit-in-Snowflake apps (build → govern → ship) |
|
|
49
|
+
| **jobwright** | **data-orchestration jobs (govern → validate → safely ship)** |
|
|
50
|
+
|
|
51
|
+
> **Complementary, not overlapping with ticketwright.** ticketwright's Databricks adapter is about *querying* a warehouse while doing a ticket. jobwright is about the *jobs themselves* — their definitions, architecture compliance, and safe deploy. ticketwright's "pause before any prod job deploy" gotcha is exactly the hand-off point to jobwright's `diff-job` + deploy-safety guard.
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
- **A deploy-safety guard.** A `PreToolUse` hook asks for confirmation before destructive orchestration commands (`databricks jobs reset`/`delete`, `airflow dags delete`, `DROP TASK`, …) and before destructive warehouse SQL — including SQL hidden in a `-f` file. On `api-reset` platforms it specifically blocks a reset from a possibly-stale repo definition until you've run a live-vs-repo diff. This turns a prose rule into a runtime gate.
|
|
56
|
+
- **A jobs index.** A deterministic `JOBS.md` + `OBJECTS.md` over every job in the repo — ticket, purpose, schedule, owner, status, and architecture-compliance flags — so you (and the agent) recall prior work before rebuilding, and see migration debt at a glance.
|
|
57
|
+
- **Architecture compliance.** A static scan of job code for deprecated-schema references and layer-referencing violations, driven by config — no database connection.
|
|
58
|
+
- **Per-job validation.** A local PASS/FAIL gate that runs the same checks your CI does, scoped to one job.
|
|
59
|
+
|
|
60
|
+
## Two seams
|
|
61
|
+
|
|
62
|
+
jobwright has two orthogonal abstraction axes, expressed as two config blocks:
|
|
63
|
+
|
|
64
|
+
- **platform** — the orchestrator a job *runs on* (`databricks` / `airflow` / `dbt` / `snowflake_tasks` / …). Owns the lifecycle verbs. Its `deploy_model` (`api-reset` | `git-sync` | `sql-ddl`) decides whether live-vs-repo drift even applies.
|
|
65
|
+
- **warehouse / architecture** — the store a job *reads/writes* and the static schema-reference rules. Policy only; jobwright never opens a database connection.
|
|
66
|
+
|
|
67
|
+
The platform seam is pluggable via thin adapters (a Python class implementing the verb contract + a markdown playbook). Shipped adapters span all three deploy models:
|
|
68
|
+
|
|
69
|
+
| Platform | `deploy_model` | Drift detection |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Databricks Jobs (reference) | `api-reset` | yes — live can drift from repo |
|
|
72
|
+
| Snowflake Tasks | `sql-ddl` | yes — live DDL vs repo DDL |
|
|
73
|
+
| Apache Airflow | `git-sync` | n/a — git is the source of truth |
|
|
74
|
+
| dbt | `git-sync` | n/a — project is the source of truth |
|
|
75
|
+
|
|
76
|
+
The generic checks (`syntax`, `deps`, `architecture`, `docs`) and the deploy-safety guard are platform-agnostic — see [`examples/`](examples/) for a Databricks repo and an Airflow + BigQuery repo. Adding a platform is two files (a `JobPlatformAdapter` subclass + a markdown playbook); see [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install jobwright # or: uvx jobwright
|
|
82
|
+
# Claude Code plugin (skills + hooks):
|
|
83
|
+
# /plugin marketplace add kyle-chalmers/jobwright
|
|
84
|
+
# /plugin install jobwright@jobwright
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quickstart
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cp jobwright.config.example.yaml jobwright.config.yaml # edit for your repo
|
|
91
|
+
jobwright doctor # check config + environment
|
|
92
|
+
jobwright jobs-index # render jobs/JOBS.md + OBJECTS.md
|
|
93
|
+
jobwright diff-job BI-813 # live-vs-repo drift for one job
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
See [`jobwright.config.example.yaml`](jobwright.config.example.yaml) for the full, documented config (including an Airflow + BigQuery variant) and [`examples/`](examples/) for a runnable sample repo.
|
|
97
|
+
|
|
98
|
+
## Status
|
|
99
|
+
|
|
100
|
+
Alpha. Shipped: the deploy-safety guard, the jobs index, four platform adapters (Databricks, Snowflake Tasks, Airflow, dbt) across all three deploy models, the generic checks (`syntax` / `job-defs` / `deps` / `architecture` / `docs`) + composite `validate-job`, the job scaffolder + generated `AGENTS.md`, the SessionStart + index-regen hooks, and 10 lifecycle skills. Publishing is gated on a security/leak review — see [`docs/PUBLISHING.md`](docs/PUBLISHING.md).
|
|
101
|
+
|
|
102
|
+
### CLI
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
jobwright doctor | init | jobs-index [--check] | validate-job <folder> [--offline]
|
|
106
|
+
check {syntax|job-defs|deps|architecture|docs} <paths>
|
|
107
|
+
new-job <ticket> "<name>" | gen-agents | diff-job <job>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
MIT licensed.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# jobwright
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kyle-chalmers/jobwright/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/jobwright/)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/astral-sh/ruff)
|
|
8
|
+
|
|
9
|
+
**An open-source AI layer for governing, validating, and safely shipping data-orchestration jobs with Claude Code.**
|
|
10
|
+
|
|
11
|
+
jobwright treats your *jobs* — Databricks Jobs, Airflow DAGs, dbt jobs, Snowflake Tasks — as deployable artifacts that deserve a governed lifecycle: a catalog you can recall before you rebuild, architecture-compliance scanning, a per-job validation gate, and a deploy-safety guard that prompts for confirmation before known destructive commands (so a stale-definition overwrite can't happen unattended).
|
|
12
|
+
|
|
13
|
+
It is the third in a family of Claude Code "AI layer" kits:
|
|
14
|
+
|
|
15
|
+
| Kit | Domain |
|
|
16
|
+
|---|---|
|
|
17
|
+
| [ticketwright](https://github.com/kyle-chalmers/ticketwright) | ticket-driven data work (plan → implement → validate) |
|
|
18
|
+
| [streamsnow](https://github.com/kyle-chalmers/streamsnow) | Streamlit-in-Snowflake apps (build → govern → ship) |
|
|
19
|
+
| **jobwright** | **data-orchestration jobs (govern → validate → safely ship)** |
|
|
20
|
+
|
|
21
|
+
> **Complementary, not overlapping with ticketwright.** ticketwright's Databricks adapter is about *querying* a warehouse while doing a ticket. jobwright is about the *jobs themselves* — their definitions, architecture compliance, and safe deploy. ticketwright's "pause before any prod job deploy" gotcha is exactly the hand-off point to jobwright's `diff-job` + deploy-safety guard.
|
|
22
|
+
|
|
23
|
+
## Why
|
|
24
|
+
|
|
25
|
+
- **A deploy-safety guard.** A `PreToolUse` hook asks for confirmation before destructive orchestration commands (`databricks jobs reset`/`delete`, `airflow dags delete`, `DROP TASK`, …) and before destructive warehouse SQL — including SQL hidden in a `-f` file. On `api-reset` platforms it specifically blocks a reset from a possibly-stale repo definition until you've run a live-vs-repo diff. This turns a prose rule into a runtime gate.
|
|
26
|
+
- **A jobs index.** A deterministic `JOBS.md` + `OBJECTS.md` over every job in the repo — ticket, purpose, schedule, owner, status, and architecture-compliance flags — so you (and the agent) recall prior work before rebuilding, and see migration debt at a glance.
|
|
27
|
+
- **Architecture compliance.** A static scan of job code for deprecated-schema references and layer-referencing violations, driven by config — no database connection.
|
|
28
|
+
- **Per-job validation.** A local PASS/FAIL gate that runs the same checks your CI does, scoped to one job.
|
|
29
|
+
|
|
30
|
+
## Two seams
|
|
31
|
+
|
|
32
|
+
jobwright has two orthogonal abstraction axes, expressed as two config blocks:
|
|
33
|
+
|
|
34
|
+
- **platform** — the orchestrator a job *runs on* (`databricks` / `airflow` / `dbt` / `snowflake_tasks` / …). Owns the lifecycle verbs. Its `deploy_model` (`api-reset` | `git-sync` | `sql-ddl`) decides whether live-vs-repo drift even applies.
|
|
35
|
+
- **warehouse / architecture** — the store a job *reads/writes* and the static schema-reference rules. Policy only; jobwright never opens a database connection.
|
|
36
|
+
|
|
37
|
+
The platform seam is pluggable via thin adapters (a Python class implementing the verb contract + a markdown playbook). Shipped adapters span all three deploy models:
|
|
38
|
+
|
|
39
|
+
| Platform | `deploy_model` | Drift detection |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Databricks Jobs (reference) | `api-reset` | yes — live can drift from repo |
|
|
42
|
+
| Snowflake Tasks | `sql-ddl` | yes — live DDL vs repo DDL |
|
|
43
|
+
| Apache Airflow | `git-sync` | n/a — git is the source of truth |
|
|
44
|
+
| dbt | `git-sync` | n/a — project is the source of truth |
|
|
45
|
+
|
|
46
|
+
The generic checks (`syntax`, `deps`, `architecture`, `docs`) and the deploy-safety guard are platform-agnostic — see [`examples/`](examples/) for a Databricks repo and an Airflow + BigQuery repo. Adding a platform is two files (a `JobPlatformAdapter` subclass + a markdown playbook); see [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install jobwright # or: uvx jobwright
|
|
52
|
+
# Claude Code plugin (skills + hooks):
|
|
53
|
+
# /plugin marketplace add kyle-chalmers/jobwright
|
|
54
|
+
# /plugin install jobwright@jobwright
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cp jobwright.config.example.yaml jobwright.config.yaml # edit for your repo
|
|
61
|
+
jobwright doctor # check config + environment
|
|
62
|
+
jobwright jobs-index # render jobs/JOBS.md + OBJECTS.md
|
|
63
|
+
jobwright diff-job BI-813 # live-vs-repo drift for one job
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See [`jobwright.config.example.yaml`](jobwright.config.example.yaml) for the full, documented config (including an Airflow + BigQuery variant) and [`examples/`](examples/) for a runnable sample repo.
|
|
67
|
+
|
|
68
|
+
## Status
|
|
69
|
+
|
|
70
|
+
Alpha. Shipped: the deploy-safety guard, the jobs index, four platform adapters (Databricks, Snowflake Tasks, Airflow, dbt) across all three deploy models, the generic checks (`syntax` / `job-defs` / `deps` / `architecture` / `docs`) + composite `validate-job`, the job scaffolder + generated `AGENTS.md`, the SessionStart + index-regen hooks, and 10 lifecycle skills. Publishing is gated on a security/leak review — see [`docs/PUBLISHING.md`](docs/PUBLISHING.md).
|
|
71
|
+
|
|
72
|
+
### CLI
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
jobwright doctor | init | jobs-index [--check] | validate-job <folder> [--offline]
|
|
76
|
+
check {syntax|job-defs|deps|architecture|docs} <paths>
|
|
77
|
+
new-job <ticket> "<name>" | gen-agents | diff-job <job>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
MIT licensed.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
seam: platform
|
|
3
|
+
kind: airflow
|
|
4
|
+
transport: cli
|
|
5
|
+
deploy_model: git-sync
|
|
6
|
+
requires: []
|
|
7
|
+
auth: |
|
|
8
|
+
Airflow CLI talks to the configured Airflow environment (AIRFLOW_HOME / connection).
|
|
9
|
+
Verify: `airflow dags list`.
|
|
10
|
+
destructive_patterns:
|
|
11
|
+
# MUST stay in sync with AirflowAdapter.destructive_patterns (jobwright/platforms/airflow.py).
|
|
12
|
+
- 'airflow\s+dags\s+delete\b'
|
|
13
|
+
- 'airflow\s+db\s+(reset|clean|downgrade)\b'
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Apache Airflow adapter
|
|
17
|
+
|
|
18
|
+
Deploy model **git-sync**: DAGs are code, so the repo file *is* the definition. There is
|
|
19
|
+
no live-vs-repo drift — `get_live_definition` / `diff_live_vs_repo` raise ManualFallback
|
|
20
|
+
and point at `git diff`. "Deploy" is a git push / DAG-folder sync, not an API call.
|
|
21
|
+
|
|
22
|
+
## verb: list_jobs
|
|
23
|
+
```bash
|
|
24
|
+
airflow dags list -o json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## verb: get_job_definition
|
|
28
|
+
Reads the DAG `.py` from the configured `dags_dir`.
|
|
29
|
+
|
|
30
|
+
## verb: get_live_definition / diff_live_vs_repo
|
|
31
|
+
N/A (git-sync) — use `git status` / `git diff`.
|
|
32
|
+
|
|
33
|
+
## verb: list_active_runs
|
|
34
|
+
```bash
|
|
35
|
+
airflow dags list-runs -d <dag_id> --state running -o json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## verb: trigger_run
|
|
39
|
+
Checks active runs first, then `airflow dags trigger <dag_id>`.
|
|
40
|
+
|
|
41
|
+
## verb: get_run / get_run_output
|
|
42
|
+
Need dag_id (+ task_id): `airflow dags list-runs -d <dag_id>`, `airflow tasks logs <dag_id> <task_id> <run_id>`.
|
|
43
|
+
|
|
44
|
+
## Gotchas
|
|
45
|
+
- Airflow's REST API has no delete-DAG endpoint by design — deletion is a code/file operation.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
seam: platform
|
|
3
|
+
kind: databricks
|
|
4
|
+
transport: cli
|
|
5
|
+
deploy_model: api-reset
|
|
6
|
+
requires: [profile]
|
|
7
|
+
auth: |
|
|
8
|
+
Databricks CLI profile in ~/.databrickscfg (host + token), selected by
|
|
9
|
+
`platform.profile` in jobwright.config.yaml. Verify: `databricks jobs list --profile <p>`.
|
|
10
|
+
jobwright never stores the token — only the profile NAME.
|
|
11
|
+
destructive_patterns:
|
|
12
|
+
# MUST stay in sync with DatabricksAdapter.destructive_patterns (jobwright/platforms/databricks.py).
|
|
13
|
+
# selftest.sh checks both lists agree.
|
|
14
|
+
- 'databricks\s+jobs\s+reset\b'
|
|
15
|
+
- 'databricks\s+jobs\s+delete\b'
|
|
16
|
+
- 'databricks\s+jobs\s+update\b'
|
|
17
|
+
- 'databricks\s+jobs\s+(run-now|submit)\b'
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Databricks Jobs adapter
|
|
21
|
+
|
|
22
|
+
Reference implementation of the jobwright platform verb contract. Deploy model is
|
|
23
|
+
**api-reset**: the live job definition can drift from the repo JSON, so drift
|
|
24
|
+
detection is mandatory and `databricks jobs reset` is guarded.
|
|
25
|
+
|
|
26
|
+
> **The incident this guards against:** `databricks jobs reset` is a *full replace*
|
|
27
|
+
> of a job from the file you pass. Repo JSONs can be stale (jobs are sometimes
|
|
28
|
+
> edited in the UI). Resetting from a stale JSON has overwritten correct live state
|
|
29
|
+
> and broken production jobs. Always `diff-job` before a reset.
|
|
30
|
+
|
|
31
|
+
## verb: list_jobs
|
|
32
|
+
**Out:** `[{job_id, name, paused, schedule}]`
|
|
33
|
+
```bash
|
|
34
|
+
databricks jobs list -o json --profile <p>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## verb: get_job_definition
|
|
38
|
+
**In:** ref (ticket / job name / filename stem) · **Out:** repo-side definition
|
|
39
|
+
Resolves the JSON under `platform.job_def_dirs` by filename stem, ticket-key prefix, or `name` field.
|
|
40
|
+
|
|
41
|
+
## verb: get_live_definition
|
|
42
|
+
**In:** ref · **Out:** live definition from the workspace
|
|
43
|
+
```bash
|
|
44
|
+
databricks jobs get <job_id> -o json --profile <p>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## verb: diff_live_vs_repo
|
|
48
|
+
**In:** ref (+ optional repo_path) · **Out:** `{drift, added, removed, changed, detail}`
|
|
49
|
+
Normalizes both sides (unwraps `settings`, drops volatile keys: job_id, created_time, creator_user_name, run_as, …) then compares dotted paths.
|
|
50
|
+
|
|
51
|
+
## verb: list_active_runs
|
|
52
|
+
**In:** ref · **Out:** `[{run_id, state, started}]`
|
|
53
|
+
```bash
|
|
54
|
+
databricks jobs list-runs --job-id <id> --active-only -o json --profile <p>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## verb: deploy
|
|
58
|
+
Withheld as a one-shot — use the `/safe-deploy` skill (diff → confirm → reset). The guard hook backstops any raw `databricks jobs reset`.
|
|
59
|
+
|
|
60
|
+
## verb: trigger_run
|
|
61
|
+
Checks `list_active_runs` first (refuses if a run is active), then `databricks jobs run-now <id>`.
|
|
62
|
+
|
|
63
|
+
## verb: get_run / get_run_output
|
|
64
|
+
```bash
|
|
65
|
+
databricks jobs get-run <run_id> -o json --profile <p>
|
|
66
|
+
databricks jobs get-run-output <run_id> -o json --profile <p>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Gotchas
|
|
70
|
+
- Repo JSONs are the *unwrapped* settings (top-level `name`, `schedule`, `tasks`); the API returns them under `settings`.
|
|
71
|
+
- Repo JSONs have **no** `job_id`; jobwright resolves the live job by matching `name`.
|
|
72
|
+
- A `run-now` timeout does **not** mean the run failed to start — check `list-runs --active-only` before retrying.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
seam: platform
|
|
3
|
+
kind: dbt
|
|
4
|
+
transport: cli
|
|
5
|
+
deploy_model: git-sync
|
|
6
|
+
requires: []
|
|
7
|
+
auth: |
|
|
8
|
+
`dbt` CLI with a configured profiles.yml. Verify: `dbt debug`.
|
|
9
|
+
destructive_patterns:
|
|
10
|
+
# MUST stay in sync with DbtAdapter.destructive_patterns (jobwright/platforms/dbt.py).
|
|
11
|
+
- 'dbt\b(?=.*\b(?:run|build|seed|snapshot)\b)(?=.*(?:--target|-t)[=\s]+prod(?![\w-]))'
|
|
12
|
+
- 'dbt\b(?=.*\brun-operation\b)'
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# dbt adapter
|
|
16
|
+
|
|
17
|
+
Deploy model **git-sync**: the dbt project is the source of truth. dbt's unit is a model,
|
|
18
|
+
not a standalone job, so discovery/run map best-effort to the CLI and drift verbs raise
|
|
19
|
+
ManualFallback. The guard is the high-value piece: a `--target prod` run/build, or any
|
|
20
|
+
`run-operation`, mutates the warehouse and must be confirmed.
|
|
21
|
+
|
|
22
|
+
## verb: list_jobs
|
|
23
|
+
```bash
|
|
24
|
+
dbt ls --resource-type model --output name
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## verb: get_job_definition
|
|
28
|
+
Reads the model `.sql` from the models dir (configured via `dags_dir`).
|
|
29
|
+
|
|
30
|
+
## verb: get_live_definition / diff_live_vs_repo
|
|
31
|
+
N/A (git-sync) — use `git diff`.
|
|
32
|
+
|
|
33
|
+
## verb: trigger_run
|
|
34
|
+
`dbt build --select <model>` (default target; a prod target is caught by the guard).
|
|
35
|
+
|
|
36
|
+
## verb: list_active_runs / get_run / get_run_output
|
|
37
|
+
dbt core has no run registry — read `target/run_results.json`, or use the dbt Cloud Runs API.
|
|
38
|
+
|
|
39
|
+
## Gotchas
|
|
40
|
+
- A bare `dbt run` uses the default target; only `--target prod` (or `-t prod`) trips the guard.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
seam: platform
|
|
3
|
+
kind: snowflake_tasks
|
|
4
|
+
transport: cli
|
|
5
|
+
deploy_model: sql-ddl
|
|
6
|
+
requires: [profile]
|
|
7
|
+
auth: |
|
|
8
|
+
`snow` CLI connection (named by platform.profile). Verify: `snow sql -q "SHOW TASKS"`.
|
|
9
|
+
destructive_patterns:
|
|
10
|
+
# MUST stay in sync with SnowflakeTasksAdapter.destructive_patterns (jobwright/platforms/snowflake_tasks.py).
|
|
11
|
+
- '\bDROP\s+TASK\b'
|
|
12
|
+
- '\bCREATE\s+OR\s+REPLACE\s+TASK\b'
|
|
13
|
+
- '\bALTER\s+TASK\b'
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Snowflake Tasks adapter
|
|
17
|
+
|
|
18
|
+
Deploy model **sql-ddl**: a task is defined by DDL and the live object can drift from
|
|
19
|
+
the repo DDL — so drift detection applies and replace/drop/alter are guarded.
|
|
20
|
+
|
|
21
|
+
## verb: list_jobs
|
|
22
|
+
```sql
|
|
23
|
+
SHOW TASKS
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## verb: get_job_definition
|
|
27
|
+
Reads the task DDL `.sql` from the configured `job_def_dirs`.
|
|
28
|
+
|
|
29
|
+
## verb: get_live_definition
|
|
30
|
+
```sql
|
|
31
|
+
SELECT GET_DDL('TASK', '<task_name>')
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## verb: diff_live_vs_repo
|
|
35
|
+
Normalizes whitespace/case of repo DDL vs live `GET_DDL` and reports drift.
|
|
36
|
+
|
|
37
|
+
## verb: list_active_runs
|
|
38
|
+
```sql
|
|
39
|
+
SELECT QUERY_ID, STATE FROM TABLE(INFORMATION_SCHEMA.TASK_HISTORY(TASK_NAME => '<task>')) WHERE STATE = 'EXECUTING'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## verb: trigger_run
|
|
43
|
+
```sql
|
|
44
|
+
EXECUTE TASK <task_name>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## verb: get_run / get_run_output
|
|
48
|
+
Query `TASK_HISTORY` / `QUERY_HISTORY` by `QUERY_ID`.
|
|
49
|
+
|
|
50
|
+
## Gotchas
|
|
51
|
+
- `CREATE OR REPLACE TASK` silently overwrites the live definition — always `diff-job` first.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# jobwright kit self-test — run before committing / publishing.
|
|
3
|
+
# Verifies: lint, the Phase 0 contract tests (adapter verb coverage,
|
|
4
|
+
# md/py destructive-pattern sync, the deploy-safety guard, index determinism),
|
|
5
|
+
# and that no platform names leak into the skills (skills call verbs, not tools).
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
9
|
+
cd "$ROOT"
|
|
10
|
+
|
|
11
|
+
PY="${PYTHON:-python3}"
|
|
12
|
+
if [ -x .venv/bin/python ]; then PY=".venv/bin/python"; fi
|
|
13
|
+
|
|
14
|
+
echo "==> ruff"
|
|
15
|
+
"$PY" -m ruff check . || { echo "FAIL: ruff"; exit 1; }
|
|
16
|
+
|
|
17
|
+
echo "==> pytest (Phase 0 contract)"
|
|
18
|
+
"$PY" -m pytest -q || { echo "FAIL: pytest"; exit 1; }
|
|
19
|
+
|
|
20
|
+
echo "==> skill leak check"
|
|
21
|
+
if [ -d skills ] && [ -n "$(find skills -name '*.md' -print -quit 2>/dev/null)" ]; then
|
|
22
|
+
if grep -rEli '\b(databricks|airflow|dbt|dagster|prefect|snowflake_tasks|glue)\b' skills/ >/dev/null 2>&1; then
|
|
23
|
+
echo "FAIL: a platform name leaked into skills/ — skills must call abstract verbs, not name tools"
|
|
24
|
+
grep -rEli '\b(databricks|airflow|dbt|dagster|prefect|snowflake_tasks|glue)\b' skills/ || true
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
echo "OK: jobwright selftest passed"
|