diff --git a/.agents/commands/pre-ci.md b/.agents/commands/pre-ci.md index 7f14d70d..c832cd75 100644 --- a/.agents/commands/pre-ci.md +++ b/.agents/commands/pre-ci.md @@ -7,7 +7,7 @@ Run a subset of fast CI checks locally. These are lightweight validations that c uv run invoke format ``` -2. **Lint** (YAML, Ruff, ty, mypy, markdownlint, vale): +2. **Lint** (YAML, Ruff, ty, mypy, rumdl, vale): ```bash uv run invoke lint ``` diff --git a/dev/rules/python-testing-integration.md b/.agents/rules/python-testing-integration.md similarity index 100% rename from dev/rules/python-testing-integration.md rename to .agents/rules/python-testing-integration.md diff --git a/dev/rules/python-testing-unit.md b/.agents/rules/python-testing-unit.md similarity index 100% rename from dev/rules/python-testing-unit.md rename to .agents/rules/python-testing-unit.md diff --git a/dev/rules/python-testing.md b/.agents/rules/python-testing.md similarity index 100% rename from dev/rules/python-testing.md rename to .agents/rules/python-testing.md diff --git a/.agents/skills/opsmill-dev-analyzing-bugs/SKILL.md b/.agents/skills/opsmill-dev-analyzing-bugs/SKILL.md new file mode 100644 index 00000000..e05ae042 --- /dev/null +++ b/.agents/skills/opsmill-dev-analyzing-bugs/SKILL.md @@ -0,0 +1,207 @@ +--- +name: opsmill-dev-analyzing-bugs +description: >- + Performs root-cause analysis of a bug — from a GitHub issue, an issue URL, or a free-text + description — before any reproduction or fix is written. TRIGGER when: triaging or diagnosing + a bug, investigating why something misbehaves, an issue number/URL handed over for analysis, + needing the root cause before touching code. DO NOT TRIGGER when: a failing reproduction test + already exists and you are ready to fix → opsmill-dev-fixing-bugs; writing that reproduction test → + opsmill-dev-test-driving-bugs; capturing a new bug as a ticket → opsmill-dev-creating-issues. +argument-hint: +compatibility: >- + Works in any git repo; anchors discovery on the OpsMill `dev/` layout with codebase fallback. + `gh` is optional, used only for issue numbers/URLs. +metadata: + pipeline: bug-fixing (1 of 3 — analyze → test-drive → fix) + version: 0.1.0 + author: OpsMill +user-invocable: true +--- + +# Bug analyst + +## User Input + +```text +$ARGUMENTS +``` + +## Your role + +You are a senior engineer performing root cause analysis. You do **NOT** write fixes or tests. +Your output will be consumed by `/opsmill-dev-test-driving-bugs` and `/opsmill-dev-fixing-bugs`, so be structured and precise. + +## Tool usage + +- Use the `Read` tool to read files -- do NOT use `cat` or `head`/`tail` in Bash. +- Use the `Glob` tool to find files -- do NOT use `find` or `ls -R` in Bash. +- Use the `Grep` tool to search file contents -- do NOT use `grep` or `rg` in Bash. +- Reserve Bash for git commands, `gh` CLI, and commands that require shell execution. +- Shell state does **not** persist across separate Bash calls (variables, `cd`); re-derive or + restate anything you need in a later snippet. + +## Input + +Parse `$ARGUMENTS` to determine what you are analysing: + +- **Issue number or URL** (e.g. `4872` or `https://github.com/org/repo/issues/4872`): extract + the issue number. If `gh` is available, fetch the issue: + + ```bash + gh issue view + ``` + +- **Free-text description**: treat the text itself as the bug report. + +Derive a **key** for this analysis (used to name the handoff file and downstream branch/PR): + +- If an issue number is present, `` = `-`. +- Otherwise `` = `` only. + +The `` is a lowercase, hyphenated 2--5 word summary of the bug (e.g. +`internal-groups-dropdown`). Always include the slug so concurrent analyses never collide. + +This `` is invented here (the slug is free-form), so it is the **canonical** one for the +whole pipeline. You will persist it -- and the downstream branch name `ai-bug-pipeline-` -- +into the handoff file below, so `/opsmill-dev-test-driving-bugs` and `/opsmill-dev-fixing-bugs` read them instead of re-deriving a +slug that could drift (e.g. `internal-groups-dropdown` vs `groups-dropdown-internal`). + +If `$ARGUMENTS` is empty or an issue cannot be fetched, inform the developer and **STOP**. + +## Discover project context (read what exists, skip what doesn't) + +Anchor on the common OpsMill `dev/` structure; fall back to exploration when it is absent. +**None of these files are required.** + +| File | If present, use it for | +| --- | --- | +| root `AGENTS.md` | Project map, working agreements, where code lives. | +| `dev/documentation-architecture.md` | Mapping the bug to the relevant code package(s). | +| `dev/knowledge/` | Descriptive architecture — how the affected area actually works. | +| `dev/guidelines/` | Prescriptive rules for the affected area. | + +## Investigation + +Follow these sections in order. + +### Issue clarity check + +Verify the report has enough information to work with: + +| Required | Description | +|----------|-------------| +| **Clear problem statement** | Can you understand what the bug actually is? | +| **Reproduction path** | Are there steps to reproduce, OR can you infer them from the description? | +| **Expected vs actual** | Is it clear what should happen vs what happens? | + +Rate the clarity: + +- **CLEAR**: intent, reproduction scenario, and expected behavior are understandable (even if + some details like the affected release are missing). +- **UNCLEAR**: the intent and reproduction scenario are not understandable. + +If the bug is **UNCLEAR**, inform the developer what information is missing and **STOP**. + +### Investigate the codebase + +1. Read root `AGENTS.md` and `dev/documentation-architecture.md` (if present) to determine which + code package(s) relate to the issue. Then: + - If you can determine the related code package, rate code identification as **RESOLVED**. + - If you cannot, rate it **EXPLORATION REQUIRED** and explore the codebase (use `Glob`/`Grep`) + until you locate the affected area. + +2. Read the relevant source files in the affected area to understand the current behavior. + +3. Identify the most likely root cause(s) -- point to specific files and lines. + - If you **cannot** identify a root cause after exploration, inform the developer and **STOP**. + +4. Formulate a fix strategy. This is NOT the exact code -- it is the recommended approach: + - **Approach:** What should the fixer do and where? Reference existing functions/methods that + should be reused rather than reimplemented. + - **Scope:** Which files/functions need changes? How large should the change be? + - **Do NOT:** List common wrong approaches (e.g., adding a guard clause when the real fix is a + missing validation, creating new abstractions when an existing one should be reused). + +## Output + +Determine the repository's default branch and fetch its latest state, to record what the +analysis is based on: + +```bash +DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') +# Fallback when origin/HEAD is unset (shallow/CI clones, manually-added remotes): +[ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH=$(git remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p') +[ "$DEFAULT_BRANCH" = "(unknown)" ] && DEFAULT_BRANCH="" # git prints "(unknown)" when remote HEAD is indeterminate +DEFAULT_BRANCH=${DEFAULT_BRANCH:-main} +git fetch origin "$DEFAULT_BRANCH" || { echo "Cannot fetch origin/$DEFAULT_BRANCH -- set the default branch manually and retry."; exit 1; } +git rev-parse "origin/$DEFAULT_BRANCH" +``` + +If that fetch fails, **STOP** and report it -- do not write the analysis without a baseline +commit (the `exit 1` only fails the shell call; you must not continue to the next section). + +Write the analysis to `.bug-analysis-.md` in the repo root using the template below. This +file is a **local working-tree artifact, not committed** -- add `.bug-analysis-*.md` to the +repo's `.gitignore` if it is not already ignored, and never `git add` it. + +Then display the full analysis to the developer in the conversation. + +### Analysis template + +Replace all ``: + +````markdown +## Root cause analysis for + +**Key:** `` +**Branch:** `ai-bug-pipeline-` +**Issue:** +**Based on:** `>` +**Bug clarity:** CLEAR +**Code identification:** RESOLVED | EXPLORATION REQUIRED + +### Root cause + + + +### Affected files + +- `path/to/file.ext` -- line X: + +### Explanation + + + +## Fix strategy + +**Approach:** + +**Scope:** + +**Do NOT:** + +- +- + +## Notes for downstream steps + + +```` + +## Quality gates + +Per `../quality-gates/gates/gate-model.md`. `analyzing-bugs` is **Tier 0** (advisory output). + +| Gate | Trigger | Tier | Primitives | Pass criteria | On-fail | +|---|---|---|---|---|---| +| Grounded-analysis | before writing the analysis file | T0 | P1 + self-review | Every root-cause claim cites a concrete file:line. | revise before writing | + +## Common mistakes + +| 🚩 Red flag | Do instead | +| --- | --- | +| Writing fix code (or a test) into the analysis | This step does analysis only — describe the fix *strategy*, not the patch; `/opsmill-dev-test-driving-bugs` and `/opsmill-dev-fixing-bugs` do the rest | +| Analysing an **UNCLEAR** bug or one with no findable root cause | STOP and tell the developer what's missing — a confident-sounding analysis of an unclear bug misleads both later steps | +| Re-deriving or guessing a slug later in the pipeline | Invent the `` once here and **persist** it (and `ai-bug-pipeline-`) in the handoff header so `/opsmill-dev-test-driving-bugs` / `/opsmill-dev-fixing-bugs` read it instead of drifting | +| Writing the analysis without a baseline commit | If the default-branch fetch fails, STOP — the `**Based on:**` SHA is what later steps reproduce against | +| `git add`-ing the `.bug-analysis-*.md` file | It is a local working-tree artifact — never commit it | diff --git a/.agents/skills/opsmill-dev-analyzing-dependency-bumps/SKILL.md b/.agents/skills/opsmill-dev-analyzing-dependency-bumps/SKILL.md new file mode 100644 index 00000000..30aea624 --- /dev/null +++ b/.agents/skills/opsmill-dev-analyzing-dependency-bumps/SKILL.md @@ -0,0 +1,115 @@ +--- +name: opsmill-dev-analyzing-dependency-bumps +description: >- + Produces a breaking-change / deprecation / opportunity report for a dependency-bump PR, + grounded in how this codebase actually uses each bumped package, not the changelog alone. + TRIGGER when: the user gives a PR URL or number for a dependency version bump (Dependabot, + Renovate, or hand-rolled) and asks what could break, what changed, or whether it is safe to + merge (e.g. "scan this dependabot PR", "what breaks if we take this bump", "is safe + to merge"). DO NOT TRIGGER when: the PR is a feature or bugfix rather than a dependency version + bump → a code-review or bug skill instead. +argument-hint: +compatibility: >- + Requires GitHub access to fetch the PR (gh CLI authenticated, GitHub MCP server, or + equivalent) and a checkout of the target repo to grep real usage. Read-only — never edits, + merges, or comments unless explicitly asked. +metadata: + version: 0.1.0 + author: OpsMill + +user-invocable: true +--- + +# Analyze Dependency Bump + +Given a dependency-upgrade PR, produce a breaking-change / deprecation / opportunity report **grounded in this repo's actual usage**. The governing principle: + +> A changelog tells you what changed in the library. Only the codebase tells you whether it matters here. + +Every claim in the report must trace to real usage in the repo — or to the verified *absence* of usage. A "safe to merge" backed only by the changelog is not allowed. + +## Output contract + +A **chat report** — do not write files or post PR comments unless the user explicitly asks. Lead with a one-line verdict, then one section per bumped package: + +```text +Verdict: + +## +- Classification: direct | transitive (chain: pkg ← parent ← … ← root) +- Usage in repo: +- Breaking changes: +- Deprecations: +- Security / bug fixes: +- Opportunities: +- Recommendation: +``` + +Hard rules for the report: + +- Every **safe** claim is backed by either no-usage (with the grep that proves it) or a verified role mismatch. Never assert safety from the changelog alone. +- Every **impacts-us** claim cites `file:line` and names the required code change. +- Keep it dry and specific. No filler, no restating the PR body. + +## Workflow + +Detect the ecosystem from the manifest/lockfile the PR touches and use its lockfile, reverse-dep tool, and import idiom. When a repo mixes ecosystems, run the steps once per affected ecosystem. + +### 1. Fetch the PR and extract every version bump + +```bash +gh pr view --repo --json title,body,files,headRefName,baseRefName +gh pr diff --repo +``` + +Parse the **lockfile diff** (`uv.lock`, `poetry.lock`, `package-lock.json`, `pnpm-lock.yaml`, `Cargo.lock`, `go.sum`, etc.) for exact `old → new` versions — do not trust the title alone. A bot PR names one package in the title even when the lock pulls in several. Handle both single and grouped bumps. If only a number is given and the repo is ambiguous, infer from the cwd's `git remote` or ask. + +### 2. Classify each bump: direct vs transitive + +This decides everything downstream. + +- **Direct** — listed in the manifest (`pyproject.toml`, `package.json`, `Cargo.toml`, `go.mod`), including dev/optional/group sections. +- **Transitive** — only in the lockfile, pulled in by another package. + +For transitive packages, compute the reverse-dependency chain and report it (e.g. `certifi ← httpx ← pytest-httpx`, terminating in a dev-only test tool). Depth, and whether the chain terminates in a **runtime** vs **dev/test** dependency, is a primary impact signal. Use the ecosystem's reverse-dep tool (`npm ls `, `cargo tree -i `, `go mod why `, …), or parse the lockfile's resolved graph directly (e.g. `uv.lock` TOML `[[package]]` → `dependencies` / `optional-dependencies` / `dev-dependencies`) to build a reverse map — the latter needs no synced environment. + +### 3. Grep real usage in the repo + +Grep the repo for the package's import idiom — first to find imports of the package, then to find the specific symbols the changelog flags: + +```bash +# Python example — substitute the import pattern + --include glob per ecosystem +grep -rn "import \|from " --include="*.py" . +grep -rn "\|" --include="*.py" . +``` + +Guard against **symbol collisions** — a name can belong to a different library (e.g. `request.url` on an `httpx` object is not `starlette`; a `parse()` could come from any of several deps). Confirm the import source before counting a match. **Zero direct imports is a strong, citable result**: most changelog entries then cannot apply — say so plainly. + +### 4. Read the full changelog for every bump (deep by default) + +For every bumped package, direct or transitive, read release notes spanning the **whole** `old → new` range — breaking changes hide in intermediate versions, not just the endpoints. + +- The PR body usually embeds the bot's release-notes / changelog / commits sections. Read them. +- When truncated or missing, fetch upstream with `WebFetch` (`https://github.com///compare/...`, the `CHANGELOG`), or use the Context7 MCP for the library's migration docs. +- If the notes still can't be retrieved (private upstream, no published changelog, no network), **ask the user to paste the release notes or a link** rather than guessing. Never invent changelog content — say which versions you couldn't source and report only what the repo's usage lets you verify. + +Categorize each notable change: **breaking**, **deprecation**, **security fix**, **bug fix**, **new capability**. + +### 5. Cross every changelog item against actual usage + +Label each notable change by its relevance *to this repo*: + +- **Impacts us** — the changed/removed/deprecated surface is imported and used here. Cite `file:line`, name the required change. +- **Role mismatch** — common for web frameworks and client/server libs. A client SDK is unaffected by server-side form-parsing changes (and vice versa). State the role this repo plays. +- **Not used** — the surface exists in the library but the repo never touches it. Safe. +- **Opportunity** — a new API or default that would simplify or improve existing code here; point at the adoptable code. + +Be skeptical of the PR's own auto-generated migration notes (cubic / Dependabot summaries). They address the library's general audience, not this repo — verify each against the code rather than repeating it. + +### 6. For large grouped bumps + +Fan out: one analysis pass per package (steps 2–5), then synthesize. Breadth must not dilute grounding — a "not used" verdict still requires the grep that proves it. If the only way to cut transitive-bump noise is upstream, say so and whether pinning is worth it. + +## Notes + +- Pairs with PR-review skills — this one answers "what does this bump mean for us", not "is the diff well-written". diff --git a/.agents/skills/opsmill-dev-backporting-fixes/SKILL.md b/.agents/skills/opsmill-dev-backporting-fixes/SKILL.md new file mode 100644 index 00000000..d51e2528 --- /dev/null +++ b/.agents/skills/opsmill-dev-backporting-fixes/SKILL.md @@ -0,0 +1,121 @@ +--- +name: opsmill-dev-backporting-fixes +description: >- + Ports an existing fix from one long-lived branch to another (e.g. a stable fix landed on the + development branch), adapting it to the target branch's current code rather than cherry-picking + blindly. TRIGGER when: phrasings like "backport to ", "port this stable + fix to develop", "land on too", or a fix that already exists on one branch has + to land on another. DO NOT TRIGGER when: syncing one whole branch into another wholesale → + opsmill-dev-merging-branches; replaying local work onto its base → opsmill-dev-rebase; fixing a + brand-new bug that has no existing fix → opsmill-dev-analyzing-bugs / opsmill-dev-fixing-bugs. +argument-hint: A fix reference on the source branch (commit SHA, merged PR, or ticket) and the target branch to land it on +compatibility: Requires a Git working tree. Locating the source fix and opening the PR need GitHub access (gh CLI authenticated, GitHub MCP server, or equivalent). Composes the `opsmill-dev-commit`, `opsmill-dev-pr`, `opsmill-dev-monitoring-pull-requests`, and `opsmill-dev-creating-issues` skills. +metadata: + version: 0.1.0 + author: OpsMill +--- + +# Backport a Fix + +Port a single fix that already exists on one branch onto another long-lived branch, **adapting it to the target branch's current code** instead of copying it verbatim. Used when a fix merged on a release/stable line has to also land on the development line (or the reverse), and a plain cherry-pick is wrong because the branches have diverged. + +This is **selective and adaptive**: one fix, reshaped to fit the target. That is the opposite of `merging-branches`, which carries a whole branch across wholesale. If the request is "sync all of `` into ``", use `merging-branches`; if it is "make *this fix* also work on ``", use this skill. + +This skill is repo-agnostic. The **core workflow** below applies to any OpsMill repo; the **Per-Project Specifics** block near the end is where a repo's concrete base-branch, changelog, and validation conventions live — read the target repo's `AGENTS.md` and `dev/` docs to fill it in. + +## The core insight + +**A clean cherry-pick is not proof of a correct backport.** The source fix encodes assumptions about the code as it was on the source branch. If the target branch has since diverged in the area the fix touches — a refactor, a new abstraction, a different event or scoping model — copying the fix can compile, merge cleanly, and still be wrong (a no-op, a double-apply, or a violation of an invariant the target added). The work is to understand *what the fix achieves*, then re-express that on the target's terms. + +## Inputs + +1. **A fix reference on the source branch** — a commit SHA (or range), a merged PR, or a ticket that names the fix. Resolve it to the actual commits and, critically, **the tests that came with it** (the reproduction test and any test the fix changed). +2. **The target branch** the fix must land on. + +If either is ambiguous (direction unclear, or only a ticket given), confirm the source commits and the target branch with the user before proceeding. + +## Steps to Follow + +1. **Locate the source fix and its tests.** From the commit/PR/ticket, identify every commit that makes up the fix, and read them in full: + + ```bash + git log --oneline | grep -i # find the commit(s) + git show # read the change AND its tests + ``` + + Note separately: the **behavior change** (the product code), the **reproduction test**, and any **existing test the fix modified** (often a test that asserted the now-fixed-wrong behavior). You will need all three on the target. + +2. **Determine the target base branch.** This is frequently **not** the repo's default branch. A stable→develop backport targets `develop`; the repo default may be `stable`. Confirm against the project's branch model (`AGENTS.md`, `dev/guidelines/git-workflow.md`) rather than assuming `origin/HEAD`. + +3. **Set up an isolated worktree off the target base** — the same fresh-worktree-off-`` flow that `merging-branches` (its integration-branch step) and `rebase` already perform, so the backport never disturbs the current checkout. Reuse that canonical flow rather than re-deriving it here; in particular it covers the case this worktree-based workflow actually hits — a branch or worktree pre-created on the *wrong* base — by verifying with `git merge-base --is-ancestor HEAD origin/` and resetting an unpushed branch with `git reset --hard origin/`. Initialise submodules and install dependencies in the new worktree before running anything. + + > This "fresh worktree off ``" setup is shared with `merging-branches` and `rebase`. Extracting it into one canonical reference the three skills share, so they cannot drift, is worth a follow-up. + +4. **Divergence check — the heart of this skill.** Read the target branch's *current* version of the subsystem the fix touches and compare it against what the source fix assumed: + + ```dot + digraph backport_decision { + rankdir=LR; + "Does the target's code in this area\nmatch the source's assumptions?" [shape=diamond]; + "Cherry-pick / apply as-is\n(then still re-validate on target)" [shape=box]; + "ADAPT: re-express the fix on the\ntarget's mechanism, re-derive via tests" [shape=box]; + "Does the target's code in this area\nmatch the source's assumptions?" -> "Cherry-pick / apply as-is\n(then still re-validate on target)" [label="identical"]; + "Does the target's code in this area\nmatch the source's assumptions?" -> "ADAPT: re-express the fix on the\ntarget's mechanism, re-derive via tests" [label="diverged"]; + } + ``` + + - **Identical:** a cherry-pick may apply, but you still re-validate on the target (step 7) — do not assume. + - **Diverged:** do **not** force the source diff in. Understand what the fix achieves, then implement that against the target's actual structures. Drive it with the reproduction test (port it first, watch it fail on the target, make it pass) — the same discipline as `fixing-bugs`. + +5. **Port the tests, and reconcile any test that asserted the old behavior.** + - Bring the reproduction test across; adapt fixtures/imports to the target. + - If the target has a test asserting the *pre-fix* behavior (e.g. "X does not happen"), the fix likely inverts it — update or replace that test to assert the corrected behavior. Find these before CI does. + +6. **Descope what the target's architecture blocks — never ship a no-op.** Part of the source fix may be unreachable on the target because the target enforces an invariant or lacks a mechanism the source relied on. When a sub-case cannot be made to work without a larger change: + - Do **not** ship code that emits the right signal but is silently ignored downstream — that *looks* fixed and is worse than an honest gap. + - Land the part that works, and capture the rest as a linked follow-up (use `creating-issues`), recording the root cause and the architectural constraint. + +7. **Validate the ported fix on the target.** Re-run the reproduction test and the reconciled tests **on the target worktree** — passing on the source branch proves nothing here. For runtime-behavioral fixes, exercise it for real (build/run) the way the source fix was validated, not just unit tests. + + > **Gate (T2-verify · P1):** paste the test/run output from the target worktree showing the ported fix actually works there. See `../quality-gates/gates/primitives/evidence-before-done.md`. + +8. **Changelog: don't duplicate an entry that will sync over.** If the project uses fragment-based changelogs and the source fix already carries a fragment, that fragment usually arrives on the target through the normal release→development sync. Adding your own duplicates it (and can collide on the next sync). Check whether the fragment already exists on the source branch; if so, omit it here. Add a fragment only when the target genuinely needs one the source won't provide. (Confirm against the project's changelog convention.) + +9. **Commit and open the PR** against the **target base** (not the repo default). Delegate branch/commit discipline to `/opsmill-dev-commit` and PR creation to `/opsmill-dev-pr`, then drive CI with `/opsmill-dev-monitoring-pull-requests`. + + > **Ship gate (T2 · P2 + P3) — before `gh pr create`.** Run the ship gate per `../quality-gates/gates/primitives/independent-judge.md` (judge → on-FAIL STOP → R2 degrade → write receipt on PASS). R1 criteria: the source fix's diff and its tests verbatim, plus the target's current code in the touched area (NOT your summary). Artifact: `git diff origin/...HEAD`. Forbidden evasions: the merge/rebase-gate evasions in `../quality-gates/gates/primitives/anti-gaming.md`. The judge confirms the fix was **adapted to the target's actual code** (not blindly cherry-picked), is **re-validated on the target**, and any **target-blocked sub-case was descoped into a follow-up**, not shipped as a silent no-op. + +10. **Report**: the PR URL and base branch, the final CI status from `/opsmill-dev-monitoring-pull-requests`, what was adapted vs applied as-is, what (if anything) was descoped and the follow-up issue link, and the changelog decision. + +## Important Rules + +- **Selective and adaptive, not wholesale.** One fix, reshaped for the target. Wholesale branch sync is `merging-branches`. +- **Adapt to the target's code.** A clean cherry-pick that compiles is not proof; when the target diverged, re-express the fix on its terms and re-derive with the reproduction test. +- **Re-validate on the target.** "It passed on the source branch" is not evidence for the target. +- **Never ship a no-op.** If part of the fix is architecturally blocked on the target, descope it into a linked follow-up — do not ship code that looks like a fix but is ignored downstream. +- **Target the right base branch** — usually not the repo default for a backport. +- **Don't duplicate a changelog fragment** that the normal branch sync will carry over. +- The skill's only outputs are the backport branch and its PR; it never pushes to the source, target, or any long-lived branch directly (that is `/opsmill-dev-commit`'s discipline). + +## Quality gates + +Gates for this skill follow `../quality-gates/gates/gate-model.md`. `backporting-fixes` is **Tier 2 — it lands an adapted fix and opens a PR**. + +| Gate | Step / trigger | Tier | Primitives | Pass criteria | On-fail | +|---|---|---|---|---|---| +| Validated-on-target | before opening the PR | T2-verify | P1 | Reproduction + reconciled tests pass **on the target worktree** (paste output); runtime fixes exercised for real. | STOP | +| Adapted-not-copied | before `gh pr create` | T2-ship | P2 + P3 | A fresh judge confirms the fix was adapted to the target's current code (not blindly cherry-picked), re-validated on the target, and any target-blocked sub-case was descoped into a follow-up rather than shipped as a silent no-op. | STOP; adapt/descope; do not open the PR | + +## Per-Project Specifics + +The core above is generic. Before backporting, read the target repo's `AGENTS.md`, `dev/guidelines/`, and branch-model docs, then record (or append) a block like the template below. + +### Template + +- **Branch model**: which branch is the backport target for which source (e.g. `stable` → `develop`), and whether the target differs from the repo default. +- **Changelog**: the fragment system and directory, and the rule for backports (usually: omit, the source fragment syncs over). +- **Worktree + setup**: where worktrees live, and the submodule-init / dependency-install commands needed before tests run. +- **Validation**: the local checks and the targeted test command for the touched area (and what is left for CI, e.g. a heavy integration/e2e job). +- **Divergence hot-spots**: subsystems known to differ between the branches, where adaptation (not cherry-pick) is the norm. + +Keep exact paths, tool-version pins, and known gotchas in the target repo's own `AGENTS.md` / `dev/` docs — reference them rather than duplicating (and stale-ing) them here. diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/opsmill-dev-commit/SKILL.md similarity index 99% rename from .agents/skills/commit/SKILL.md rename to .agents/skills/opsmill-dev-commit/SKILL.md index fe16f951..9d5c3c43 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/opsmill-dev-commit/SKILL.md @@ -1,9 +1,9 @@ --- -name: commit +name: opsmill-dev-commit description: >- Stages and commits the current changes onto a safe working branch, enforcing branch discipline and optionally pushing upstream. TRIGGER when: the user wants to commit, save, or check in the - current changes. DO NOT TRIGGER when: opening a pull request → pr. + current changes. DO NOT TRIGGER when: opening a pull request → opsmill-dev-pr. argument-hint: Optional `push` to push the branch upstream after committing. compatibility: Requires a Git working tree. metadata: diff --git a/.agents/skills/creating-issues/SKILL.md b/.agents/skills/opsmill-dev-creating-issues/SKILL.md similarity index 97% rename from .agents/skills/creating-issues/SKILL.md rename to .agents/skills/opsmill-dev-creating-issues/SKILL.md index a3862ebf..9f4c1b21 100644 --- a/.agents/skills/creating-issues/SKILL.md +++ b/.agents/skills/opsmill-dev-creating-issues/SKILL.md @@ -1,11 +1,11 @@ --- -name: creating-issues +name: opsmill-dev-creating-issues description: >- Turns a single feature idea, improvement, or bug into ONE well-structured GitHub issue. TRIGGER when: the user wants to file/open/create an issue, turn a feature idea or improvement into a ticket, or capture something missing or broken as a ticket. DO NOT TRIGGER when: breaking work into multiple issues or planning a body of work → a planning skill; writing a full Product - Requirements Document → creating-prd; the idea is still fuzzy and unhardened → grilling-ideas + Requirements Document → opsmill-dev-creating-prd; the idea is still fuzzy and unhardened → opsmill-dev-grilling-ideas first. argument-hint: Feature, improvement, or bug to turn into a GitHub issue compatibility: Requires GitHub access (gh CLI authenticated, or an equivalent GitHub MCP/API tool) and write access to the target repository. diff --git a/.agents/skills/creating-prd/SKILL.md b/.agents/skills/opsmill-dev-creating-prd/SKILL.md similarity index 95% rename from .agents/skills/creating-prd/SKILL.md rename to .agents/skills/opsmill-dev-creating-prd/SKILL.md index 27b1c98d..a4ca6843 100644 --- a/.agents/skills/creating-prd/SKILL.md +++ b/.agents/skills/opsmill-dev-creating-prd/SKILL.md @@ -1,11 +1,11 @@ --- -name: creating-prd +name: opsmill-dev-creating-prd description: >- Synthesises the current conversation context into a Product Requirements Document and publishes it to GitHub (as a comment on a referenced issue, or a new issue). Synthesises from context; does not interview. TRIGGER when: the conversation has produced enough understanding of a feature and the user wants it captured as a PRD. DO NOT TRIGGER when: a single small issue is - enough → creating-issues; the idea has not been stress-tested yet → grilling-ideas first; bug + enough → opsmill-dev-creating-issues; the idea has not been stress-tested yet → opsmill-dev-grilling-ideas first; bug reports. argument-hint: Optional — extra instructions, scope hints, or an explicit issue number/URL to target compatibility: Requires GitHub access (gh CLI authenticated, or an equivalent GitHub MCP/API tool) and write access to the target repository. Reads project context (AGENTS.md, CONTEXT.md, dev/constitution.md, ADRs, specs) when present and falls back gracefully when absent. @@ -33,18 +33,18 @@ Take the current conversation context plus codebase understanding and produce a Always show the draft to the user and wait for explicit approval before publishing. -**Do NOT interview the user.** This skill synthesises what you already know. If the context is too thin to write a PRD, say so and recommend `/grilling-ideas` first. +**Do NOT interview the user.** This skill synthesises what you already know. If the context is too thin to write a PRD, say so and recommend `/opsmill-dev-grilling-ideas` first. ## When to use - The conversation has produced enough understanding to articulate a feature, and the user now wants it captured as a PRD on GitHub. - The user explicitly asks for a PRD, a spec writeup, or an issue body. -- A previous `/grilling-ideas` session produced a sharpened idea brief and the user wants it turned into a PRD on the tracker. +- A previous `/opsmill-dev-grilling-ideas` session produced a sharpened idea brief and the user wants it turned into a PRD on the tracker. Do **not** use this skill for: -- Bug reports → use `/creating-issues` or the bug-pipeline skills. -- Capturing an idea you have *not* yet stress-tested — run `/grilling-ideas` first. +- Bug reports → use `/opsmill-dev-creating-issues` or the bug-pipeline skills. +- Capturing an idea you have *not* yet stress-tested — run `/opsmill-dev-grilling-ideas` first. - Writing the actual implementation spec — that is a downstream spec workflow's job (e.g. `/speckit-specify`). The PRD produced here is the **input** to that step. ## Phase 0 — Discover available context (read what exists, skip what doesn't) @@ -146,13 +146,16 @@ domain (e.g. developer, operator, admin, end-user) rather than inventing a gener priority structure if one exists.> ### P1 — + - Journey: <one sentence end-to-end> - Acceptance: **Given** <state>, **When** <action>, **Then** <outcome> ### P2 — <title> (optional) + … ### P3 — <title> (optional) + … ## Functional Requirements @@ -236,7 +239,7 @@ instead of the generic one below. A ticked box requires explicit discussion befo - [NEEDS CLARIFICATION: …] -<Cap at three. If more remain, the PRD is not ready — recommend /grilling-ideas before publishing.> +<Cap at three. If more remain, the PRD is not ready — recommend /opsmill-dev-grilling-ideas before publishing.> ## Further Notes @@ -253,7 +256,7 @@ instead of the generic one below. A ticked box requires explicit discussion befo - Is anything missing, wrong, or scope-creeping? - Approve to publish? -If the draft has more than three `[NEEDS CLARIFICATION]` markers, do not offer to publish — recommend `/grilling-ideas` to resolve them first. +If the draft has more than three `[NEEDS CLARIFICATION]` markers, do not offer to publish — recommend `/opsmill-dev-grilling-ideas` to resolve them first. ## Phase 5 — Publish @@ -294,7 +297,7 @@ The PRD's structure (User Journeys, FRs, Key Entities, Edge Cases, Success Crite ## Anti-patterns -- **Do not interview.** Synthesise from context. If you cannot, stop and recommend `/grilling-ideas`. +- **Do not interview.** Synthesise from context. If you cannot, stop and recommend `/opsmill-dev-grilling-ideas`. - **Do not invent labels.** `gh label list` first. - **Do not auto-re-triage someone else's issue.** Commenting must not silently change labels or state. - **Do not embed file paths or code snippets** in Implementation Decisions, except the prototype-snippet exception. Those rot. Save them for the planning step. @@ -316,4 +319,4 @@ A PRD published to GitHub at a known URL (comment on the referenced issue, or a --- -*Inspired by [`to-prd`](https://github.com/mattpocock/skills/blob/main/skills/engineering/to-prd/SKILL.md) by Matt Pocock. Adapted from a Styrmin-specific skill into a project-agnostic one for the opsmill-dev plugin; pairs with `/grilling-ideas`.* +*Inspired by [`to-prd`](https://github.com/mattpocock/skills/blob/main/skills/engineering/to-prd/SKILL.md) by Matt Pocock. Adapted from a Styrmin-specific skill into a project-agnostic one for the opsmill-dev plugin; pairs with `/opsmill-dev-grilling-ideas`.* diff --git a/.agents/skills/opsmill-dev-fixing-bugs/SKILL.md b/.agents/skills/opsmill-dev-fixing-bugs/SKILL.md new file mode 100644 index 00000000..985fa413 --- /dev/null +++ b/.agents/skills/opsmill-dev-fixing-bugs/SKILL.md @@ -0,0 +1,263 @@ +--- +name: opsmill-dev-fixing-bugs +description: >- + Implements and validates the fix for a bug once a failing reproduction test exists. TRIGGER + when: a bug has a failing reproduction test and you are ready to make it pass, implementing + the root-cause fix, the final step of the bug-fixing pipeline. DO NOT TRIGGER when: no + reproduction test exists yet → opsmill-dev-test-driving-bugs; still diagnosing, or asked to fix a bug with + no analysis or reproduction test yet → opsmill-dev-analyzing-bugs. +argument-hint: <issue number or URL, or bug description> +compatibility: >- + Works in any git repo; detects format/lint/test/changelog commands from the project rather + than assuming a toolchain. `gh` and a GitHub remote are needed only for the draft-PR path; the + fully-local flow (when `/opsmill-dev-test-driving-bugs` ran without `pr`) needs neither. +metadata: + pipeline: bug-fixing (3 of 3 — analyze → test-drive → fix) + version: 0.1.0 + author: OpsMill +user-invocable: true +disable-model-invocation: true +--- + +# Bug fixer + +## User Input + +```text +$ARGUMENTS +``` + +## Your role + +You are a senior engineer implementing a bug fix. Two prior steps have already completed: +`/opsmill-dev-analyzing-bugs` identified the root cause, and `/opsmill-dev-test-driving-bugs` wrote a failing test. Your job is to +fix the root cause. The test is your validation criteria -- it must pass -- but the analyst's +root cause analysis is what drives your fix, **not** the test. + +## Tool usage + +- Use the `Read` tool to read files -- do NOT use `cat` or `head`/`tail` in Bash. +- Use the `Glob` tool to find files -- do NOT use `find` or `ls -R` in Bash. +- Use the `Grep` tool to search file contents -- do NOT use `grep` or `rg` in Bash. +- Reserve Bash for git commands, `gh` CLI, and commands that require shell execution. +- *Shell* state (variables, `cd`) does **not** persist across separate Bash calls -- re-derive + shell values you reuse. The pipeline's logical flags like `HAS_PR` are decisions you carry in + your own reasoning, not shell variables, so they do persist across steps. + +## Input and setup + +Start from the analysis artifact, not a reconstructed slug. Discover it with `Glob` for +`.bug-analysis-*.md` in the repo root: + +- **No match:** inform the developer "Run `/opsmill-dev-analyzing-bugs <issue>` first." and **STOP**. +- **Exactly one match:** use it. +- **Multiple matches:** pick the one whose `<key>` best matches `$ARGUMENTS`; if still ambiguous, + list them and ask which to use. + +Read it for the root cause and fix strategy, and take the canonical `<key>` and **`Branch:`** from +its header fields. (If those fields are absent -- an older analysis -- fall back to the key in the +filename and `ai-bug-pipeline-<key>`.) Using the persisted branch -- rather than re-deriving the +slug -- is what keeps this step from dead-ending when the slug would have drifted. + +Find the draft PR opened by `/opsmill-dev-test-driving-bugs` on that branch: + +```bash +gh pr list --head "<branch>" --json number,title,body,headRefName --jq '.[0]' +``` + +**If a PR exists** (`/opsmill-dev-test-driving-bugs` ran with `pr`), set `HAS_PR=true` and validate it: + +- PR body must contain `AGENT_TEST_COMPLETE`. If not, inform the developer: + "No `AGENT_TEST_COMPLETE` marker found. Run `/opsmill-dev-test-driving-bugs` first." and **STOP**. +- PR body must NOT contain `AGENT_FIX_COMPLETE`. If it does, inform the developer: + "Fix has already been applied (`AGENT_FIX_COMPLETE` present)." and **STOP**. + +**Bind `<branch>` once, here:** set `<branch>` to the PR's `headRefName`. That is the branch the +PR tracks, and it is the single value every later step (checkout, verify, push) uses -- so you +never check out one branch and push another. It normally equals the persisted `Branch:`; if it +differs (a hand-edited PR, or an older analysis with no `Branch:`), `headRefName` wins -- note the +discrepancy to the developer. + +```bash +git fetch origin +git checkout "<branch>" # <branch> is now the PR's headRefName +``` + +**If no PR exists**, `/opsmill-dev-test-driving-bugs` was run without `pr` (fully local). Don't dead-end -- check +whether the branch itself exists: + +```bash +git rev-parse --verify "<branch>" 2>/dev/null || git rev-parse --verify "origin/<branch>" 2>/dev/null +``` + +- **Branch exists:** set `HAS_PR=false`, check it out (`git checkout "<branch>"`), and read its + diff against the default branch to find the test commit. Proceed -- there is no marker to + validate in local mode. +- **Branch does not exist either:** only now is the test genuinely missing. Inform the developer + "Run `/opsmill-dev-test-driving-bugs <issue>` first." and **STOP**. + +## Implement the fix + +Follow steps 1--9. + +### Step 1: Read fix strategy + +Read the analyst's fix strategy. This is your **starting point**: follow the recommended +approach, scope, and "Do NOT" guardrails. If you believe the strategy is wrong after reading the +code, **state your reasoning to the developer before implementing** -- do not silently ignore +it. + +### Step 2: Read failing test + +Read the failing test in the PR diff. This is your validation criteria -- the fix must make it +pass -- but design your fix based on the analyst's fix strategy and root cause, not on what the +test checks. + +### Step 3: Reason about the fix + +Before writing any code, reason explicitly about the fix and state it to the developer: + +- Is the root cause a shallow symptom (null check, off-by-one) or a deeper design issue? +- If shallow: a targeted fix is appropriate. +- If deeper: a proper fix may require refactoring the affected component. Do it -- do NOT paper + over a design flaw with a guard clause. + +### Step 4: Implement the fix + +- Fix the actual root cause, not just the symptom. +- Do NOT change the test the test-writer wrote. +- Do NOT refactor code unrelated to the root cause. +- If the proper fix requires changing more than expected, that is fine: explain why so the + reviewer understands the scope. +- Stage files **by name** (`git add path/to/file`) -- never `git add .` or `git add -A`. +- Commit the fix with an explicit commit message. + +### Step 5: Verify replication test passes + +Run the specific test the test-writer wrote, using the same runner they used (the PR body / test +file tells you which). + +- If the test still FAILS, revisit your fix. Do NOT proceed until it passes. +- Before continuing, verify `git diff` shows **no changes to the test file(s)** from the + test-writer's PR. If you accidentally modified a test file, revert those changes. + +> **Gate (T2-verify · P1):** paste the actual test-run output proving PASS. Do not write "the +> test passes" without it. See `../quality-gates/gates/primitives/evidence-before-done.md`. + +### Step 6: Pre-CI checks + +Run the project's pre-CI checks before pushing. **Detect the commands from the project** rather +than assuming a toolchain -- look in `AGENTS.md`, a `Makefile`/`invoke`/`tasks` file, +`pyproject.toml`, or `package.json` scripts. Apply them in this order, fixing and committing +issues as separate commits (do NOT amend previous commits): + +1. **Auto-format** (e.g. `uv run invoke format`, `ruff format`, `npx biome check --write .`, + `prettier --write`). If formatting changed source files, re-run the later phases. +2. **Regenerate** any generated artifacts the project maintains (schemas, GraphQL/OpenAPI + codegen, docs) if such tasks exist. +3. **Lint** (e.g. `ruff`, `mypy`/`ty`, `eslint`/`biome`, markdown/yaml/prose linters) as the + project defines. +4. **Unit tests** for the affected area (e.g. `uv run invoke backend.test-unit`, + `npm run test`). Run the broader suite the project expects for a change of this size. + +Stage any files changed by generation by name -- never `git add .` / `git add -A`. + +**Changelog:** if the project has a changelog mechanism, add an entry for this fix: + +- towncrier (a `[tool.towncrier]` config or a `changelog.d`/`newsfragments` dir): create a + fragment named after the issue, e.g. + `uv run towncrier create -c "<user-facing description>" <issue_number>.fixed.md`. When there is + no issue number (free-text bug), towncrier has no number to anchor on -- use its issue-less + form with a `+` prefix, e.g. `+<key>.fixed.md` (in the free-text case `<key>` is the slug, with + no issue prefix). +- a `dev/guidelines/changelog.md` describing another process: follow it. +- otherwise a top-level `CHANGELOG.md`: add a line under the appropriate section. + +Write changelog text from the user's perspective, past tense, one sentence, no jargon. Commit +the generated/edited file. If the project has no changelog mechanism, skip this and note it. + +> **Gate (T2-verify · P1):** paste the output of each pre-CI command (format, regenerate, lint, +> unit). A claim of "clean" without output fails the gate. + +### Step 7: Scope check + +If the fix requires changes to more than ~10 files, or fundamentally alters a public API +contract, **STOP** and escalate (see below). + +### Step 8: Push (PR mode) or hand off (local mode) + +**If `HAS_PR=true`:** push your fix commits to the PR branch **before** touching the PR body. The +`AGENT_FIX_COMPLETE` marker is the "done" signal, so the commits must already be on the branch +when it is stamped (Step 9) -- otherwise a failed push leaves the PR permanently flagged +fix-complete with no fix, and a re-run dead-ends at the "Fix has already been applied" STOP. + +```bash +git push -u origin "<branch>" +``` + +`<branch>` is the value bound during setup (the PR's `headRefName`) -- the same branch you checked +out, so the push always lands on the branch the PR tracks. + +If the push fails (protected branch, non-fast-forward, network), **STOP** and report it -- do +**not** proceed to stamp the marker, so a re-run can retry cleanly. Otherwise continue to Step 9. + +**If `HAS_PR=false` (local mode):** do NOT push. Leave the fix committed on the local branch +`<branch>` and tell the developer it is ready locally -- they can review and open a PR themselves +(or re-run `/opsmill-dev-test-driving-bugs … pr` first if they want the pipeline to manage one). You are done -- skip +Step 9. + +### Step 9: Update the PR and mark complete (only if `HAS_PR=true`) + +> **Ship gate (T2 · P2 + P3) — run before any PR edit or marker stamp.** +> Run the ship gate per `../quality-gates/gates/primitives/independent-judge.md` (judge → on-FAIL +> STOP → R2 degrade → write receipt on PASS, all defined there). R1 criteria: the +> `.bug-analysis-<key>.md` file verbatim (the root cause + fix strategy — NOT your summary). +> Artifact: `git diff <default-branch>...HEAD`. Forbidden evasions: the test-gate and fix-gate +> evasions from `../quality-gates/gates/primitives/anti-gaming.md`. + +With the commits already pushed, finalize the PR **last**: + +- Update the PR title to: `fix: <short description> (closes #<issue number>)` (omit the + `closes` clause if there is no issue). +- Update the PR body: if `.github/pull_request_template.md` exists, read it and fill in **every** + section using this task's context (write "N/A" for sections with nothing meaningful, e.g. + Screenshots -- do not skip or invent). If there is no template, write a concise body covering + the root cause, the fix, and how it was validated. +- Ensure the hidden marker `<!-- AGENT_FIX_COMPLETE -->` appears somewhere in the PR body; it is + the signal downstream automation uses to detect a completed fix, so it is added here, last. +- Use `gh pr edit` to apply the title and body. +- If the work is tied to a GitHub issue, post a comment on the issue linking to the updated PR. + +## Escalation + +If at any point you determine that: + +- the analyst's root cause is incorrect and the real cause is substantially different, +- the test cannot be made to pass with a correct fix (i.e. it tests the wrong behavior), or +- the fix is beyond the scope an automated agent should handle (step 7), + +then inform the developer explaining your findings and **STOP**. Do **not** stamp +`AGENT_FIX_COMPLETE` (Step 9): an unstamped PR -- even if fix commits were already pushed in +Step 8 -- correctly signals the fix is incomplete, and the developer can take it from there. + +## Quality gates + +Gates for this skill follow `../quality-gates/gates/gate-model.md`. `fixing-bugs` is **Tier 2** — it +ships a fix and stamps a completion marker. + +| Gate | Step / trigger | Tier | Primitives | Pass criteria | On-fail | +|---|---|---|---|---|---| +| Test-passes | Step 5 | T2-verify | P1 | The test-writer's test passes; `git diff` shows the test file unchanged. Paste the test run. | STOP; revisit fix | +| Pre-CI | Step 6 | T2-verify | P1 | Format/lint/unit all clean. Paste each command's output. | STOP; fix and re-run | +| Root-cause | before Step 9 stamp | T2-ship | P2 + P3 | A fresh judge, given the `.bug-analysis-<key>.md` verbatim (R1) and `git diff <base>...HEAD`, returns PASS: fix addresses the documented root cause (not a symptom), test untouched, scope respected. | STOP; do NOT stamp `AGENT_FIX_COMPLETE`; fix and re-judge | + +## Common mistakes + +| 🚩 Red flag | Do instead | +| --- | --- | +| Designing the fix from what the test checks | The analyst's root cause drives the fix; the test is only the validation gate | +| Editing the test file to make it pass | Never touch the test-writer's test — fix the production code | +| Papering over a design flaw with a guard clause | If the root cause is structural, fix it properly even if that means a larger change | +| Refactoring code unrelated to the root cause | Keep the change scoped; escalate if it must exceed ~10 files or change a public API | +| `git add .` / `git add -A` | Stage changed files by name | +| Stamping `AGENT_FIX_COMPLETE` before the push lands | In PR mode, push in Step 8 before stamping; the marker is the "done" signal, written last in Step 9 | diff --git a/.agents/skills/grilling-ideas/SKILL.md b/.agents/skills/opsmill-dev-grilling-ideas/SKILL.md similarity index 99% rename from .agents/skills/grilling-ideas/SKILL.md rename to .agents/skills/opsmill-dev-grilling-ideas/SKILL.md index ad20990f..1ad52cfe 100644 --- a/.agents/skills/grilling-ideas/SKILL.md +++ b/.agents/skills/opsmill-dev-grilling-ideas/SKILL.md @@ -1,11 +1,11 @@ --- -name: grilling-ideas +name: opsmill-dev-grilling-ideas description: >- Stress-tests a fuzzy or vague feature idea before any PRD, spec, or ticket is written. TRIGGER when: the user has a fuzzy feature idea — one or two paragraphs, vague on users / scope / success — and wants to harden it, or says "grill / stress-test / pressure-test this idea." DO NOT TRIGGER when: the idea is already turned into a spec or PRD; bug fixes or refactors; the - idea is hardened and you are ready to write the PRD → creating-prd. + idea is hardened and you are ready to write the PRD → opsmill-dev-creating-prd. metadata: version: 0.1.0 author: OpsMill @@ -145,32 +145,43 @@ Maintain the brief **in the conversation** as decisions land, using the structur **Seed**: <one-paragraph original idea> ## Users and Value + <who, what pain, what better looks like> ## User Journeys + ### P1 — <title> + - Journey: … - Given / When / Then: … + ### P2 — <title> (optional) + … ## Functional Requirements (draft) + - FR-001: System MUST … - FR-002: Users MUST be able to … ## Key Entities + - <Entity>: <role, relationships> — existing | new ## Edge Cases + - … ## Success Criteria (draft) + - SC-001: <measurable, tech-agnostic> ## Constitution Alignment (only if dev/constitution.md exists) + - <principle>: <how the idea fits / where it pushes back> ## Governance Gates Crossed + - [ ] Database / schema change - [ ] API change - [ ] New dependency @@ -179,12 +190,15 @@ Maintain the brief **in the conversation** as decisions land, using the structur - (Replace with the list from AGENTS.md if it names different gates.) ## Assumptions + - … ## Out of Scope (v1) + - … ## Open Questions + - [NEEDS CLARIFICATION: …] ``` diff --git a/.agents/skills/opsmill-dev-merging-branches/SKILL.md b/.agents/skills/opsmill-dev-merging-branches/SKILL.md new file mode 100644 index 00000000..bb8bca6c --- /dev/null +++ b/.agents/skills/opsmill-dev-merging-branches/SKILL.md @@ -0,0 +1,222 @@ +--- +name: opsmill-dev-merging-branches +description: >- + Merges one long-lived branch into another (e.g. a release branch into the main development + branch) on a fresh integration branch, or takes over a stalled merge-conflict PR. TRIGGER when: + phrasings like "merge <release> into <dev>", "take over this merge-conflict PR", or a GitHub PR + link for a branch sync. DO NOT TRIGGER when: replaying local work onto the latest base → opsmill-dev-rebase; + opening a pull request → opsmill-dev-pr. +argument-hint: Two branch names (`<source>` into `<destination>`) or a GitHub merge-PR link/number to take over +compatibility: Requires a Git working tree. Opening the draft PR requires GitHub access (gh CLI authenticated, GitHub MCP server, or equivalent). Driving the PR's CI to green uses the `opsmill-dev-monitoring-pull-requests` skill (and the `opsmill-dev-pr` skill to mark it ready-for-review). +metadata: + version: 0.1.0 + author: OpsMill +--- + +# Merge Branches + +Merge a source branch into a destination branch on a fresh integration branch, resolve conflicts, and open a draft PR that documents what conflicted. Used for periodic release→development syncs (e.g. `stable` → `develop`, `main` → `next`) and for taking over stalled merge-conflict PRs. + +This skill is repo-agnostic. The **core workflow** below applies to any OpsMill repo. The **Per-Project Specifics** section near the end is where a given repo's concrete conflict playbook lives — read your project's `AGENTS.md` and `dev/` docs to fill it in, using the template there as a starting shape, not a literal instruction set. + +## Inputs + +The skill accepts **one** of two input forms: + +1. **Two branch names** — `<source>` and `<destination>`. The source is merged *into* the destination. +2. **A GitHub PR reference** — a URL or number of an existing merge PR that is blocked on conflicts. Extract its base (destination) and head (source) branches; the new branch will *take over* that PR. + +If the input is ambiguous (e.g. only one branch named, or direction unclear), ask the user which is the source and which is the destination before proceeding. + +## Steps to Follow + +1. **Resolve inputs**: + - **If a PR reference was given**: fetch its metadata to determine source/destination and to reference it later: + + ```bash + gh pr view <pr-number-or-url> --json number,title,url,headRefName,baseRefName,body + ``` + + - `baseRefName` is the destination, `headRefName` is the source. + - Keep the PR number — the new PR description must mention it ("Supersedes #<n>"). + - **If two branches were given**: assign source and destination per the user's phrasing ("merge X into Y" → source `X`, destination `Y`). + +2. **Check working tree is clean**: + + ```bash + git status + ``` + + - If there are uncommitted changes, stash them (`git stash --include-untracked`) and restore at the end, or ask the user to handle them first. + +3. **Fetch latest refs** for both branches: + + ```bash + git fetch origin <source> <destination> + ``` + +4. **Create the integration branch** off the **destination**, named per the project's branch-naming guideline (often `dev/guidelines/git-workflow.md`). If the project states no convention, make the name self-describing — include the source, destination, and merge intent. + + ```bash + git checkout -b <branch-name> origin/<destination> + ``` + + - **If the branch already exists** (e.g. a worktree was pre-created with this name), verify its base before merging — it may be sitting on the wrong tip. Run `git merge-base --is-ancestor HEAD origin/<destination>` — exit **0** means `HEAD` is an ancestor of `origin/<destination>` (not diverged); **non-zero** means it sits on a different tip. If it exits non-zero and the branch is unpushed (`git ls-remote --heads origin <branch>` is empty), reset it: `git reset --hard origin/<destination>`. **For a PR takeover**, the integration branch must be a *fresh branch off the destination* — do not reuse the stuck PR's source branch. + +5. **Merge the source branch** (no fast-forward, so the merge is reviewable): + + ```bash + git merge --no-ff origin/<source> + ``` + + - If the merge completes cleanly, skip to step 9. + +6. **Commit the unresolved conflicts as the merge commit**, so each resolution lands as its own reviewable diff. The merge already staged what it could auto-merge, so stage only the still-conflicted paths and commit with markers intact: + + ```bash + git diff --name-only --diff-filter=U # the conflicted files + git add $(git diff --name-only --diff-filter=U) # stage only the conflicted paths + git commit --no-edit --no-verify # merge commit, conflict markers intact + ``` + + Use `--no-verify`: this commit intentionally carries conflict markers, and pre-commit hooks (lint, markers check) would otherwise reject it. The resolution commits in step 8 are verified normally. Do not resolve anything before this commit. + +7. **Categorise** each conflicted file: **mechanical** (resolve automatically — see below) vs **non-obvious** (raise to the user — see "When to Raise Conflicts"). Track a running list of what conflicted and how you resolve it — this becomes the PR description. + +8. **Resolve conflicts in follow-up commit(s)** on top of the merge commit, so each resolution is independently auditable: + - For each mechanical conflict, read the file, resolve it, then `git add <file>`. + - For **non-obvious conflicts**, STOP and present them to the user before resolving (see "When to Raise Conflicts"). Do not guess at semantic merges. + - A clean auto-merge is not proof of a correct merge. When one side did a sweeping rename/refactor and the other added new call sites, grep the merged tree for both the old and new symbols to confirm consistency. + - Commit in logical units — one commit per conflict type or file, with a message naming what was resolved — rather than one squashed commit. Reference these resolution commit hashes in the PR's "Conflicts resolved" list so a reviewer can jump straight to each. + +9. **Validate** before opening the PR. Run the project's locally-executable CI checks (see "Per-Project Specifics" for the concrete command) and regenerate any generated files touched by the merge (see "Generated Files"). A merge that leaves generated files stale will fail CI. Scope validation to what the merge actually changed — type-check, lint, and run the unit tests for the conflicted modules rather than the whole suite when the substantive change is confined to a few files; **state in the report what was not run** (e.g. functional/integration tests needing a running stack). + +> **Gate (T2-verify · P1):** paste the build/test run showing the merged tree is sound after resolving mechanical conflicts. See `../quality-gates/gates/primitives/evidence-before-done.md`. + +10. **Push the branch** (confirm with the user first — see Important Rules): + + ```bash + git push -u origin <branch-name> + ``` + +11. **Open the draft PR** (see "PR Description"): + + > **Ship gate (T2 · P2 + P3) — before opening the draft PR.** Run the ship gate per `../quality-gates/gates/primitives/independent-judge.md` (judge → on-FAIL STOP → R2 degrade → write receipt on PASS, all defined there). R1 criteria: the two branches' diffs / the conflicting hunks verbatim (NOT your summary). Artifact: your conflict resolution (the merge diff). Forbidden evasions: the merge/rebase-gate evasions from `../quality-gates/gates/primitives/anti-gaming.md`. The judge confirms refactors and non-obvious (semantic) conflicts were SURFACED for review, not silently resolved. + + ```bash + gh pr create --draft --base <destination> --head <branch-name> \ + --title "Merge <source> into <destination>" \ + --body "$(cat <<'EOF' + <body — see PR Description section> + EOF + )" + ``` + +12. **Drive the PR's CI to green** by invoking the `/opsmill-dev-monitoring-pull-requests` skill on the new PR. + +13. **Report** the new PR URL, the final CI status from `/opsmill-dev-monitoring-pull-requests`, and a summary of conflicts resolved / raised. Surface the conflicts and behavioral decisions only — omit task-tracking scaffolding. When the PR is green and ready, the `/opsmill-dev-pr` skill can promote it from draft to ready-for-review. + +14. **Restore any stashed changes** from step 2 (`git stash pop`), resolving conflicts if they arise. + +## When to Raise Conflicts + +Resolve **mechanical** conflicts silently; **raise** anything that requires a judgement call. Raise (do not auto-resolve) when: + +- Both sides changed the **same logic** in incompatible ways (not just adjacent lines). +- A conflict spans a **refactor** — code was moved, renamed, or restructured on one side and edited on the other. If the refactor was a pure move/rename/encapsulation and the incoming edit still makes sense in its new home, port it across and resolve — but **note it in the report** so a reviewer can sanity-check the placement. Raise it only when the surrounding logic itself changed, so the incoming edit no longer applies cleanly. +- Resolving requires **understanding intent** — e.g. two different bug fixes to the same function, conflicting API signatures, diverging business logic. +- A conflict touches **migrations**, **auth/authorization**, **API/GraphQL schema**, or **database** code where a wrong merge has outsized consequences (these are typically "Ask First" areas per the project's `AGENTS.md`). +- You are **not confident** the resolution preserves both sides' intent. + +When raising, show the conflicting hunks, explain what each side did, and propose options. Wait for direction before committing the merge. When a resolution combines both sides, adapt and run the matching test as part of it. + +## Mechanical Conflict Types + +> **`--ours` vs `--theirs` during a merge.** The integration branch is the **destination** and is checked out as `HEAD`, so during `git merge origin/<source>`, `--ours` = the destination and `--theirs` = the source. To "accept the destination version", use `--ours`. + +### Generated Files (regenerate, never hand-merge) + +Never resolve a generated file by hand. Accept the destination version, then run the project's generate task and re-stage: + +```bash +git checkout --ours <generated-paths...> +<project generate command> +git add <generated-paths...> +``` + +After regenerating, `git status` over the generated trees — an **empty result confirms the merge left nothing stale**. If a generator pulls from a submodule (or any external input), make sure that input is at the commit the merge records *before* trusting the diff. + +Lockfiles, generated client types, vendored submodules, and numbered migrations are common mechanical conflicts too, but how to resolve them is project-specific (which package manager and version, which generate command, which submodule, how migrations are numbered). Read the target repo's `AGENTS.md` / `dev/` docs and follow its **Per-Project Specifics** block — see the template below for the shape. + +### Changelog Fragments + +Fragment-based changelogs (e.g. towncrier under `changelog/`) use independent per-change files; conflicts are rare. If both sides added fragments, keep both. + +## PR Description + +The draft PR body must describe **what conflicted and how it was resolved**, so a reviewer can verify the merge without re-doing it: + +```markdown +## Summary + +Merges `<source>` into `<destination>`. + +## Conflicts resolved + +- `<file>` — <one line: what each side changed and how it was reconciled> (`<resolution commit hash>`) + +(If there were no conflicts: "Clean merge, no conflicts.") + +## Conflicts raised for review + +- `<file>` — <what required a judgement call and the decision taken> (omit section if none) + +## Generated files regenerated + +- <list of generate commands run> (omit if none) +``` + +If the input was a **PR takeover**, add at the top of the body: + +```markdown +Supersedes #<original-pr-number>, which was blocked on merge conflicts. +``` + +Use the bare `#<number>` form only (not the full PR URL). + +## Important Rules + +Recap of the guardrails enforced in the steps above: + +- The integration branch is created off the **destination**; the source is merged into it. Never the reverse. +- Always open the PR as a **draft** (`--draft`). +- Never auto-resolve conflicts in migrations, auth, API/GraphQL schema, or DB code without raising them first. +- Never edit generated files by hand — accept one side and regenerate. +- Confirm with the user before pushing. The skill's only outputs are the integration branch and a draft PR — it never pushes to the source, destination, or any long-lived branch. +- Run the project's local CI checks and ensure generated files are committed before marking the PR ready — CI validates this. + +## Quality gates + +Gates for this skill follow `../quality-gates/gates/gate-model.md`. `merging-branches` is **Tier 2 — it merges a source branch and opens a draft PR**. + +| Gate | Step / trigger | Tier | Primitives | Pass criteria | On-fail | +|---|---|---|---|---|---| +| Mechanical-conflicts | during resolution | T2-verify | P1 | Mechanical conflicts resolved; build/tests pass. Paste output. | STOP | +| Semantic-surfaced | before opening the draft PR | T2-ship | P2 + P3 | A fresh judge confirms refactors / non-obvious (semantic) conflicts were SURFACED for review, not silently resolved. | STOP; surface the conflicts; do not open the PR | + +## Per-Project Specifics + +The core above is deliberately generic. Before resolving, read the target repo's `AGENTS.md`, `dev/guidelines/`, and `dev/commands/` to learn its concrete paths and commands, then apply the matching conflict-type sections. Fill in (or append) a block like the template below for the repo you're in. + +### Template + +A per-project block is short — the conflict-type *what* lives in the core above; this records the repo's concrete *how*. Fill in the bullets that apply: + +- **Validation**: the repo's local CI checks to run before pushing (lint, type-check, generated-doc gates, the test scope). +- **Generated files**: which paths are generated, and the command that regenerates them (accept the destination version, regenerate, then stage). +- **Lockfiles / generated types**: the package manager and pinned version, and the regenerate command. +- **Submodules**: which submodules exist and how to sync them to the recorded commit. +- **Migrations**: how they're numbered and what to renumber/bump on conflict. +- **Canonical sync**: the usual direction, e.g. `<release-branch>` → `<development-branch>`. + +Keep exact paths, tool-version pins, and known generator gotchas in the target repo's own `AGENTS.md` / `dev/` docs — reference them rather than duplicating (and stale-ing) them here. diff --git a/.agents/skills/monitoring-pull-requests/SKILL.md b/.agents/skills/opsmill-dev-monitoring-pull-requests/SKILL.md similarity index 96% rename from .agents/skills/monitoring-pull-requests/SKILL.md rename to .agents/skills/opsmill-dev-monitoring-pull-requests/SKILL.md index 7ae71ded..e3845ece 100644 --- a/.agents/skills/monitoring-pull-requests/SKILL.md +++ b/.agents/skills/opsmill-dev-monitoring-pull-requests/SKILL.md @@ -1,10 +1,10 @@ --- -name: monitoring-pull-requests +name: opsmill-dev-monitoring-pull-requests description: >- Watches an open pull request's CI until green and fixes failing checks. TRIGGER when: the user wants to watch a pull request's CI, babysit a PR until it goes green, or fix failing CI checks - on an open PR. DO NOT TRIGGER when: opening the PR in the first place → pr; rebasing the branch - onto its base → rebase. + on an open PR. DO NOT TRIGGER when: opening the PR in the first place → opsmill-dev-pr; rebasing the branch + onto its base → opsmill-dev-rebase. argument-hint: Optional PR number or URL (defaults to the PR for the current branch) compatibility: Requires a Git working tree and GitHub access (gh CLI authenticated). Local reproduction uses whatever toolchain the project itself defines. metadata: @@ -27,7 +27,7 @@ This skill is project-agnostic: it discovers how to reproduce CI failures from t This skill is designed to run as a **background agent**, not in the user's foreground turn. CI commonly runs for tens of minutes, and a fix-and-retry loop multiplies that by up to 5 iterations. Blocking the parent conversation that long is wasteful — spawn this skill as an asynchronous agent and let it report back when it has something to say. - **Recommended invocation:** by another skill (notably `pr` Phase 7) using the runtime's background-agent primitive (Claude Code: `Agent` with `run_in_background: true`; equivalent in other runtimes). -- **Acceptable invocation:** directly by the user as `/monitoring-pull-requests` when they explicitly want to watch a PR. In that case the loop runs in the foreground and the user can interrupt. +- **Acceptable invocation:** directly by the user as `/opsmill-dev-monitoring-pull-requests` when they explicitly want to watch a PR. In that case the loop runs in the foreground and the user can interrupt. - The skill is **self-contained**: it captures `baseline_commit` itself in Phase 0 and resolves the PR/branch from arguments or current branch. No state needs to be inherited from the spawning conversation beyond what is passed in `$ARGUMENTS`. ## Arguments @@ -75,7 +75,7 @@ Every fix attempt produces exactly one new commit on the branch, appended to `fi - If `$ARGUMENTS` looks like a number → `pr_number = $ARGUMENTS`. - If `$ARGUMENTS` looks like a URL → extract the number with `gh pr view "$ARGUMENTS" --json number -q .number`. - - Otherwise → `pr_number = $(gh pr view --json number -q .number)` for the current branch. If no PR exists, STOP and tell the user to open one (or run `/pr` first). + - Otherwise → `pr_number = $(gh pr view --json number -q .number)` for the current branch. If no PR exists, STOP and tell the user to open one (or run `/opsmill-dev-pr` first). Then capture metadata: @@ -103,7 +103,7 @@ Every fix attempt produces exactly one new commit on the branch, appended to `fi Verify it matches `headRefOid` from step 2. If they differ, STOP — the branch is out of sync with the PR head and any revert would lose work. -5. **Refuse to operate on long-lived branches.** If `branch` is the repository's default branch (`git symbolic-ref --short refs/remotes/origin/HEAD`), a long-standing integration branch (`main`, `master`, `stable`, `develop`, `dev`, `trunk`), or a release branch (`release/*`, `release-*`, version-named branches), STOP. This skill is for feature PRs only. +5. **Refuse to operate on long-lived branches.** If `branch` is the repository's default branch (`git symbolic-ref --short refs/remotes/origin/HEAD | sed 's@^origin/@@'`), a long-standing integration branch (`main`, `master`, `stable`, `develop`, `dev`, `trunk`), or a release branch (`release/*`, `release-*`, version-named branches), STOP. This skill is for feature PRs only. 6. **Discover the project's local toolchain** (used by Phases 2–4). Read whatever project context exists — `AGENTS.md`/`CLAUDE.md`/`CONTRIBUTING.md`, `Makefile`/`Taskfile`/`justfile`, `package.json` scripts, `pyproject.toml`/`tox.ini`, `.pre-commit-config.yaml` — and note: @@ -279,7 +279,7 @@ Don't push until the relevant local checks are green. This is where most "blind git push origin <branch> ``` -6. **Increment `iteration`.** Record in `attempt_log[iteration]`: +6. **Record this iteration's outcome** in `attempt_log[iteration]` (the same index Phase 2 step 5 used for this pass), then increment `iteration`: ```text { @@ -329,7 +329,8 @@ If 5 iterations completed without reaching green, the fix attempts are likely mi 3. **Force-push with lease** so we don't clobber any commits the user may have pushed in parallel: ```bash - git push --force-with-lease=<branch>:<fix_commits[-1]> origin <branch> + # <last_fix_commit> = the final SHA appended to fix_commits (the last commit this skill pushed) + git push --force-with-lease=<branch>:<last_fix_commit> origin <branch> ``` The `--force-with-lease=<ref>:<expected_oid>` form ensures the push only succeeds if the remote tip is exactly the last commit this skill pushed. If the lease check fails, STOP — the user pushed something concurrently and the revert needs human review. @@ -353,6 +354,7 @@ Always produce this report at the end, regardless of outcome. ### Iterations #### Iteration 1 + - **Failing job:** <name> - **Failure summary:** <key log line(s)> - **Reproduced locally:** yes / no (<reason if no>) @@ -362,18 +364,22 @@ Always produce this report at the end, regardless of outcome. - **CI outcome after push:** green / still failing on <jobs> #### Iteration 2 + ... ### CI-only failures (if any) + <List any failures that could not be reproduced locally and the evidence gathered before pushing speculatively.> ### Current State + - Branch: `<branch>` at `<final sha>` - Uncommitted changes: <yes/no — `git status --short`> - Reverted: yes / no ### Recommendations + <If NOT FIXED: which iteration came closest, what hypotheses remain untested, what a human should look at next.> <If GREEN: any follow-up — flaky-looking failures, related tests to watch, diff --git a/.agents/skills/pr/SKILL.md b/.agents/skills/opsmill-dev-pr/SKILL.md similarity index 91% rename from .agents/skills/pr/SKILL.md rename to .agents/skills/opsmill-dev-pr/SKILL.md index 47b8e349..00ae43b8 100644 --- a/.agents/skills/pr/SKILL.md +++ b/.agents/skills/opsmill-dev-pr/SKILL.md @@ -1,12 +1,12 @@ --- -name: pr +name: opsmill-dev-pr description: >- Opens a pull request, publishing the current branch as a PR. TRIGGER when: the user wants to open a pull request, publish the current branch as a PR, or take the current work through to an - open PR. DO NOT TRIGGER when: only committing changes → commit; babysitting CI after the PR is - already open → monitoring-pull-requests. + open PR. DO NOT TRIGGER when: only committing changes → opsmill-dev-commit; babysitting CI after the PR is + already open → opsmill-dev-monitoring-pull-requests. argument-hint: Optional `commit` to stage, commit, and push uncommitted changes before opening the PR -compatibility: Requires a Git working tree and GitHub access (gh CLI authenticated, GitHub MCP server, or equivalent). Works best alongside the `commit` and `monitoring-pull-requests` skills from this plugin. +compatibility: Requires a Git working tree and GitHub access (gh CLI authenticated, GitHub MCP server, or equivalent). Works best alongside the `opsmill-dev-commit` and `opsmill-dev-monitoring-pull-requests` skills from this plugin. metadata: version: 0.1.0 author: OpsMill @@ -42,7 +42,7 @@ First, verify the agent is on a safe branch and understand the current git state - When ambiguous, ask the user. 4. **If on an unsafe branch** — the default branch, a long-standing integration branch, a release branch, or a placeholder/scratch branch (the `commit` skill's step 2 holds the canonical rules; don't re-derive the list here): - **If `commit` was passed:** proceed to Phase 2; the `commit` skill will refuse the unsafe branch, propose a properly named feature branch, and ask the user for approval before creating it. Do not pre-create the branch here — let the `commit` skill own that conversation so the rules stay in one place. - - **If `commit` was NOT passed:** STOP. There's no feature branch to open a PR from. Tell the user they need to either invoke `/pr commit` (so the `commit` skill creates a branch and captures the changes), or switch to a feature branch first. + - **If `commit` was NOT passed:** STOP. There's no feature branch to open a PR from. Tell the user they need to either invoke `/opsmill-dev-pr commit` (so the `commit` skill creates a branch and captures the changes), or switch to a feature branch first. 5. If already on a feature branch, continue to the next phase. ### 2. Commit Changes (only when `commit` argument is provided) @@ -55,7 +55,7 @@ When `commit` IS provided, run any project-specific prep first, then delegate th 2. If the project checks in generated artefacts (GraphQL schemas, OpenAPI clients, generated SDK types, protobufs) and their sources were modified, regenerate them using the project's documented commands and include the regenerated files — CI commonly fails on stale generated artefacts. -3. Invoke the `commit` skill with the `push` argument: `/commit push`. The skill will: +3. Invoke the `commit` skill with the `push` argument: `/opsmill-dev-commit push`. The skill will: - Refuse to commit on protected or placeholder branches and, if needed, propose a conventional feature branch name (`feat/…`, `fix/…`, etc.) for user approval before creating it. - Stage changes safely (explicit paths, secret-pattern warnings). - Draft a conventional-commit message and confirm it with the user. @@ -64,7 +64,7 @@ When `commit` IS provided, run any project-specific prep first, then delegate th Do not duplicate any of that logic inline here. If the user has additional commit-message guidance specific to this PR (e.g., spec or issue reference, conventional-commit scope), pass it along when invoking the skill. -4. After `/commit push` returns, confirm the working tree is clean and the branch is published before continuing to Phase 3. +4. After `/opsmill-dev-commit push` returns, confirm the working tree is clean and the branch is published before continuing to Phase 3. ### 3. Analyze All Branch Changes @@ -102,7 +102,7 @@ Before opening the PR, ensure that the project's documentation reflects the chan - Read each relevant doc and compare against the actual changes. - If a doc is outdated or missing coverage of new functionality, propose updates. - Present proposed doc changes to the user for approval. -3. Commit any approved doc updates to the branch (with a `docs:` conventional commit, following the repo's commit style). +3. If `commit` was passed, commit any approved doc updates to the branch (with a `docs:` conventional commit, following the repo's commit style). If `commit` was not passed, leave the approved edits uncommitted in the working tree and tell the user to run `/opsmill-dev-pr commit` to include them — without `commit`, the skill makes no commits (see Arguments). 4. Push if new commits were added. ### 5. Draft PR Description @@ -119,23 +119,29 @@ Focus on business value — what problem does this solve, what capability does i ```markdown ## Summary + [1-3 sentences: what business problem this solves or what capability it adds. Frame as outcomes for users/operators, not as code changes.] ## Key Changes + [Bulleted list of the most important changes, framed as outcomes: + - "Operators can now back up and restore environments" NOT "Added backup_service.py" - "Queries are validated against the schema in CI" NOT "Moved queries to .graphql files"] ## Related Context + [If tied to a spec, PRD, ADR, or issue: link it and highlight the key requirements addressed. Omit this section if none exists.] ## Documentation Updates + [List any docs that were added or updated as part of this PR. Omit this section if no docs were changed.] ## Test Plan + [How to verify — test commands to run, manual verification steps, or CI checks to watch] ``` @@ -180,7 +186,7 @@ Don't just open the PR and walk away — but don't re-implement the CI babysitti run_in_background: true, prompt: "Invoke the monitoring-pull-requests skill and run its workflow against PR #<N> on branch <branch>. The PR was just opened by - the /pr skill at commit <sha>; pass that commit as the + the /opsmill-dev-pr skill at commit <sha>; pass that commit as the expected baseline — monitoring-pull-requests re-captures and verifies the baseline_commit itself in its Phase 0. Follow every phase of the skill verbatim, including the narrow- @@ -198,7 +204,7 @@ Don't just open the PR and walk away — but don't re-implement the CI babysitti Do not poll or re-summarize CI here. The agent's final report is the closing status; surface it to the user when it lands. -If the runtime cannot spawn background agents, fall back to invoking `/monitoring-pull-requests <pr-number>` synchronously, or — as a last resort — poll `gh run list --branch <branch-name>` every 30–60 seconds and follow the same narrow-reproduction-first discipline `monitoring-pull-requests` enforces. Do not push speculative fixes when local reproduction is possible. +If the runtime cannot spawn background agents, fall back to invoking `/opsmill-dev-monitoring-pull-requests <pr-number>` synchronously, or — as a last resort — poll `gh run list --branch <branch-name>` every 30–60 seconds and follow the same narrow-reproduction-first discipline `monitoring-pull-requests` enforces. Do not push speculative fixes when local reproduction is possible. ## Notes diff --git a/.agents/skills/opsmill-dev-pruning-residues/SKILL.md b/.agents/skills/opsmill-dev-pruning-residues/SKILL.md new file mode 100644 index 00000000..3e9be746 --- /dev/null +++ b/.agents/skills/opsmill-dev-pruning-residues/SKILL.md @@ -0,0 +1,117 @@ +--- +name: opsmill-dev-pruning-residues +description: Use when an artifact has been argued, iterated, or debugged into its current shape and now carries rationale that only made sense in the process that produced it. Applies to docs, code comments, commit and PR/issue text, and config-file comments. Sweeps the artifact so it states what it does, not why it was argued into that shape. Triggers on "tidy this", "strip the residue", "make this read standalone". +argument-hint: A file path, code range, commit/PR, or the current diff to sweep; defaults to the artifact under discussion +--- + +# Prune Residue + +An artifact that was shaped through a process — a review thread, an iterative coding +session, a debugging back-and-forth — accumulates sentences and comments whose job was +to win or track that process. They are load-bearing *while the work is happening*. In the +finished artifact they are noise: whoever reads it next never saw the process and does not +need to relitigate it. + +The principle: **the artifact does the work, so it states what to do — not why it ended up +in this shape.** This skill sweeps an artifact for residue and cuts it, while protecting the +rationale that genuinely changes what the reader or maintainer does. + +It applies to: + +- **Any artifact** — reference docs, code comments, commit messages, PR/issue descriptions, + config-file comments. Anything carrying prose that a process left behind. +- **Any process** — human review, an AI pairing/coding session, a debugging loop, plain + iteration over time. The residue looks the same regardless of who or what produced it. + +## The Test + +This is the whole skill in one question. For every sentence, clause, or comment that +explains, justifies, compares, or narrates rather than instructs or documents: + +> Would a first-time reader or maintainer who never saw the process act differently without it? + +- **No** → it's residue. Cut it. +- **Yes — it prevents a mistake the reader would otherwise make, or it's needed to choose + between options the artifact actually presents** → keep it, but compress to the shortest + form that still carries the load. + +Word count is not the goal. The goal is an artifact where every remaining sentence or comment +either tells the reader what to do, documents what something is, or stops them doing the +wrong thing. + +## Residue Patterns to Flag + +These appear in prose and in code comments alike: + +- **Sequencing justification** — "we do X before Y so that…" when the steps or statements are + already ordered. The ordering enforces it; just state the step. +- **Defensive comparison** — "rather than Z, which would…", "instead of the obvious approach…", + "// using a map here instead of a list because…" — defending against an alternative that was + raised in the process but isn't present. The reader can't see Z, so the defense reads as noise. +- **Process artifacts** — "as discussed", "per feedback", "note that reviewers raised…", + "this addresses the concern that…", "// per PR review", "// changed to fix the failing test". +- **Meta-hedging** — "you might wonder why…", "it may seem odd, but…", "for clarity…". +- **Restated rationale** — a *why*-clause that only repeats what the instruction or code already + makes obvious. +- **Provenance narration** — "this previously used X; we switched to Y", "// was a loop, now a + comprehension", "// refactored per request". That belongs in version-control history, not in + the artifact. +- **Session narration in commit/PR text** — recounting the back-and-forth ("first tried A, then + B failed, so C") instead of stating what the change does and why it's correct now. + +## What to Keep (load-bearing rationale) + +Not all *why* is residue. Keep — but tighten — rationale that: + +- **Prevents a real mistake**: "use `--no-verify` because the commit carries conflict markers"; + "// must run before `init()` or the cache is cold" — an ordering constraint the code does not + enforce on its own. +- **Warns of a non-obvious consequence** the reader can't infer from the action or code itself: + "// O(n²), but n is bounded < 10 by the schema". +- **Records an external constraint** the artifact can't show: "// HACK: workaround for upstream + bug opsmill/infrahub#123, remove when fixed"; "// API returns dates as strings, not epochs". +- **Disambiguates a real fork** the artifact presents, where the reader must choose a branch. +- **Actionable `TODO`/`FIXME`** with a concrete next step (as opposed to a stale "// fix later"). + +When unsure whether a *why* is load-bearing, **keep it and flag it for the user** rather than +cut silently. + +## Where Not to Apply This + +Some artifacts exist *to record a decision and its alternatives* — there the rationale is the +payload, not residue. Do not sweep: + +- **Decision records** — ADRs (MADR "Considered Options" / "Consequences"), `DECISION.md`, + RFCs, design docs. "X instead of Y because Z" and what was rejected are the whole point. +- **Changelogs / release notes** — provenance ("changed from X to Y") is the content. +- **Post-mortems / retros** — the narrative of what happened is the value. + +The test still discriminates: this skill strips rationale that a *process deposited into a doc +whose job is to instruct*. When the doc's job is to record *why a choice was made*, leave its +rationale intact. + +## Steps to Follow + +1. **Identify the target** — the file, code range, commit, or PR the user named, or the artifact + under discussion. If it's a diff or PR, sweep only the changed prose and comments, not the + whole file. +2. **Read it whole.** Residue is usually only visible in context — a sentence or comment reads + fine in isolation but is redundant given the step, statement, or comment above it. +3. **Classify** each rationale-bearing sentence or comment with the Test above: cut, keep, or + tighten. +4. **Present the proposed cuts before editing** — a list of `quote → verdict → one-line reason`, + grouped cut / tighten / kept-but-flagged. Let the user veto any line. Do not edit first and + explain after. +5. **Apply** the approved cuts — touch prose and comments only (see Important Rules). +6. **Re-lint** with the repo's linters if present, and confirm clean. For code, confirm the + change is comment-only (e.g. the diff touches no executable lines). +7. **Re-read top-to-bottom as a first-time reader.** Does every step, comment, and statement + still stand alone without the cut text? If a cut left a gap, the rationale was load-bearing — + restore it in compressed form. + +## Important Rules + +- Preserve the artifact's voice and house style; this is a sweep, not a rewrite. +- **Never change executable code.** Edit comments and prose only; leave statements, logic, + commands, config values, and examples byte-for-byte intact. A comment is fair game; the line + it describes is not. diff --git a/.agents/skills/rebase/SKILL.md b/.agents/skills/opsmill-dev-rebase/SKILL.md similarity index 98% rename from .agents/skills/rebase/SKILL.md rename to .agents/skills/opsmill-dev-rebase/SKILL.md index 6a7bccf4..a43b0d81 100644 --- a/.agents/skills/rebase/SKILL.md +++ b/.agents/skills/opsmill-dev-rebase/SKILL.md @@ -1,11 +1,11 @@ --- -name: rebase +name: opsmill-dev-rebase description: >- Rebases a feature branch onto its latest base, resolving conflicts while preserving the intent of local changes, optionally force-pushing and watching CI afterward. TRIGGER when: the user wants to rebase a feature branch, update a branch against its base, or replay local work on top of the latest upstream. DO NOT TRIGGER when: merging a release branch into dev → - merging-branches; only watching CI on an already-open PR → monitoring-pull-requests. + opsmill-dev-merging-branches; only watching CI on an already-open PR → opsmill-dev-monitoring-pull-requests. argument-hint: Optional `push` to force-push and monitor CI after a successful rebase compatibility: Requires a Git working tree. CI monitoring requires GitHub access (gh CLI authenticated, GitHub MCP server, or equivalent). metadata: diff --git a/.agents/skills/opsmill-dev-test-driving-bugs/SKILL.md b/.agents/skills/opsmill-dev-test-driving-bugs/SKILL.md new file mode 100644 index 00000000..fdcf36f7 --- /dev/null +++ b/.agents/skills/opsmill-dev-test-driving-bugs/SKILL.md @@ -0,0 +1,292 @@ +--- +name: opsmill-dev-test-driving-bugs +description: >- + Writes a single failing test that reproduces a bug after its root-cause analysis is complete, + before any fix is written. TRIGGER when: a bug has a completed root-cause analysis and you need + the failing reproduction test, writing a test that proves a bug exists, the second step of the + bug-fixing pipeline. DO NOT TRIGGER when: still triaging or diagnosing the bug → opsmill-dev-analyzing-bugs; + implementing the fix once the test exists → opsmill-dev-fixing-bugs; general feature test-first work → + superpowers test-driven-development. +argument-hint: <issue number or URL, or bug description> [pr] +compatibility: >- + Works in any git repo with a test suite; discovers testing conventions from the OpsMill `dev/` + layout with auto-detection fallback. The draft-PR step needs `gh` and a GitHub remote. +metadata: + pipeline: bug-fixing (2 of 3 — analyze → test-drive → fix) + version: 0.1.0 + author: OpsMill +user-invocable: true +disable-model-invocation: true +--- + +# Bug test-writer + +## User Input + +```text +$ARGUMENTS +``` + +## Your role + +You are a senior QA engineer writing a targeted failing test that reproduces a confirmed bug. +`/opsmill-dev-analyzing-bugs` has already identified the root cause. Your job is to write **ONE** test that +fails on the current code, proving the bug exists. + +## Tool usage + +- Use the `Read` tool to read files -- do NOT use `cat` or `head`/`tail` in Bash. +- Use the `Glob` tool to find files -- do NOT use `find` or `ls -R` in Bash. +- Use the `Grep` tool to search file contents -- do NOT use `grep` or `rg` in Bash. +- Reserve Bash for git commands, `gh` CLI, and commands that require shell execution. +- *Shell* state (variables, `cd`) does **not** persist across separate Bash calls -- re-derive + any shell value you reuse rather than relying on one set in an earlier step. The pipeline's + logical flags like `OPEN_PR` are decisions you carry in your own reasoning, not shell variables, + so they do persist across steps. + +## Input and setup + +Parse `$ARGUMENTS` for an optional **`pr`** flag: if the word `pr` appears anywhere in the +arguments (case-insensitive), set `OPEN_PR=true`. Otherwise `OPEN_PR=false`. The rest of +`$ARGUMENTS` (issue number / URL / description) is only used to disambiguate when more than one +analysis exists -- do **not** re-derive the slug from it. + +Discover the handoff file `/opsmill-dev-analyzing-bugs` wrote, using `Glob` for `.bug-analysis-*.md` in the repo +root: + +- **No match:** inform the developer "Run `/opsmill-dev-analyzing-bugs <issue>` first." and **STOP**. +- **Exactly one match:** use it. +- **Multiple matches:** pick the one whose `<key>` best matches `$ARGUMENTS` (issue number, then + slug words). If still ambiguous, list them and ask the developer which to use. + +Read the full analysis. Take the canonical `<key>` and branch from its **`Key:`** and +**`Branch:`** header fields rather than re-deriving a slug -- this keeps the branch/PR names +consistent across steps. (If those fields are absent -- an older analysis -- fall back to the +key embedded in the filename, `.bug-analysis-<key>.md`, and `ai-bug-pipeline-<key>`.) + +If the analysis file is missing required fields (Root cause, Affected files), inform the +developer and **STOP**. + +## Write the test + +Follow steps 0--9 below. **Step 10 (draft PR) runs only when `OPEN_PR=true`.** If +`OPEN_PR=false`, the run stays **fully local**: stop after step 9 with the test committed on the +local branch `<branch>` -- do NOT push and do NOT open a PR. Display the test results and the +branch name, and tell the developer that `/opsmill-dev-fixing-bugs` will pick it up from that local branch. + +### Step 0: Create working branch + +Detect the default branch and create a working branch from it: + +```bash +DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') +# Fallback when origin/HEAD is unset (shallow/CI clones, manually-added remotes): +[ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH=$(git remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p') +[ "$DEFAULT_BRANCH" = "(unknown)" ] && DEFAULT_BRANCH="" # git prints "(unknown)" when remote HEAD is indeterminate +DEFAULT_BRANCH=${DEFAULT_BRANCH:-main} +git fetch origin "$DEFAULT_BRANCH" || { echo "Cannot fetch origin/$DEFAULT_BRANCH -- set the default branch manually and retry."; exit 1; } +# Fetch the working branch into a tracking ref with an explicit refspec, so origin/<branch> is +# created even on single-branch/shallow clones (a bare `git fetch origin <branch>` only updates +# FETCH_HEAD there). Silently a no-op when the branch doesn't exist on the remote yet. +git fetch origin "+refs/heads/<branch>:refs/remotes/origin/<branch>" 2>/dev/null +# Resume the branch reconciled to the pushed tip if it exists on the remote; else a local-only +# branch (a no-pr run not yet pushed); else create it fresh from the default branch: +if git rev-parse --verify -q "origin/<branch>" >/dev/null; then + git checkout -B "<branch>" "origin/<branch>" # remote tip wins -> never reuse a stale local copy +elif git rev-parse --verify -q "<branch>" >/dev/null; then + git checkout "<branch>" # local-only branch (no-pr run, never pushed) +else + git checkout -b "<branch>" "origin/$DEFAULT_BRANCH" +fi +``` + +- `<branch>` is the `Branch:` value from the analysis (`ai-bug-pipeline-<key>`) -- use it verbatim + so `/opsmill-dev-fixing-bugs` finds the same branch. +- Re-runs are deterministic -- see the inline comments for how each branch state (pushed, + local-only, absent) is handled and why the remote tip wins. +- If the **default-branch** `git fetch` fails, **STOP** and report it -- do not continue to write a + test with no working branch (the `exit 1` only fails that one shell call). The working-branch + fetch is best-effort (a missing remote branch is normal); a network failure there only means the + branch can't be resumed from the remote, in which case STOP rather than recreating from default. + +### Step 1: Read testing documentation + +Determine which area the bug lives in (backend, frontend, etc.) and read the project's testing +conventions. Anchor on the `dev/` layout, falling back to detection: + +- Read root `AGENTS.md` and `dev/documentation-architecture.md` (if present) to map the bug to a + code package. +- Backend testing docs, if present: `dev/knowledge/**/testing.md` and `dev/guidelines/**/testing.md`. +- Frontend testing docs, if present: `dev/guides/frontend/writing-unit-tests.md`, + `writing-component-tests.md`, `writing-e2e-tests.md`. +- **Fallback** (no `dev/` testing docs): detect the test runner and layout from the project + itself -- `pyproject.toml` / `tox.ini` / `pytest.ini` (pytest), `package.json` scripts + (vitest / jest / playwright), or a `Makefile` / `AGENTS.md` documenting the test command. + +### Step 2: Discover existing test setup + +Read the test setup local to the target test directory and its parents (e.g. `conftest.py` +files up the tree for pytest, shared setup/`setup.ts` for JS suites). Understand the available +fixtures, their scopes, and setup/teardown patterns. Do NOT reinvent setup logic that already +exists. + +### Step 3: Check reusable utilities + +Look for the project's reusable test helpers, base classes, factories, and fakes/adapters +(commonly under `tests/helpers/`, `tests/adapters/`, `tests/fake/`, or similar). Use these +instead of writing your own test infrastructure. + +### Step 4: Read existing tests + +Read 2--3 existing tests in the target test directory to learn the naming conventions, class +structure, and import patterns before writing your own. + +### Step 5: Choose the test type + +Pick the right test level using whatever guidance you found in step 1. Examples of common +levels: + +- **Python / pytest:** unit, component, functional, integration. Reuse existing schema fixtures + and helpers when available. +- **Frontend:** unit/component (e.g. Vitest, colocated `.test.ts`), E2E (e.g. Playwright). Use + a GIVEN/WHEN/THEN structure and existing factories when the project does. + +### Step 6: Write the test + +Write a single targeted test that reproduces the bug: + +- **Assert the CORRECT/EXPECTED behavior.** The test fails because the bug prevents the expected + behavior. Do NOT assert that the buggy behavior succeeds. For example: if the bug is + "duplicate records can be created," assert that the second creation raises an error or that + only one record exists -- this FAILS on buggy code and PASSES once fixed. +- The test MUST exercise the **actual production code path**, not a reimplementation. Call the + real functions/classes from the source. Do NOT copy production logic into the test. +- Test **observable behavior**, not internal implementation details (test that an API returns + wrong data or that a constraint is violated -- not that a constructor received specific + kwargs). +- **Test the affected code path** identified in the analysis "Affected files" section, not a + lower-level abstraction it calls internally. A test that can pass without changing the + affected code path is testing the wrong thing. +- Place it in the correct test folder following project conventions (test files usually mirror + source structure). If adding to an existing test file is more appropriate than creating a new + one, do that. + +### Step 7: Verify the test FAILS + +**CRITICAL: verify the test FAILS on the current code.** Run just this test using the project's +detected runner (e.g. `uv run pytest path::Test::test -x -v`, `npm run test -- <path>`, +`npx playwright test path`). Note the `--` for `npm run`: without it npm swallows the path +instead of forwarding it to the test runner. + +- If a test run takes more than ~5 minutes, kill it and investigate why. +- The failure must be an **assertion error that directly relates to the root cause** (e.g. + `AssertionError: Expected ValidationError but none was raised`, or `assert 2 == 1` when + duplicates were found). +- If the test **PASSES**, your assertions are wrong -- you are likely asserting buggy behavior. + Flip them to assert what SHOULD happen. +- If it fails for the **wrong reason** (import error, missing fixture, syntax error), fix those + and re-run until it fails for the reason in the root cause. + +> **Gate (T2-verify · P1):** paste the actual failing test run proving it fails for the documented reason (not an import/collection error). See `../quality-gates/gates/primitives/evidence-before-done.md`. + +### Step 8: Format and lint + +Run the project's formatter and linter on the test file(s) and fix any issues before committing. +Use whatever the project defines (e.g. `uv run invoke format` / `uv run invoke lint`, +`npx biome check --write .`, `ruff format`, `prettier`/`eslint`). Detect these from +`AGENTS.md` / `Makefile` / `pyproject.toml` / `package.json`. + +### Step 9: Commit test files + +Commit ONLY the test file(s). Do NOT touch production code. Stage files **by name** +(`git add path/to/test_file`) -- never `git add .` or `git add -A`, as unrelated working-tree +files would be committed by mistake. Commit message: `test: add failing test for <key>`. + +### Step 10: Open draft PR (only if `OPEN_PR=true`) + +> **Ship gate (T2 · P2 + P3) — before opening the draft PR or stamping `AGENT_TEST_COMPLETE`.** Run the ship gate per `../quality-gates/gates/primitives/independent-judge.md` (judge → on-FAIL STOP → R2 degrade → write receipt on PASS, all defined there). R1 criteria: the `.bug-analysis-<key>.md` verbatim (the root cause the test must prove — NOT your summary). Artifact: the test diff. Forbidden evasions: the test-gate evasions from `../quality-gates/gates/primitives/anti-gaming.md`. The judge confirms the test genuinely exercises the bug (not trivially-passing, not always-failing). + +Push the branch and open a **draft** Pull Request against the repo's default branch: + +```bash +git push -u origin "<branch>" || { echo "Push rejected (non-fast-forward / protected / network). STOP and resolve before retrying -- do not open or edit a PR."; exit 1; } +# Idempotent: on a resumed run a draft PR may already exist for this branch -- edit it +# instead of creating a duplicate (gh pr create errors if one already exists). Default to +# CREATE when the count is empty/non-numeric (a gh error), since create reports a real +# duplicate clearly, whereas edit on a missing PR would just fail. +PR_COUNT=$(gh pr list --head "<branch>" --state open --json number --jq 'length' 2>/dev/null) +if [ "${PR_COUNT:-0}" -gt 0 ] 2>/dev/null; then + gh pr edit "<branch>" --title "<title>" --body-file <tmp-body-file> +else + # No --base: gh pr create defaults the base to the repo's default branch, so the + # default-branch detection from Step 0 (which doesn't persist across Bash calls) isn't needed here. + gh pr create --draft --title "<title>" --body-file <tmp-body-file> +fi +``` + +Write the PR body to a temp file first (use the `Write` tool, e.g. under the system temp dir), +then pass it via `--body-file`. + +- Title: `test: failing test for <key> -- <short description>` +- PR body with this exact structure: + +````markdown +## Analyst's findings (summary) + +> **Root cause:** <copied from analysis> +> **Affected files:** +> <copied from analysis> + +## Replication test + +**Test file:** `path/to/test_file.ext` +**Test name:** `test_name_here` + +**What it tests:** <one sentence explaining the observable behavior being asserted> + +**Verification:** Test confirmed FAILING on current code. +**Failure reason:** <one sentence explaining HOW the test fails and why that proves the bug> + +<details><summary>Failure output (last 20 lines)</summary> + +```text +<paste the relevant failure output> +``` + +</details> + +## Test expectations + +<what the test asserts and any edge cases it covers -- NOT how to fix the bug> + +<!-- AGENT_TEST_COMPLETE --> +```` + +If the analysis was triggered by a GitHub issue, post a short comment on the issue linking to +the draft PR. + +## Escalation + +If the test cannot be made to fail for the right reason after 3 attempts, inform the developer +explaining what was tried and **STOP**. Do NOT open a PR or include the `AGENT_TEST_COMPLETE` +marker. + +## Quality gates + +Gates for this skill follow `../quality-gates/gates/gate-model.md`. `test-driving-bugs` is **Tier 2 — it writes a failing reproduction test and stamps `AGENT_TEST_COMPLETE` unconditionally** (in PR mode it also opens a draft PR; both paths require the full ship gate). + +| Gate | Step / trigger | Tier | Primitives | Pass criteria | On-fail | +|---|---|---|---|---|---| +| Test-fails-right | after writing the test | T2-verify | P1 | The new test FAILS for the documented reason. Paste the failing run. | STOP; fix the test | +| Test-catches-bug | before opening the draft PR / stamping `AGENT_TEST_COMPLETE` | T2-ship | P2 + P3 | A fresh judge, given the `.bug-analysis-<key>.md` verbatim and the test diff, confirms the test actually exercises the bug (not trivially-passing, not always-failing). | STOP; do not stamp; fix and re-judge | + +## Common mistakes + +| 🚩 Red flag | Do instead | +| --- | --- | +| Test passes on the current (buggy) code | You're asserting the buggy behavior — flip the assertions to the expected behavior so it FAILS now | +| Copying production logic into the test | Call the real functions/classes from the source; test the actual code path | +| Test passes without touching the affected files | Exercise the affected code path from the analysis, not a lower-level abstraction it calls | +| Editing production code "to help the test" | This step writes tests only — production code is `/opsmill-dev-fixing-bugs`'s job | +| `git add .` / `git add -A` | Stage the test file(s) by name | +| Opening the PR / writing `AGENT_TEST_COMPLETE` after a failed attempt | The marker is the "test ready" signal — only emit it once the test fails for the right reason | diff --git a/.claude/rules b/.claude/rules index 068c692d..2d5c9a97 120000 --- a/.claude/rules +++ b/.claude/rules @@ -1 +1 @@ -../dev/rules \ No newline at end of file +../.agents/rules \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff89b20e..306ce479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,27 +99,18 @@ jobs: if: | needs.files-changed.outputs.documentation == 'true' || needs.files-changed.outputs.github_workflows == 'true' - needs: ["files-changed"] + needs: ["files-changed", "prepare-environment"] runs-on: "ubuntu-latest" timeout-minutes: 5 steps: - name: "Check out repository code" uses: "actions/checkout@v7" - - name: "Linting: markdownlint" - uses: DavidAnson/markdownlint-cli2-action@v23 + - name: "Install uv" + uses: astral-sh/setup-uv@v7 with: - config: docs/.markdownlint.yaml - globs: | - **/*.{md,mdx} - !changelog/*.md - !.agents/commands/** - !.agents/skills/** - !.claude/commands/** - !.claude/skills/** - !dev/commands/** - !dev/skills/** - !.specify/templates/** - !.specify/extensions/** + version: ${{ needs.prepare-environment.outputs.UV_VERSION }} + - name: "Linting: rumdl" + run: "uvx rumdl@0.2.28 check ." action-lint: if: needs.files-changed.outputs.github_workflows == 'true' diff --git a/.gitignore b/.gitignore index 1be288cb..d4efe98f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ sandbox/ # SpecKit internal cache .specify/**/.cache/ +.specify/feature.json diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml deleted file mode 100644 index 0fb365c0..00000000 --- a/.markdownlint-cli2.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -# markdownlint-cli2 configuration -# https://github.com/DavidAnson/markdownlint-cli2#configuration - -globs: - - "**/*.{md,mdx}" - - "!.venv/**" - - "!node_modules/**" - - "!**/node_modules/**" - - "!.git/**" - - "!.claude/commands/**" - - "!dev/commands/**" - - "!.specify/templates/**" - -# Markdownlint settings are specified separately via --config flag diff --git a/AGENTS.md b/AGENTS.md index d38194f1..fbea0494 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ Foundational library for programmatically interacting with Infrahub. Abstracts t ```bash uv sync --all-groups --all-extras # Install all deps uv run invoke format # Format code -uv run invoke lint # Full pipeline: ruff, yamllint, ty, mypy, markdownlint, vale +uv run invoke lint # Full pipeline: ruff, yamllint, ty, mypy, rumdl, vale uv run invoke lint-code # All linters for Python code uv run invoke docs-generate # Generate all docs (CLI + SDK) uv run invoke docs-validate # Check generated docs match committed version diff --git a/changelog/1138.housekeeping.md b/changelog/1138.housekeeping.md new file mode 100644 index 00000000..4764d9fa --- /dev/null +++ b/changelog/1138.housekeeping.md @@ -0,0 +1 @@ +Replaced `markdownlint-cli2` with [rumdl](https://github.com/rvben/rumdl) for markdown linting. This removes the Node.js/npm dependency for the markdown check, speeds up linting, and consolidates the configuration into `pyproject.toml` under `[tool.rumdl]`. diff --git a/dev/constitution.md b/dev/constitution.md index a4670ff4..e63ff06b 100644 --- a/dev/constitution.md +++ b/dev/constitution.md @@ -1,46 +1,55 @@ # [PROJECT_NAME] Constitution + <!-- Example: Spec Constitution, TaskFlow Constitution, etc. --> ## Core Principles ### [PRINCIPLE_1_NAME] + <!-- Example: I. Library-First --> [PRINCIPLE_1_DESCRIPTION] <!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries --> ### [PRINCIPLE_2_NAME] + <!-- Example: II. CLI Interface --> [PRINCIPLE_2_DESCRIPTION] <!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats --> ### [PRINCIPLE_3_NAME] + <!-- Example: III. Test-First (NON-NEGOTIABLE) --> [PRINCIPLE_3_DESCRIPTION] <!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced --> ### [PRINCIPLE_4_NAME] + <!-- Example: IV. Integration Testing --> [PRINCIPLE_4_DESCRIPTION] <!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas --> ### [PRINCIPLE_5_NAME] + <!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity --> [PRINCIPLE_5_DESCRIPTION] <!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles --> ## [SECTION_2_NAME] + <!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. --> [SECTION_2_CONTENT] <!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. --> ## [SECTION_3_NAME] + <!-- Example: Development Workflow, Review Process, Quality Gates, etc. --> [SECTION_3_CONTENT] <!-- Example: Code review requirements, testing gates, deployment approval process, etc. --> ## Governance + <!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan --> [GOVERNANCE_RULES] diff --git a/docs/.markdownlint.yaml b/docs/.markdownlint.yaml deleted file mode 100644 index f0087fa9..00000000 --- a/docs/.markdownlint.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -default: true -MD013: false # disables max line-length -MD014: false # dollar signs used before commands -MD024: false # disables 'no duplicate headings', which we use in tabs for instructions -MD025: - front_matter_title: "" # prevent collisions with h1s and frontmatter titles -MD029: false # allows manually creating ordered lists -MD033: false # allows inline html to override markdown styles -MD034: false # no-bare-urls -MD041: false # allow 1st line to not be a top-level heading (required for Towncrier) -MD045: false # no alt text around images -MD047: false # single trailing newline -MD059: # Link descriptions that are prohibited - prohibited_texts: [] diff --git a/docs/docs/python-sdk/guides/installation.mdx b/docs/docs/python-sdk/guides/installation.mdx index d50eaf96..5478a956 100644 --- a/docs/docs/python-sdk/guides/installation.mdx +++ b/docs/docs/python-sdk/guides/installation.mdx @@ -1,6 +1,7 @@ --- title: Installing infrahub-sdk --- + # Installing infrahub-sdk The Infrahub SDK for Python is available on [PyPI](https://pypi.org/project/infrahub-sdk/) and can be installed using the pip package installer. It is recommended to install the SDK into a virtual environment. @@ -15,7 +16,9 @@ pip install infrahub-sdk Extras can be installed as part of the Python SDK and are not installed by default. <!-- vale off --> + ### ctl + <!-- vale on --> The `ctl` extra provides the `infrahubctl` command, which allows you to interact with an Infrahub instance. @@ -23,8 +26,11 @@ The `ctl` extra provides the `infrahubctl` command, which allows you to interact ```shell pip install 'infrahub-sdk[ctl]' ``` + <!-- vale off --> + ### tests + <!-- vale on --> The `tests` extra provides all the components for the testing framework of Transforms, Queries and Checks. @@ -32,8 +38,11 @@ The `tests` extra provides all the components for the testing framework of Trans ```shell pip install 'infrahub-sdk[tests]' ``` + <!-- vale off --> + ### all + <!-- vale on --> Installs `infrahub-sdk` together with all the extras. diff --git a/docs/docs/python-sdk/guides/tracking.mdx b/docs/docs/python-sdk/guides/tracking.mdx index 293bd65c..2afb3f67 100644 --- a/docs/docs/python-sdk/guides/tracking.mdx +++ b/docs/docs/python-sdk/guides/tracking.mdx @@ -106,12 +106,14 @@ This behavior ensures that the tracking group specifically reflects changes made When enabling tracking mode with the `start_tracking` method, you can customize the tracking session with the following parameters: <!-- vale off --> + | Parameter | Description | |------------------------|-----------------------------------------------------------------------------------------------------------------------------| | **identifier** | Unique string to identify the session, used for correlating operations and logs. Defaults to "python-sdk" if not specified. | | **params** | Optional dictionary for extra context, enabling fine-grained control over tracking. | | **delete_unused_nodes**| Boolean indicating if nodes not referenced should be automatically deleted, helping maintain a clean state. | | **group_type** | Type of group object for tracking, default is `CoreStandardGroup`, customizable for specific grouping logic. | + <!-- vale on --> These parameters provide flexibility, enabling detailed auditing, efficient data management, and support for idempotent operations. @@ -245,4 +247,4 @@ if group: ``` </TabItem> -</Tabs> +</Tabs> \ No newline at end of file diff --git a/docs/docs/python-sdk/reference/compatibility.mdx b/docs/docs/python-sdk/reference/compatibility.mdx index 8763c4e1..586e5f9e 100644 --- a/docs/docs/python-sdk/reference/compatibility.mdx +++ b/docs/docs/python-sdk/reference/compatibility.mdx @@ -10,6 +10,7 @@ This page documents which versions of the Infrahub Python SDK are compatible wit Each Infrahub release pins a specific SDK version. Using the matching SDK version ensures full compatibility. Newer patch releases of the SDK within the same minor version are generally safe to use. <!-- vale off --> + | Infrahub | Required SDK | Release date | | --- | --- | --- | | 1.9.x | >= 1.20.0 | April 2026 | @@ -23,6 +24,7 @@ Each Infrahub release pins a specific SDK version. Using the matching SDK versio | 1.1.x | >= 1.3.0 | December 2024 | | 1.0.x | >= 1.0.0 | October 2024 | | 0.16.x | >= 0.13.1 | September 2024 | + <!-- vale on --> ## Detailed release mapping @@ -30,6 +32,7 @@ Each Infrahub release pins a specific SDK version. Using the matching SDK versio The table below shows the exact SDK version pinned to each Infrahub release. <!-- vale off --> + | Infrahub | SDK version | Infrahub release date | | --- | --- | --- | | 1.9.3 | 1.20.0 | 2026-05-05 | @@ -123,16 +126,19 @@ The table below shows the exact SDK version pinned to each Infrahub release. | 0.16.3 | 0.14.0 | 2024-10-10 | | 0.16.2 | 0.13.1 | 2024-10-01 | | 0.16.1 | 0.13.1 | 2024-09-25 | + <!-- vale on --> ## Python version support <!-- vale off --> + | SDK version | Python versions | | --- | --- | | >= 1.17.0 | 3.10, 3.11, 3.12, 3.13, 3.14 | | 1.16.0 | 3.10, 3.11, 3.12, 3.13 | | 1.13.0 - 1.15.x | 3.9, 3.10, 3.11, 3.12, 3.13 | + <!-- vale on --> :::note @@ -144,11 +150,13 @@ The Infrahub server requires Python 3.12 or later. The SDK supports older Python Some SDK features require a minimum Infrahub version: <!-- vale off --> + | Feature | Minimum SDK | Minimum Infrahub | | --- | --- | --- | | infrahubctl branch report | 1.19.0 | 1.7 | | FileObject support | 1.19.0 | 1.8 | | NumberPool support | 1.13.0 | 1.3 | + <!-- vale on --> ## General guidance diff --git a/docs/docs/python-sdk/reference/config.mdx b/docs/docs/python-sdk/reference/config.mdx index bbb1b8a7..ffd36fd6 100644 --- a/docs/docs/python-sdk/reference/config.mdx +++ b/docs/docs/python-sdk/reference/config.mdx @@ -29,153 +29,199 @@ The Python SDK (Async or Sync) client can be configured using an instance of the The following settings can be defined in the `Config` class <!-- vale off --> + ## address + <!-- vale on --> **Description**: The URL to use when connecting to Infrahub.<br /> **Type**: `string`<br /> **Default value**: http://localhost:8000<br /> **Environment variable**: `INFRAHUB_ADDRESS`<br /> <!-- vale off --> + ## api_token + <!-- vale on --> **Description**: API token for authentication against Infrahub.<br /> **Type**: `string`<br /> **Environment variable**: `INFRAHUB_API_TOKEN`<br /> <!-- vale off --> + ## echo_graphql_queries + <!-- vale on --> **Description**: If set the GraphQL query and variables will be echoed to the screen<br /> **Type**: `boolean`<br /> **Default value**: False<br /> **Environment variable**: `INFRAHUB_ECHO_GRAPHQL_QUERIES`<br /> <!-- vale off --> + ## username + <!-- vale on --> **Description**: Username for accessing Infrahub<br /> **Type**: `string`<br /> **Environment variable**: `INFRAHUB_USERNAME`<br /> <!-- vale off --> + ## password + <!-- vale on --> **Description**: Password for accessing Infrahub<br /> **Type**: `string`<br /> **Environment variable**: `INFRAHUB_PASSWORD`<br /> <!-- vale off --> + ## default_branch + <!-- vale on --> **Description**: Default branch to target if not specified for each request.<br /> **Type**: `string`<br /> **Default value**: main<br /> **Environment variable**: `INFRAHUB_DEFAULT_BRANCH`<br /> <!-- vale off --> + ## default_branch_from_git + <!-- vale on --> **Description**: Indicates if the default Infrahub branch to target should come from the active branch in the local Git repository.<br /> **Type**: `boolean`<br /> **Default value**: False<br /> **Environment variable**: `INFRAHUB_DEFAULT_BRANCH_FROM_GIT`<br /> <!-- vale off --> + ## identifier + <!-- vale on --> **Description**: Tracker identifier<br /> **Type**: `string`<br /> **Environment variable**: `INFRAHUB_IDENTIFIER`<br /> <!-- vale off --> + ## insert_tracker + <!-- vale on --> **Description**: Insert a tracker on queries to the server<br /> **Type**: `boolean`<br /> **Default value**: False<br /> **Environment variable**: `INFRAHUB_INSERT_TRACKER`<br /> <!-- vale off --> + ## max_concurrent_execution + <!-- vale on --> **Description**: Max concurrent execution in batch mode<br /> **Type**: `integer`<br /> **Default value**: 5<br /> **Environment variable**: `INFRAHUB_MAX_CONCURRENT_EXECUTION`<br /> <!-- vale off --> + ## mode + <!-- vale on --> **Description**: Default mode for the client<br /> **Type**: `object`<br /> **Environment variable**: `INFRAHUB_MODE`<br /> <!-- vale off --> + ## pagination_size + <!-- vale on --> **Description**: Page size for queries to the server<br /> **Type**: `integer`<br /> **Default value**: 50<br /> **Environment variable**: `INFRAHUB_PAGINATION_SIZE`<br /> <!-- vale off --> + ## retry_delay + <!-- vale on --> **Description**: Number of seconds to wait until attempting a retry.<br /> **Type**: `integer`<br /> **Default value**: 5<br /> **Environment variable**: `INFRAHUB_RETRY_DELAY`<br /> <!-- vale off --> + ## retry_on_failure + <!-- vale on --> **Description**: Retry operation in case of failure<br /> **Type**: `boolean`<br /> **Default value**: False<br /> **Environment variable**: `INFRAHUB_RETRY_ON_FAILURE`<br /> <!-- vale off --> + ## max_retry_duration + <!-- vale on --> **Description**: Maximum duration until we stop attempting to retry if enabled.<br /> **Type**: `integer`<br /> **Default value**: 300<br /> **Environment variable**: `INFRAHUB_MAX_RETRY_DURATION`<br /> <!-- vale off --> + ## schema_converge_timeout + <!-- vale on --> **Description**: Number of seconds to wait for schema to have converged<br /> **Type**: `integer`<br /> **Default value**: 60<br /> **Environment variable**: `INFRAHUB_SCHEMA_CONVERGE_TIMEOUT`<br /> <!-- vale off --> + ## timeout + <!-- vale on --> **Description**: Default connection timeout in seconds<br /> **Type**: `integer`<br /> **Default value**: 60<br /> **Environment variable**: `INFRAHUB_TIMEOUT`<br /> <!-- vale off --> + ## transport + <!-- vale on --> **Description**: Set an alternate transport using a predefined option<br /> **Type**: `object`<br /> **Environment variable**: `INFRAHUB_TRANSPORT`<br /> <!-- vale off --> + ## proxy + <!-- vale on --> **Description**: Proxy address<br /> **Type**: `string`<br /> **Environment variable**: `INFRAHUB_PROXY`<br /> <!-- vale off --> + ## proxy_mounts + <!-- vale on --> **Description**: Proxy mounts configuration<br /> **Type**: `object`<br /> **Environment variable**: `INFRAHUB_PROXY_MOUNTS`<br /> <!-- vale off --> + ## marketplace_url + <!-- vale on --> **Description**: Base URL for the Infrahub Marketplace.<br /> **Type**: `string`<br /> **Default value**: https://marketplace.infrahub.app<br /> **Environment variable**: `INFRAHUB_MARKETPLACE_URL`<br /> <!-- vale off --> + ## update_group_context + <!-- vale on --> **Description**: Update GraphQL query groups<br /> **Type**: `boolean`<br /> **Default value**: False<br /> **Environment variable**: `INFRAHUB_UPDATE_GROUP_CONTEXT`<br /> <!-- vale off --> + ## tls_insecure + <!-- vale on --> **Description**: Indicates if TLS certificates are verified. @@ -185,7 +231,9 @@ The following settings can be defined in the `Config` class **Default value**: False<br /> **Environment variable**: `INFRAHUB_TLS_INSECURE`<br /> <!-- vale off --> + ## tls_ca_file + <!-- vale on --> **Description**: File path to CA cert or bundle in PEM format<br /> **Type**: `string`<br /> diff --git a/docs/docs/python-sdk/reference/templating.mdx b/docs/docs/python-sdk/reference/templating.mdx index 191d249e..52dff67e 100644 --- a/docs/docs/python-sdk/reference/templating.mdx +++ b/docs/docs/python-sdk/reference/templating.mdx @@ -33,6 +33,7 @@ For backward compatibility, `validate(restricted=True)` maps to `CORE` and `vali The following filters are [shipped with Jinja2](https://jinja.palletsprojects.com/en/stable/templates/#list-of-builtin-filters) and enabled within Infrahub. <!-- vale off --> + | Name | CORE | WORKER | LOCAL | | ---- | ---- | ------ | ----- | | abs | ✅ | ✅ | ✅ | @@ -89,6 +90,7 @@ The following filters are [shipped with Jinja2](https://jinja.palletsprojects.co | wordcount | ✅ | ✅ | ✅ | | wordwrap | ✅ | ✅ | ✅ | | xmlattr | ❌ | ✅ | ✅ | + <!-- vale on --> ## Netutils filters @@ -96,6 +98,7 @@ The following filters are [shipped with Jinja2](https://jinja.palletsprojects.co The following Jinja2 filters from <a href="https://netutils.readthedocs.io">Netutils</a> are included within Infrahub. <!-- vale off --> + | Name | CORE | WORKER | LOCAL | | ---- | ---- | ------ | ----- | | abbreviated_interface_name | ✅ | ✅ | ✅ | @@ -176,6 +179,7 @@ The following Jinja2 filters from <a href="https://netutils.readthedocs.io">Netu | vlanconfig_to_list | ✅ | ✅ | ✅ | | vlanlist_to_config | ✅ | ✅ | ✅ | | wildcardmask_to_netmask | ✅ | ✅ | ✅ | + <!-- vale on --> ## Infrahub filters @@ -183,6 +187,7 @@ The following Jinja2 filters from <a href="https://netutils.readthedocs.io">Netu These filters are provided by the Infrahub SDK for artifact and file object content composition. <!-- vale off --> + | Name | CORE | WORKER | LOCAL | | ---- | ---- | ------ | ----- | | `artifact_content` | ❌ | ✅ | ❌ | @@ -191,6 +196,7 @@ These filters are provided by the Infrahub SDK for artifact and file object cont | `file_object_content_by_id` | ❌ | ✅ | ❌ | | `from_json` | ✅ | ✅ | ✅ | | `from_yaml` | ✅ | ✅ | ✅ | + <!-- vale on --> ### Usage examples diff --git a/docs/docs/python-sdk/sdk_ref/infrahub_sdk/client.mdx b/docs/docs/python-sdk/sdk_ref/infrahub_sdk/client.mdx index 00c44dc6..b4ff5410 100644 --- a/docs/docs/python-sdk/sdk_ref/infrahub_sdk/client.mdx +++ b/docs/docs/python-sdk/sdk_ref/infrahub_sdk/client.mdx @@ -59,6 +59,7 @@ get(self, kind: str | type[SchemaType], raise_when_missing: bool = True, at: Tim ``` </details> + #### `delete` ```python @@ -87,6 +88,7 @@ create(self, kind: type[SchemaType], data: dict | None = ..., branch: str | None ``` </details> + #### `get_version` ```python @@ -264,6 +266,7 @@ Retrieve all nodes of a given kind. - list\[InfrahubNode]: List of Nodes </details> + #### `filters` ```python @@ -312,6 +315,7 @@ Retrieve nodes of a given kind based on provided filters. - list\[InfrahubNodeSync]: List of Nodes that match the given filters. </details> + #### `clone` ```python @@ -439,6 +443,7 @@ Allocate a new IP address by using the provided resource pool. - `ValueError`: If ``resource_pool`` is not a ``CoreIPAddressPool``. </details> + #### `allocate_next_ip_prefix` ```python @@ -483,6 +488,7 @@ Allocate a new IP prefix by using the provided resource pool. - `ValueError`: If ``resource_pool`` is not a ``CoreIPPrefixPool``. </details> + #### `create_batch` ```python @@ -563,6 +569,7 @@ get(self, kind: str | type[SchemaTypeSync], raise_when_missing: bool = True, at: ``` </details> + #### `delete` ```python @@ -591,6 +598,7 @@ create(self, kind: type[SchemaTypeSync], data: dict | None = ..., branch: str | ``` </details> + #### `get_version` ```python @@ -808,6 +816,7 @@ Retrieve all nodes of a given kind. - list\[InfrahubNodeSync]: List of Nodes </details> + #### `filters` ```python @@ -856,6 +865,7 @@ Retrieve nodes of a given kind based on provided filters. - list\[InfrahubNodeSync]: List of Nodes that match the given filters. </details> + #### `create_batch` ```python @@ -948,6 +958,7 @@ Allocate a new IP address by using the provided resource pool. - `ValueError`: If ``resource_pool`` is not a ``CoreIPAddressPool``. </details> + #### `allocate_next_ip_prefix` ```python @@ -992,6 +1003,7 @@ Allocate a new IP prefix by using the provided resource pool. - `ValueError`: If ``resource_pool`` is not a ``CoreIPPrefixPool``. </details> + #### `repository_update_commit` ```python diff --git a/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/node.mdx b/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/node.mdx index 8c48dc0b..99671d1e 100644 --- a/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/node.mdx +++ b/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/node.mdx @@ -178,6 +178,7 @@ The node must have been saved (have an id) before calling this method. ``` </details> + #### `matches_local_checksum` ```python @@ -696,6 +697,7 @@ The node must have been saved (have an id) before calling this method. ``` </details> + #### `matches_local_checksum` ```python diff --git a/docs/package-lock.json b/docs/package-lock.json index 8f9d613c..a445e87b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -13,7 +13,6 @@ "@iconify/react": "^6.0.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", - "markdownlint-cli2": "^0.20.0", "prism-react-renderer": "^2.3.0", "raw-loader": "^4.0.2", "react": "^18.0.0", @@ -5177,9 +5176,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5197,9 +5193,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5217,9 +5210,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5237,9 +5227,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5257,9 +5244,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5277,9 +5261,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5405,18 +5386,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@slorber/remark-comment": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", @@ -6040,12 +6009,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, - "node_modules/@types/katex": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", - "license": "MIT" - }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -9857,18 +9820,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -11318,12 +11269,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" - }, "node_modules/jsonfile": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", @@ -11336,31 +11281,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/katex": { - "version": "0.16.45", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", - "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -11574,9 +11494,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11598,9 +11515,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11622,9 +11536,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11646,9 +11557,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11722,25 +11630,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/linkify-it": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", - "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -11890,23 +11779,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -11917,161 +11789,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/markdownlint": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", - "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", - "license": "MIT", - "dependencies": { - "micromark": "4.0.2", - "micromark-core-commonmark": "2.0.3", - "micromark-extension-directive": "4.0.0", - "micromark-extension-gfm-autolink-literal": "2.1.0", - "micromark-extension-gfm-footnote": "2.1.0", - "micromark-extension-gfm-table": "2.1.1", - "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.2", - "string-width": "8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/DavidAnson" - } - }, - "node_modules/markdownlint-cli2": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.20.0.tgz", - "integrity": "sha512-esPk+8Qvx/f0bzI7YelUeZp+jCtFOk3KjZ7s9iBQZ6HlymSXoTtWGiIRZP05/9Oy2ehIoIjenVwndxGtxOIJYQ==", - "license": "MIT", - "dependencies": { - "globby": "15.0.0", - "js-yaml": "4.1.1", - "jsonc-parser": "3.3.1", - "markdown-it": "14.1.0", - "markdownlint": "0.40.0", - "markdownlint-cli2-formatter-default": "0.0.6", - "micromatch": "4.0.8" - }, - "bin": { - "markdownlint-cli2": "markdownlint-cli2-bin.mjs" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/DavidAnson" - } - }, - "node_modules/markdownlint-cli2-formatter-default": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", - "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/DavidAnson" - }, - "peerDependencies": { - "markdownlint-cli2": ">=0.0.4" - } - }, - "node_modules/markdownlint-cli2/node_modules/globby": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-15.0.0.tgz", - "integrity": "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==", - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.5", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdownlint-cli2/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/markdownlint-cli2/node_modules/path-type": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdownlint-cli2/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdownlint/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/markdownlint/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdownlint/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12489,12 +12206,6 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "license": "CC0-1.0" }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, "node_modules/media-chrome": { "version": "4.19.0", "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.19.0.tgz", @@ -12712,81 +12423,6 @@ ], "license": "MIT" }, - "node_modules/micromark-extension-directive": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", - "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-extension-frontmatter": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", @@ -13180,81 +12816,6 @@ ], "license": "MIT" }, - "node_modules/micromark-extension-math": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", - "dependencies": { - "@types/katex": "^0.16.0", - "devlop": "^1.0.0", - "katex": "^0.16.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-math/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-math/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", @@ -16874,15 +16435,6 @@ "node": ">=6" } }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/pupa": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", @@ -19327,12 +18879,6 @@ "node": "*" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -19388,18 +18934,6 @@ "node": ">=4" } }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/docs/package.json b/docs/package.json index 2c3cc492..fc8bf31d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,6 @@ "@iconify/react": "^6.0.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", - "markdownlint-cli2": "^0.20.0", "prism-react-renderer": "^2.3.0", "raw-loader": "^4.0.2", "react": "^18.0.0", diff --git a/pyproject.toml b/pyproject.toml index afcc780c..0c9a343a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ lint = [ "ruff==0.15.12", "astroid>=3.1,<4.0", "ty==0.0.14", + "rumdl==0.2.28", ] types = [ "types-ujson", @@ -518,6 +519,36 @@ directory = "housekeeping" name = "Housekeeping" showcontent = true +[tool.rumdl] +# Markdown linter (https://github.com/rvben/rumdl), replaces markdownlint-cli2. +# The first row mirrors the rules markdownlint had disabled; the second row disables +# rumdl-only rules and rules that false-positive on our MDX/Docusaurus docs +# (fenced blocks nested in <Tabs>, `$(base_url)` template links) that markdownlint +# never enforced. +disable = [ + "MD013", "MD014", "MD024", "MD029", "MD033", "MD034", "MD041", "MD045", "MD047", + "MD046", "MD057", "MD064", "MD069", "MD071", "MD077", +] +exclude = [ + ".venv", + "node_modules", + ".git", + "changelog/*.md", + ".agents/commands", + ".agents/skills", + ".claude/commands", + ".claude/skills", + "dev/skills", + ".specify/templates", + ".specify/extensions", +] + +[tool.rumdl.MD025] +front_matter_title = "" + +[tool.rumdl.MD059] +prohibited_texts = [] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/skills-lock.json b/skills-lock.json index a8cd89dc..ebda4832 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,53 +1,95 @@ { "version": 1, "skills": { - "commit": { + "opsmill-dev-analyzing-bugs": { + "source": "opsmill/opsmill-skills", + "sourceType": "github", + "skillPath": "opsmill-dev/skills/analyzing-bugs/SKILL.md", + "computedHash": "b1280c4b1eeae4dfd6c1b47078ba44586b23842ec27551a64e40217003fd66b2" + }, + "opsmill-dev-analyzing-dependency-bumps": { + "source": "opsmill/opsmill-skills", + "sourceType": "github", + "skillPath": "opsmill-dev/skills/analyzing-dependency-bumps/SKILL.md", + "computedHash": "911a83c2dd4e8af17d66462a58a7fa49d34fb98a8e4d2a74e41bdda548cd840b" + }, + "opsmill-dev-backporting-fixes": { + "source": "opsmill/opsmill-skills", + "sourceType": "github", + "skillPath": "opsmill-dev/skills/backporting-fixes/SKILL.md", + "computedHash": "7ed25f500a0377112525dd9d744276d9fd7951f9155cd7ad6d4b0db6d16adb40" + }, + "opsmill-dev-commit": { "source": "opsmill/opsmill-skills", "sourceType": "github", "skillPath": "opsmill-dev/skills/commit/SKILL.md", - "computedHash": "84c357ba427a6a7a9a6b1187d24ce9fd740f7dfb2127606f7f844afcf5ccacbb" + "computedHash": "b6beec6d93760af276b8ba096c8b6c26753cd7156a0c70bce8155dadd6d40bda" }, - "creating-issues": { + "opsmill-dev-creating-issues": { "source": "opsmill/opsmill-skills", "sourceType": "github", "skillPath": "opsmill-dev/skills/creating-issues/SKILL.md", - "computedHash": "32e9263438e0a5c6e33df321cd7d21590f9637ba8aad0d9fbd87aaf6d42ee4bb" + "computedHash": "1699a624c06126bf21f58714141624456faa27df45fcfc7e4fc19a0bb572d547" }, - "creating-prd": { + "opsmill-dev-creating-prd": { "source": "opsmill/opsmill-skills", "sourceType": "github", "skillPath": "opsmill-dev/skills/creating-prd/SKILL.md", - "computedHash": "df4b95b240690fd5494c4aff661c77d7db2ab226b459469d243252800584d673" + "computedHash": "9ea56b6f8483a5f4f3ba2c98943f8d9ed56b0d85b865eda0cc559f013f32bedc" }, - "grilling-ideas": { + "opsmill-dev-fixing-bugs": { + "source": "opsmill/opsmill-skills", + "sourceType": "github", + "skillPath": "opsmill-dev/skills/fixing-bugs/SKILL.md", + "computedHash": "9b08acdd7fccf8a279a837fd4a6e74bca93802970c7a4d06cfe7fea87ac5b11f" + }, + "opsmill-dev-grilling-ideas": { "source": "opsmill/opsmill-skills", "sourceType": "github", "skillPath": "opsmill-dev/skills/grilling-ideas/SKILL.md", - "computedHash": "7408a0c4ce288141715f5fb0b242632c12515e5bdab16bf9d1ad03243f1f133d" + "computedHash": "914f1fcd6f004e307c5e2ec38b011d2d48bb5468bf09a431cca92954c5f36897" }, - "monitoring-pull-requests": { + "opsmill-dev-merging-branches": { + "source": "opsmill/opsmill-skills", + "sourceType": "github", + "skillPath": "opsmill-dev/skills/merging-branches/SKILL.md", + "computedHash": "84e89da78071ab7824946c7bc8598c68f87125e4c41b5c333539d2083b16cd40" + }, + "opsmill-dev-monitoring-pull-requests": { "source": "opsmill/opsmill-skills", "sourceType": "github", "skillPath": "opsmill-dev/skills/monitoring-pull-requests/SKILL.md", - "computedHash": "b52cf3902ca8a290aa3167ff5e6b0a54068a45187357e342e5967f18fb00d854" + "computedHash": "1ba029703b207cc1ca9230e71aca999cebb27d6452cc7da33c55d32b5ab848c0" }, - "pr": { + "opsmill-dev-pr": { "source": "opsmill/opsmill-skills", "sourceType": "github", "skillPath": "opsmill-dev/skills/pr/SKILL.md", - "computedHash": "146ab1b524c0c83b1f1df8a339b9e05c91a0addcb5890e1e2bf4b1d2db861fc6" + "computedHash": "597d57432dee92d23a062cf03e8978a58b7c91404fb0427b8c752b2a0974a554" }, - "quality-gates": { + "opsmill-dev-pruning-residues": { "source": "opsmill/opsmill-skills", "sourceType": "github", - "skillPath": "opsmill-dev/skills/quality-gates/SKILL.md", - "computedHash": "2dee3043a860df4cd964b6ae705cb502be49bdf4f26f7accc7b53b388d211038" + "skillPath": "opsmill-dev/skills/pruning-residues/SKILL.md", + "computedHash": "6662d133e7c65b3da0687ac1ec57d2bf0d8422ff6415df633c038c1c8cf58523" }, - "rebase": { + "opsmill-dev-rebase": { "source": "opsmill/opsmill-skills", "sourceType": "github", "skillPath": "opsmill-dev/skills/rebase/SKILL.md", - "computedHash": "13579a9261d17154d4899ab9c5cc147c1668b9767187641a210fb83a0189b9d6" + "computedHash": "00f916b8332b9a5c8b9ce127c78d8ed3f06eac3299e5891c10348c91a0feff5f" + }, + "opsmill-dev-test-driving-bugs": { + "source": "opsmill/opsmill-skills", + "sourceType": "github", + "skillPath": "opsmill-dev/skills/test-driving-bugs/SKILL.md", + "computedHash": "1a9514502876e98d188f00b7aef80efb8621d06813c3485968755a14969284fa" + }, + "quality-gates": { + "source": "opsmill/opsmill-skills", + "sourceType": "github", + "skillPath": "opsmill-dev/skills/quality-gates/SKILL.md", + "computedHash": "2dee3043a860df4cd964b6ae705cb502be49bdf4f26f7accc7b53b388d211038" } } } diff --git a/tasks.py b/tasks.py index 048789a6..69bf7986 100644 --- a/tasks.py +++ b/tasks.py @@ -44,6 +44,11 @@ def require_tool(name: str, install_hint: str) -> None: raise Exit(f" - {name} is not installed. {install_hint}", code=1) +def _rumdl_fix(context: Context, path: Path) -> None: + """Auto-fix rumdl violations in the generated markdown under *path*.""" + context.run(f'rumdl check --fix "{path}"', pty=True) + + @task(name="docs-generate") def docs_generate(context: Context) -> None: """Generate all documentation (infrahubctl CLI + Python SDK).""" @@ -266,8 +271,7 @@ def _generate_sdk_api_docs(context: Context) -> None: target_path = output_dir / reduce(operator.truediv, (Path(part) for part in file_key.split("-"))) MDXDocPage(page=page, output_path=target_path).to_mdx() - with context.cd(DOCUMENTATION_DIRECTORY): - context.run(f"npx --no-install markdownlint-cli2 {output_dir}/ --fix --config .markdownlint.yaml", pty=True) + _rumdl_fix(context, output_dir) @task @@ -316,11 +320,11 @@ def lint_ruff(context: Context) -> None: @task -def lint_markdownlint(context: Context) -> None: - """Run markdownlint to check all markdown files.""" - print(" - Check documentation with markdownlint-cli2") - exec_cmd = "npx --no-install markdownlint-cli2 **/*.{md,mdx} !node_modules/** --config .markdownlint.yaml" - with context.cd(DOCUMENTATION_DIRECTORY): +def lint_markdown(context: Context) -> None: + """Run the markdown linter to check all markdown files.""" + print(" - Check documentation with rumdl") + exec_cmd = "rumdl check ." + with context.cd(MAIN_DIRECTORY_PATH): context.run(exec_cmd) @@ -346,7 +350,7 @@ def lint_code(context: Context) -> None: @task def lint_docs(context: Context) -> None: """Run all documentation linters.""" - lint_markdownlint(context) + lint_markdown(context) lint_vale(context) @@ -407,6 +411,7 @@ def generate_python_sdk(context: Context) -> None: _generate_infrahub_sdk_configuration_documentation() _generate_infrahub_sdk_template_documentation() _generate_infrahub_sdk_compatibility_documentation() + _rumdl_fix(context, DOCUMENTATION_DIRECTORY / "docs" / "python-sdk" / "reference") _generate_sdk_api_docs(context) diff --git a/uv.lock b/uv.lock index 60d05ab5..4cf1c1ca 100644 --- a/uv.lock +++ b/uv.lock @@ -734,6 +734,7 @@ dev = [ { name = "pytest-xdist" }, { name = "requests" }, { name = "ruff" }, + { name = "rumdl" }, { name = "towncrier" }, { name = "ty" }, { name = "types-python-slugify" }, @@ -745,6 +746,7 @@ lint = [ { name = "astroid" }, { name = "mypy" }, { name = "ruff" }, + { name = "rumdl" }, { name = "ty" }, { name = "yamllint" }, ] @@ -814,6 +816,7 @@ dev = [ { name = "pytest-xdist", specifier = ">=3.3.1" }, { name = "requests", specifier = ">=2.33.0" }, { name = "ruff", specifier = "==0.15.12" }, + { name = "rumdl", specifier = "==0.2.28" }, { name = "towncrier", specifier = ">=24.8.0" }, { name = "ty", specifier = "==0.0.14" }, { name = "types-python-slugify", specifier = ">=8.0.0.3" }, @@ -825,6 +828,7 @@ lint = [ { name = "astroid", specifier = ">=3.1,<4.0" }, { name = "mypy", specifier = "==1.11.2" }, { name = "ruff", specifier = "==0.15.12" }, + { name = "rumdl", specifier = "==0.2.28" }, { name = "ty", specifier = "==0.0.14" }, { name = "yamllint" }, ] @@ -886,17 +890,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } wheels = [ @@ -913,17 +917,17 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.11'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, - { name = "jedi", marker = "python_full_version >= '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, - { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "stack-data", marker = "python_full_version >= '3.11'" }, - { name = "traitlets", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/34/29b18c62e39ee2f7a6a3bba7efd952729d8aadd45ca17efc34453b717665/ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731", size = 4396932, upload-time = "2025-09-29T10:55:53.948Z" } wheels = [ @@ -935,7 +939,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1468,8 +1472,8 @@ name = "pendulum" version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil", marker = "python_full_version < '3.13'" }, - { name = "tzdata", marker = "python_full_version < '3.13'" }, + { name = "python-dateutil" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/7c/009c12b86c7cc6c403aec80f8a4308598dfc5995e5c523a5491faaa3952e/pendulum-3.1.0.tar.gz", hash = "sha256:66f96303560f41d097bee7d2dc98ffca716fbb3a832c4b3062034c2d45865015", size = 85930, upload-time = "2025-04-19T14:30:01.675Z" } wheels = [ @@ -2502,6 +2506,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] +[[package]] +name = "rumdl" +version = "0.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/40/da219dd6f19eaeb4e620c0b2c942adc0d743aed6186e37d35ba1ab4d0a6a/rumdl-0.2.28.tar.gz", hash = "sha256:ff7e4b9ffaa617495a9fed775323e557d7719208ba2596b778864c0d31c132e9", size = 2834931, upload-time = "2026-07-03T09:21:25.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/1a/13ad9ef84cf0407b5685d6c297d44df45d3fa68a7289291451d960df5401/rumdl-0.2.28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1c5580ef900acee7b112655c569355af5914e7e26248612c2a0df7f877cf1928", size = 6032238, upload-time = "2026-07-03T09:21:13.156Z" }, + { url = "https://files.pythonhosted.org/packages/60/97/f7cfffd9515a5bdf2273a92662183ab172c7fd5149c4296b089ce7f3b7dd/rumdl-0.2.28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9e38e5e48db8b38935cfd2ba986aa8deb7bdf784de4228629f581849e11a8b76", size = 5698394, upload-time = "2026-07-03T09:21:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/18111e2471c767d69b7874a643b7dbec5978fd03b65394e122a9990c8015/rumdl-0.2.28-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8299462c03d498b90014fb03983d9aa8cdd2d2a5ff64f8a27231e8ffe00cdbb9", size = 5836882, upload-time = "2026-07-03T09:21:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/65/e5/0cd0fd9c530c53192e4fd12c33717b6c4f1c7f9ac28f2443869e101719b3/rumdl-0.2.28-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2a0f14e56ce50583d600404ff80aac7c98e11862103dcbfcb5aee5225184b702", size = 6205179, upload-time = "2026-07-03T09:21:18.986Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d5/06a52f22d768296cf3deef12dde93c1b22a9177a3adff7944f7602b47c22/rumdl-0.2.28-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d518eaec6972d1651b71e48d7a2d9dd5e2ec73acbd29e3e51343d0cd6c65b10", size = 5828991, upload-time = "2026-07-03T09:21:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ff/d35ce3e759b7696fe07d3dbdc05de8494f5f0ad202d9e44595de8024df01/rumdl-0.2.28-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b8d2249d7951e46b455e8812f30b7ec453c24c37afa34ff5def541756113d652", size = 6191050, upload-time = "2026-07-03T09:21:22.331Z" }, + { url = "https://files.pythonhosted.org/packages/94/6d/2d233563f17e28da273200fd9532d4ed706294ae4287e0a52b424eef71a2/rumdl-0.2.28-py3-none-win_amd64.whl", hash = "sha256:e96ba5e683e2b1dc39037879770930d28d0cbc5e3d13fdd872abb327cd5a68f5", size = 6126533, upload-time = "2026-07-03T09:21:23.942Z" }, +] + [[package]] name = "shellingham" version = "1.5.4"