Skip to content

feat(dependabot-policy): add org Dependabot policy analyzer component - #153

Merged
kyle-sexton merged 6 commits into
mainfrom
feat/dependabot-policy-component
Jul 16, 2026
Merged

feat(dependabot-policy): add org Dependabot policy analyzer component#153
kyle-sexton merged 6 commits into
mainfrom
feat/dependabot-policy-component

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

Adds dependabot-policy, a synced analyzer component that enforces the organization Dependabot policy on every .github/dependabot.yml updates entry: the weekly schedule, a cooldown soak of at least 7 days, a groups block so related bumps batch into one pull request, and an open-pull-requests-limit of at most 5. Part of the volume-control workstream (melodic-software/github-iac#78).

A dependabot.yml varies per repository (different ecosystems and directories) and cannot be byte-synced, so this mirrors runner-policy: a byte-identical analyzer, policy.json, and schema materialize into consumers while each repository owns its config and a locally-owned exception file. The canonical values standards already dogfoods are the target each repository converges to.

Shared-component admission evidence

Per docs/component-lifecycle.md:

  • Live consumerstandards itself. The new dependabot-policy CI job runs the analyzer (node components/dependabot-policy/dependabot-policy.mjs --root .) against this repository's own dependabot.yml and is aggregated into the required ci-status gate.
  • Owner and outcome — Owner: standards maintainers. Problem: bot-pull-request bursts were a compound cause of the 2026-07-16 job backlog (github-iac#78). Acceptance: every updates entry batches, soaks, and caps its pull requests, or records a reasoned exception; the gate blocks drift. Rollback: remove the component entry and the CI job.
  • Delivery boundary — Exact materialization of a byte-identical analyzer + policy.json through this repository; the dependabot.yml it checks stays authoritative per consumer. The config cannot be byte-synced (per-repo ecosystems/directories), Dependabot has no extends/shared-config mechanism, and there is no organization control-plane setting for grouping/cooldown/limit — the analyzer is the only boundary that keeps one source of truth for the policy values.
  • Alternatives and overlap — A prose convention was rejected: the convention tier is adopt-by-copy, outside the sync loop, so it cannot satisfy the issue's "roll out via sync" and gives no gate. Folding into runner-policy was rejected for cohesion; there is no finding overlap (runner-policy never inspects dependabot.yml). Complements the demand-shaping work in ci-workflows#122 and extends the W4 Dependabot batching decision (github-iac#82) with an enforcement gate rather than duplicating it.
  • Operational fit — Static file parse; no network, credentials, or privileged access. Runs in well under a second. Deterministic pass/fail, fail-closed on parse errors and unknown config. OS-independent on Node >= 24. False-positive surface is bounded to four entry rules plus two file-level rules, with an exception seam for the two documented deviation classes.
  • Upstream health — Runtime dependencies ajv@8.20.0 and yaml@2.9.0, the exact pins already vetted for runner-policy (byte-identical lockfile tree, 6 packages). No other dependencies.
  • Legal and securityajv and yaml are MIT-licensed. The analyzer is read-only with no data or credential access. Boundaries, fail-closed behavior, and review triggers are in components/dependabot-policy/THREAT-MODEL.md.
  • Update path — The new npm dependency root is registered in .github/dependabot.yml (/components/dependabot-policy: weekly, cooldown 7, limit 5, grouped), itself conforming to the policy this component enforces.
  • Verification — 19 behavioral cases (dependabot-policy.test.mjs) covering conformant pass, each rule's violation, the directories plural form, exception waivers and inventory drift, fail-closed schema, and malformed YAML/JSON. CI runs the same --root . entrypoint consumers use.

Enforcement rollout. Blocking from the start: the sole live consumer (standards) already conforms with no exceptions, so no observation period is needed. Downstream consumers move to blocking in their own integration PRs after a clean run.

Dependabot drift (org-wide audit, informational)

Audited .github/dependabot.yml across all 12 active repos (raw gh api). The org is already ~90% converged; standards is the reference. Remaining drift the standard will catch:

  • No dependabot.yml at all: knowledge-corpus, songwriting (neither has required CI or PR workflows, so both are low priority and out of the enforced set).
  • medley — broad config drift: nuget runs daily with no cooldown and no groups; docker, npm (root), npm /tests/e2e, pip, and both uv roots lack cooldown and groups; no explicit pull-request limit; commit prefix chore vs the org's build. Only its github-actions entry conforms.
  • ci-runner-canary: the github-actions entry has no groups block.
  • Explicitness-only (functionally fine): open-pull-requests-limit is omitted in provisioning, dotfiles, medley, github-iac; GitHub's default is already 5, so behavior matches — the analyzer accepts an omitted limit and only flags a limit above 5.
  • Legitimate documented exceptions (not drift): claude-code-plugins npm-root daily + no cooldown (tracks Claude Code releases) and github-iac dotnet-sdk daily + no cooldown (SDK freshness) map to the tracks-upstream-release exception; single-tool pip/uv entries map to single-tool-ecosystem. These are handled by the locally-owned exception seam, not flagged.

Per-consumer rollout (follow-up integration work)

The 9 requires-ci repositories (each has an active required_status_checks ruleset rule). standards is wired in this PR; each of the other eight needs a separate integration PR: manifest enrollment (materializes the analyzer + policy), a locally-owned .github/dependabot-policy.json for its documented exceptions, the CI job, and a dependabot entry for the analyzer lockfile. medley and ci-runner-canary also need their dependabot.yml brought into conformance in that same change.

  • melodic-software/standards — dogfooded in this PR
  • melodic-software/.github
  • melodic-software/ci-runner
  • melodic-software/ci-workflows
  • melodic-software/claude-code-plugins — exception: npm-root tracks-upstream-release
  • melodic-software/dotfiles
  • melodic-software/github-iac — exception: dotnet-sdk tracks-upstream-release
  • melodic-software/medley — config remediation + likely single-tool-ecosystem exceptions
  • melodic-software/provisioning

The component is registered in distribution/sync-manifest.yml but enrolled in no target yet, so nothing materializes until each integration PR adds it to that target's managed set. knowledge-corpus, songwriting, and ci-runner-canary have no required CI (the last has no PR workflows); a dependabot.yml for the first two is optional low-priority follow-up.

Note for the reviewer / merge order

This PR and #152 (concurrency-policy) were authored in parallel off origin/main and both touch the same shared integration lines — the ci.yml biome paths, the ci-status needs list and RESULTS aggregation, package.json scripts, .github/dependabot.yml, and the distribution/sync-manifest.yml component block. Whichever merges second will need a trivial rebase to add its job/entry alongside the other's rather than replacing it. The two components are otherwise independent.

Test plan

  • npm run test:dependabot-policy — 19/19 passing
  • npm run lint:dependabot-policy — passes against this repository (no exceptions needed)
  • npm run lint:runner-policy — still passes with the new CI job present
  • biome, markdownlint, and sync-manifest.sh validate — clean

Closes #150.

@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 13s —— View job


PR Review: feat(dependabot-policy): add org Dependabot policy analyzer component

Tasks

  • Gather context and understand the request
  • Read the PR diff and changed files
  • Review the analyzer implementation (dependabot-policy.mjs)
  • Review test coverage (dependabot-policy.test.mjs)
  • Review schema files and policy configuration
  • Review CI integration (.github/workflows/ci.yml, dependabot.yml)
  • Review distribution and sync manifest
  • Review documentation (README, THREAT-MODEL)
  • Synthesize findings and post review

Summary

Solid implementation that mirrors runner-policy closely in both structure and quality. The fail-closed parsing, schema validation, exception-drift detection, and test coverage are all well-executed. I have four items below — one correctness nit and three minor observations.


Findings

1. entryKey for directories entries is order-dependent and comma-unsafe

dependabot-policy.mjs line 146

if (Array.isArray(entry.directories)) {
  return `${prefix}:${entry.directories.join(",")}`;
}

The key for a directories entry is the comma-joined list. If a consumer ever reorders those paths in their dependabot.yml, the key changes and any existing exception for that entry immediately becomes exception-inventory-drift. This is a quiet footgun when the drift check fires unexpectedly after a cosmetic YAML reorder.

Comma-in-path is a separate (extremely unlikely in practice) risk. Neither is a blocker for this PR since no exceptions currently reference directories entries, but the exception-inventory-drift check is designed to catch rot, not introduce it.

Consider documenting in the README exceptions section that exception keys for directories entries must exactly reflect the join order used in dependabot.yml.


2. file field in findings is hardcoded regardless of the dependabotPath parameter

dependabot-policy.mjs lines 254 and 265

// "missing" finding
finding("dependabot-config-missing", DEFAULT_DEPENDABOT_PATH, ...)

// all entry findings
const file = DEFAULT_DEPENDABOT_PATH;

auditRepository accepts dependabotPath but findings always report .github/dependabot.yml. In the current CLI the parameter isn't exposed, so there's no user-visible issue. The exported function API is technically inconsistent though — a caller passing a custom dependabotPath would get findings pointing at the default path, not the file actually checked. Since runner-policy uses the same pattern this is consistent, but worth a comment on file if the function signature stays public. Fix this →


3. New CI job is missing a name: field (minor inconsistency)

ci.yml lines 229–258

The runner-policy job has name: Runner policy which controls the display label in the GitHub UI. The new dependabot-policy job omits it, so the job displays as dependabot-policy rather than Dependabot policy. Not a correctness issue — just inconsistent with the sibling job and slightly less readable in the checks list.


4. dependabot.yml comment gives process guidance that will silently rot

.github/dependabot.yml lines 68–72

# The dependabot policy analyzer is distributed with its own runtime
# lockfile, so it is a separate npm dependency root. Keep this entry in sync
# with that component; every downstream materialization must add the
# equivalent destination path.

This is integration guidance aimed at contributors doing downstream materialization, embedded as a comment on a single entry. The risk is that a future editor reads this out of context and silently fails to follow it. The sync-manifest.yml materialization is the authoritative gate; this comment adds nothing there. The README's "Enforcement gate" section already covers consumer setup. Consider dropping the comment or reducing it to a single-line reference to the README.


What's solid

  • Fail-closed everywhere: malformed YAML, duplicate JSON members, schema violations, missing schema files — all crash loudly before any findings are emitted.
  • Exception-drift detection: both the "entry doesn't exist" and "waiver already satisfied" arms are covered, with clean test cases for each.
  • pr-limit-too-high not waivable: the WAIVE_BY_RULE omission is intentional, the schema confirms it, and the "unknown waiver fails closed" test covers the path. Clean.
  • Test coverage: 19 cases covering every rule, both key forms (directory/directories), omitted-vs-exceeded limit, both exception reasons, all three exception-inventory-drift triggers, and the four fail-closed parse scenarios.
  • Schema discipline: additionalProperties: false on both schemas, uniqueItems: true on waives, required on all meaningful fields.
  • CI wiring: select-runner, !cancelled() guard, scoped contents: read permission, persist-credentials: false, and correct aggregation into ci-status — all correct.
  • Sync manifest: only the distributable files (no test file, no README) are materialized. Correct.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e22ebab6a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/dependabot-policy/dependabot-policy.mjs Outdated
Comment thread components/dependabot-policy/dependabot-policy.mjs
kyle-sexton added a commit that referenced this pull request Jul 16, 2026
Two fail-open gaps found in review:

- A conformant `cooldown.default-days` no longer passes when the soak is
  defeated. A match-all `cooldown.exclude` ("*") skips the soak for every
  dependency, and a `cooldown.include` list inverts it to opt-in; both are now
  reported as cooldown-soak-bypassed and are waivable under the cooldown
  category. A narrow first-party exclude (melodic-software/*) still passes.
- A `version: 2` config whose `updates` is absent, misspelled, non-array, or
  empty is now reported as updates-missing rather than silently auditing an
  empty policy.

Adds regression tests; suite is 24/24. Addresses the Codex review findings on
#153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 22s —— View job


Review of fix commit b799c4d

Tasks

  • Gather context and understand the request
  • Read the diff for the new commit (fix: reject soak bypass and empty updates)
  • Verify the two Codex findings are addressed
  • Check for any regressions or new issues
  • Post final review

Both Codex findings from the initial review are correctly addressed. Here's the verdict on each.


Codex P1 — Cooldown soak bypass ✅ Fixed

dependabot-policy.mjs lines 182–197

The implementation correctly identifies the two bypass patterns:

  • exclude: ["*"] — a match-all wildcard that removes the soak from every dependency. Detected via exclude.includes("*"), so exclude: ["melodic-software/*", "*"] is also caught. ✅
  • Any non-empty include list — inverts the soak to opt-in, leaving most dependencies un-soaked. ✅

The asymmetric treatment of exclude vs include is intentional and correct: a narrow exclude (e.g. "melodic-software/*") still soaks everything else and passes, while any include restricts to a subset and fails. The code comment explains the invariant clearly.

The new cooldown-soak-bypassed rule maps to the existing "cooldown" waive category (same as cooldown-below-minimum), so a cooldown waiver suppresses both without any schema change. The schema's "cooldown" enum already covers it.

Four new test cases cover all the critical paths: match-all exclude fires, include fires, narrow exclude passes, cooldown waiver suppresses the new rule.


Codex P2 — Empty/absent updates fail-closed ✅ Fixed

dependabot-policy.mjs lines 278–287

A version: 2 config whose updates is absent, non-array, or empty now emits updates-missing rather than silently auditing an empty list. The existing Array.isArray(dependabot.updates) ? dependabot.updates : [] guard collapses all three cases to an empty array, so a single length === 0 check covers them all.

updates-missing is correctly excluded from WAIVE_BY_RULE — you can't waive away having no update entries; you'd need to actually add entries or remove the config entirely.

No early return after the finding — the exception drift check still runs, which correctly surfaces drift if someone configured exceptions for a repo that now has no entries.

Test covers the two most realistic cases: absent key and wrong type (object instead of array). An explicit updates: [] case would be a minor completeness add, but it's clearly handled by the same code path and not a gap worth blocking on.


No regressions

The fix is scoped to the two new rules and two test additions. The 19 original cases are unchanged; the suite runs 24/24. Nothing in the exception-drift or waiver-consumed logic is affected.


Standing observations from the initial review

The four findings from the first review pass (e22ebab) are unchanged — none are blocking. For the record:

  • entryKey ordering — exception keys for directories entries are order-sensitive; document this in the README exceptions section. Fix this →
  • file hardcoded to DEFAULT_DEPENDABOT_PATHauditRepository's dependabotPath param isn't reflected in findings. Not user-visible today, but a comment on the export would clarify intent.
  • CI job missing name: — the new job shows as dependabot-policy in the checks list rather than Dependabot policy like the sibling runner-policy job. Fix this →
  • dependabot.yml comment — consider trimming the multi-line materialization comment to a single-line README reference.

The fix commit is clean. The two Codex findings are closed.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Addressed both Codex findings in b799c4d:

  • P1 (soak bypass): a conformant default-days now also checks that the soak actually applies. A match-all cooldown.exclude: ["*"] and a cooldown.include list are reported as cooldown-soak-bypassed (waivable under the cooldown category). A narrow first-party exclude like melodic-software/* still passes, so standards' own config is unaffected.
  • P2 (empty updates): a version: 2 config whose updates is absent, misspelled, non-array, or empty is now updates-missing instead of silently passing.

Regression tests added; suite is 24/24.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b799c4da75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/dependabot-policy/dependabot-policy.mjs Outdated
Comment thread components/dependabot-policy/dependabot-policy.schema.json
kyle-sexton added a commit that referenced this pull request Jul 16, 2026
Two more fail-open gaps found in review:

- A scalar or null item in `updates` was silently skipped; it is now reported
  as malformed-update-entry, so a broken entry cannot pass unaudited.
- The exception schema now ties `waives` to `reason`: tracks-upstream-release
  may waive only schedule and cooldown, and single-tool-ecosystem may waive
  only groups. A waiver outside its reason's scope fails closed, so a
  groups-only justification can no longer license skipping the cooldown soak.

Adds regression tests; suite is 26/26. Addresses the Codex review findings on
#153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 17s —— View job


Tasks

  • Gather context and understand the request
  • Read the diff for the new commit (8f4bf45 — fix: flag malformed entries; bind waivers to reason)
  • Verify the two Codex findings are addressed
  • Check for any regressions or new issues
  • Post final review

Review of fix commit 8f4bf45

Both Codex findings from the second review pass are correctly addressed. Here's the verdict on each.


Codex P2 — Malformed update entries flagged ✅ Fixed

dependabot-policy.mjs lines 292–303

Non-mapping entries in updates (scalar, null, or array) now emit malformed-update-entry with a zero-indexed location (updates[0], updates[1], …) rather than being silently skipped. The rule is correctly absent from WAIVE_BY_RULE — you can't waive a malformed entry. The continue after pushing the finding is correct; the exception-drift sweep still runs afterward, same as the updates-missing path.

The existing guard condition (entry === null || typeof entry !== "object" || Array.isArray(entry)) now actively produces a finding instead of a silent skip — no new logic needed, just the push.

Test covers scalar string and null entry cases. Array-as-entry falls under the same guard and is handled, though not explicitly tested — not a gap worth blocking on given how implausible the YAML form is.


Codex P2 — Exception waivers bound to reason ✅ Fixed

dependabot-policy.schema.json lines 39–62

The allOf with if/then correctly restricts waives values based on reason:

  • tracks-upstream-release → only ["schedule", "cooldown"] permitted
  • single-tool-ecosystem → only ["groups"] permitted

JSON Schema 2020-12 semantics are correct here: both the base properties.waives.items constraint and the then.properties.waives.items constraint must hold simultaneously, so the narrower set prevails at validation time. A cooldown waiver under single-tool-ecosystem fails closed; a groups waiver under tracks-upstream-release fails closed. The if condition includes required: ["reason"] — this is redundant (the parent already requires reason) but idiomatic and harmless.

Tests verify both fail-closed cases: single-tool-ecosystem + waives: ["cooldown"] and tracks-upstream-release + waives: ["groups"] both reject with ConfigurationError. ✅


No regressions

The fix touches only the malformed-entry branch (now emits instead of silently continuing) and the schema. The 24 prior cases are unchanged; the suite runs 26/26.


Standing observations from the initial review

Four non-blocking observations from the first pass (e22ebab) remain open. None block merge.

  • entryKey ordering for directories entries — exception keys for multi-root entries are order-sensitive (comma-joined from the YAML list); a cosmetic reorder causes exception-inventory-drift. The README's "What it checks" preamble now mentions that directories is joined, but stops short of warning that the join order must match the YAML order. Worth one sentence in the Exceptions section. Fix this →
  • file hardcoded to DEFAULT_DEPENDABOT_PATHauditRepository's dependabotPath param is not reflected in findings. Not user-visible today.
  • CI job missing name: — displays as dependabot-policy in the checks list rather than Dependabot policy. Fix this →
  • dependabot.yml comment verbosity — the four-line materialization comment on the dependabot-policy entry could be reduced to a single-line README reference.

Both Codex findings are closed. The fix commit is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f4bf45e3b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/dependabot-policy/dependabot-policy.mjs Outdated
Comment thread components/dependabot-policy/dependabot-policy.mjs
kyle-sexton and others added 3 commits July 16, 2026 15:11
Adds a synced analyzer component that enforces the organization Dependabot
policy on every `.github/dependabot.yml` updates entry: the weekly schedule,
a cooldown soak of at least 7 days, a groups block so related bumps batch into
one pull request, and an open-pull-requests limit of at most 5.

Part of the volume-control workstream (melodic-software/github-iac#78).
Bot-pull-request bursts were a compound cause of the 2026-07-16 job backlog. A
default 3-day cooldown now applies with no configuration (the 2026-07-14
Dependabot change); the standard tightens it to 7 and additionally requires
grouping and a pull-request cap.

A dependabot.yml varies per repository (different ecosystems and directories),
so it cannot be byte-synced. This mirrors runner-policy: a byte-identical
analyzer, policy, and schema materialize into consumers while each repository
owns its config and a locally-owned exception file.

- Read-only Node analyzer with canonical values in policy.json, strict
  Draft 2020-12 policy and exception schemas, a locked ajv/yaml runtime, and a
  fail-closed ecosystem-keyed exception seam (tracks-upstream-release,
  single-tool-ecosystem) with required reason and justification and per-waiver
  consumption checks.
- Behavioral test suite (19 cases) and threat model.
- Registered in distribution/sync-manifest.yml (unenrolled; per-consumer
  rollout is separate integration work).
- Dogfooded by a new dependabot-policy CI job that audits this repository's own
  dependabot.yml, which conforms with no exceptions.

- [x] npm run test:dependabot-policy — 19/19 passing
- [x] npm run lint:dependabot-policy — passes against this repository
- [x] npm run lint:runner-policy — still passes with the new CI job present
- [x] biome, markdownlint, and sync-manifest validate — clean

Refs #150.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
Two fail-open gaps found in review:

- A conformant `cooldown.default-days` no longer passes when the soak is
  defeated. A match-all `cooldown.exclude` ("*") skips the soak for every
  dependency, and a `cooldown.include` list inverts it to opt-in; both are now
  reported as cooldown-soak-bypassed and are waivable under the cooldown
  category. A narrow first-party exclude (melodic-software/*) still passes.
- A `version: 2` config whose `updates` is absent, misspelled, non-array, or
  empty is now reported as updates-missing rather than silently auditing an
  empty policy.

Adds regression tests; suite is 24/24. Addresses the Codex review findings on
#153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
Two more fail-open gaps found in review:

- A scalar or null item in `updates` was silently skipped; it is now reported
  as malformed-update-entry, so a broken entry cannot pass unaudited.
- The exception schema now ties `waives` to `reason`: tracks-upstream-release
  may waive only schedule and cooldown, and single-tool-ecosystem may waive
  only groups. A waiver outside its reason's scope fails closed, so a
  groups-only justification can no longer license skipping the cooldown soak.

Adds regression tests; suite is 26/26. Addresses the Codex review findings on
#153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
@kyle-sexton
kyle-sexton force-pushed the feat/dependabot-policy-component branch from 8f4bf45 to 9f5815c Compare July 16, 2026 19:16
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 51s —— View job


Tasks

  • Gather context and understand the request
  • Read the current analyzer code (dependabot-policy.mjs)
  • Analyze new Codex findings (semver cooldown overrides, missing identity keys)
  • Post review

Review of HEAD commit 9f5815c

Two new Codex findings on the latest commit are both confirmed. Neither blocks the component's core mission (all in-scope repos use real ecosystem/directory entries and none currently use semver-specific cooldown overrides), but both are genuine enforcement gaps worth closing before rollout.


Codex P1 — Semver cooldown overrides bypass the soak ✅ Confirmed

dependabot-policy.mjs lines 167–198

The analyzer validates cooldown.default-days >= 7 and catches the exclude: ["*"] / include bypass patterns introduced in b799c4d. What it doesn't check: semver-patch-days, semver-minor-days, and semver-major-days. Per the Dependabot cooldown reference, those keys take precedence over default-days when set — so an entry with default-days: 7 and semver-patch-days: 0 passes the check while patch updates have no soak at all.

The fix sits in the else branch at line 178, alongside the existing bypass checks:

const semverKeys = ["semver-patch-days", "semver-minor-days", "semver-major-days"];
const cooldown = entry.cooldown ?? {};
const lowSemverOverride = semverKeys.find(
  (k) => typeof cooldown[k] === "number" && cooldown[k] < policy.cooldownMinimumDays,
);
if (lowSemverOverride) {
  findings.push(
    finding(
      "cooldown-soak-bypassed",
      file,
      key,
      `cooldown.${lowSemverOverride} overrides default-days for that semver tier and is below the ${policy.cooldownMinimumDays}-day minimum`,
    ),
  );
}

Reusing cooldown-soak-bypassed (already waivable under cooldown) keeps the schema unchanged. A test case would be: default-days: 7, semver-patch-days: 0cooldown-soak-bypassed. Fix this →


Codex P2 — Mapping entries missing identity keys pass unaudited ✅ Confirmed

dependabot-policy.mjs line 293

The existing malformed-entry guard catches non-mapping entries (scalar, null, array). A mapping that's missing package-ecosystem or both directory/directories is not caught — it reaches entryKey(), gets a garbage key like ?:? or npm:?, and if the policy-control fields happen to be conformant, the audit passes. Dependabot itself would reject such an entry as invalid (missing required keys), so the gate can read "passed" while the config is non-functional.

The fix adds an identity check immediately after the non-mapping guard at line 303:

const hasEcosystem =
  typeof entry["package-ecosystem"] === "string" && entry["package-ecosystem"].length > 0;
const hasLocation =
  typeof entry.directory === "string" ||
  (Array.isArray(entry.directories) && entry.directories.length > 0);
if (!hasEcosystem || !hasLocation) {
  findings.push(
    finding(
      "malformed-update-entry",
      file,
      `updates[${index}]`,
      "each updates entry must be a mapping with a package-ecosystem and a directory",
    ),
  );
  continue;
}

The error message text is already what the existing malformed-update-entry message says — the check just wasn't enforcing it for mapping entries. Fix this →


No regressions in the current commit

The 9f5815c changes (malformed-entry emission + reason-bound waivers) are scoped and clean. 26 existing test cases unaffected.


Standing observations from the initial review (non-blocking, unchanged)

  • entryKey ordering — exception keys for directories entries are order-sensitive; a cosmetic YAML reorder triggers exception-inventory-drift. Worth a sentence in the README Exceptions section. Fix this →
  • file hardcoded to DEFAULT_DEPENDABOT_PATHauditRepository's dependabotPath param isn't reflected in findings. Not user-visible today.
  • CI job missing name:runner-policy has name: Runner policy (line 193 of ci.yml); the new dependabot-policy job (line 256) omits it. Fix this →
  • dependabot.yml comment verbosity — the four-line materialization comment on the dependabot-policy entry could be reduced to a single-line README reference.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f5815cf95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/dependabot-policy/dependabot-policy.mjs Outdated
Comment thread components/dependabot-policy/dependabot-policy.mjs Outdated
Two more fail-open gaps found in review:

- A semver-specific cooldown override (semver-patch-days: 0, say) takes
  precedence over default-days for that update type, so a short override
  reintroduced the burst the soak prevents. Every configured cooldown
  day-count (default-days and each semver-*-days) now must meet the floor.
- An updates entry that is a mapping but omits package-ecosystem or
  directory/directories produced a bogus key and passed unaudited; it is now
  reported as incomplete-update-entry and skipped.

Adds regression tests; suite is 28/28. Addresses the Codex re-review findings
on #153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 55s —— View job


Tasks

  • Gather context and understand the request
  • Read commit 44297e5 changes (validate semver cooldowns and entry identity)
  • Verify the two Codex P1/P2 findings from the previous review are addressed
  • Review the two new Codex findings from commit 9f5815c (zero PR limits, groups applies-to)
  • Post final review

Review of fix commit 44297e5

Both Codex findings from the previous review pass are correctly addressed. Here's the verdict on each, followed by the two new Codex findings that arrived after that commit was cut.


Codex P1 — Semver cooldown overrides ✅ Fixed

dependabot-policy.mjs lines 172–189

The implementation unifies all four cooldown day-keys into one belowFloor filter. The asymmetric treatment is correct and intentional:

  • default-days — mandatory: flagged if absent or below the floor.
  • semver-*-days — optional: flagged only when explicitly set below the floor.

So default-days: 7, semver-patch-days: 0 produces cooldown-below-minimum with message cooldown default-days ... semver-patch-days ... must each be an integer >= 7 — but wait, default-days: 7 is not below the floor, so only semver-patch-days appears in belowFloor. The test confirms: the test entry replaces default-days: 7 with default-days: 7 / semver-patch-days: 0 and expects exactly cooldown-below-minimum. ✅

One subtlety: the bypass-check else branch (lines 189–209) only runs when belowFloor.length === 0. If a semver override is flagged, the bypass check is skipped. That's fine — a value-below-floor is the stricter finding; there's no need to also check exclude/include when the day count itself already fails.


Codex P2 — Missing identity keys ✅ Fixed

dependabot-policy.mjs lines 316–328

Using a separate rule name (incomplete-update-entry vs malformed-update-entry) is the right call: the entry is a mapping, just an incomplete one. The check correctly gates on package-ecosystem being a non-empty string and at least one of directory (string) or directories (array) being present. Both are rightly absent from WAIVE_BY_RULE.

A minor edge case: directories: [] passes Array.isArray(entry.directories) and would clear the identity check. The resulting entryKey would be "npm:" (empty joined string). In practice this is implausible YAML and Dependabot itself would reject it — not worth blocking on.

Test covers the two realistic cases: entry missing package-ecosystem, entry missing both directory and directories. ✅


Open findings from the 9f5815c Codex pass

These two findings appeared in the Codex review comment after that commit was already cut. Neither is addressed in 44297e5.


New P2 — open-pull-requests-limit: 0 passes undetected

dependabot-policy.mjs line 230

if (typeof limit === "number" && limit > policy.maxOpenPullRequests) {

The guard rejects values above the maximum (> 5) but silently accepts 0. Per the Dependabot reference, limit: 0 disables version updates for that ecosystem entirely — equivalent to removing the entry. A repository could pass the gate while Dependabot opens zero PRs for that package ecosystem.

The fix is a single additional condition:

if (typeof limit === "number" && (limit === 0 || limit > policy.maxOpenPullRequests)) {

Or more readably, require the limit to be in range [1, maxOpenPullRequests] when present. The PR description currently says the analyzer "only flags a limit above 5" — that prose would need updating if the check is tightened. Fix this →


New P2 — Groups with applies-to: "security-updates" only are not caught

dependabot-policy.mjs lines 213–217

const hasGroups =
  groups !== null &&
  typeof groups === "object" &&
  !Array.isArray(groups) &&
  Object.keys(groups).length > 0;

hasGroups is satisfied by any non-empty groups object, including one whose every group sets applies-to: "security-updates". Per the docs, applies-to defaults to version-updates when omitted, so the only failure scenario is someone explicitly annotating all groups with applies-to: "security-updates" — contrived, but it would make a group-present config silently skip the batching the policy is trying to enforce.

In practice no active repo does this (the org audit showed conformant or absent groups, never security-only), and the policy document doesn't define "version-updates group" as a distinct requirement. The risk is low but the gap is real if applies-to is ever used.

If you do address it, the fix is a check that at least one group key lacks applies-to or has applies-to: "version-updates". Fix this →


Standing observations (non-blocking, carried from prior passes)

These four remain open and none block merge:

  • entryKey ordering — exception keys for directories entries are order-sensitive; a cosmetic YAML reorder triggers exception-inventory-drift. Worth one sentence in the README Exceptions section. Fix this →
  • file hardcoded to DEFAULT_DEPENDABOT_PATHauditRepository's dependabotPath param is not reflected in findings. Not user-visible today.
  • CI job missing name: — displays as dependabot-policy in the checks list rather than Dependabot policy. Fix this →
  • dependabot.yml comment verbosity — the four-line materialization comment on the dependabot-policy entry could be reduced to a single-line README reference.

Both P1 and P2 from the prior review are closed. The two new Codex findings (limit: 0 and applies-to: "security-updates") are the remaining open items.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44297e5bdb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/dependabot-policy/dependabot-policy.mjs
…roups

Two more policy-bypass vectors found in review:

- open-pull-requests-limit: 0 disables version updates entirely rather than
  capping volume; the limit must now be between 1 and the maximum
  (pr-limit-disables-updates).
- A group whose applies-to is security-updates does not batch version updates,
  so groups-missing now requires at least one group covering version updates
  (applies-to absent or version-updates).

Adds regression tests; suite is 31/31. Addresses the Codex re-review findings
on #153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 34s —— View job


Tasks

  • Gather context and understand the request
  • Read the latest commit (08a4063 — reject disabling PR limit and security-only groups)
  • Verify the two P2 findings from the prior pass are addressed
  • Assess the new Codex P1 (match-all ignore rules)
  • Check standing non-blocking observations
  • Post final review

Review of fix commit 08a4063

Both P2 findings from the prior pass are correctly addressed. Here's the verdict on each, followed by the one open Codex P1 and the standing carry-overs.


Prior P2a — Zero PR limit ✅ Fixed

dependabot-policy.mjs lines 238–256

if (typeof limit === "number" && limit < 1) {
  findings.push(finding("pr-limit-disables-updates", ...));
} else if (typeof limit === "number" && limit > policy.maxOpenPullRequests) {
  findings.push(finding("pr-limit-too-high", ...));
}

Using a separate rule name (pr-limit-disables-updates) rather than folding into pr-limit-too-high is the right call — the failure mode is categorically different (updates disabled vs volume excessive). limit < 1 catches 0 and any negative value. Neither rule is in WAIVE_BY_RULE. The README at line 61 documents the new rule. Test at line 194 confirms open-pull-requests-limit: 0 emits pr-limit-disables-updates. ✅


Prior P2b — Security-only groups caught ✅ Fixed

dependabot-policy.mjs lines 211–236

const groupValues = groups !== null && typeof groups === "object" && !Array.isArray(groups)
  ? Object.values(groups)
  : [];
const coversVersionUpdates = groupValues.some((group) => {
  if (group === null || typeof group !== "object" || Array.isArray(group)) return false;
  const appliesTo = group["applies-to"];
  return appliesTo === undefined || appliesTo === "version-updates";
});

Replacing the old hasGroups boolean with coversVersionUpdates is correct: an empty {} (no groups defined), a groups object whose every entry has applies-to: "security-updates", or a groups object with non-mapping values all produce coversVersionUpdates === false and emit groups-missing. Two tests confirm: security-only fails (line 200), explicit version-updates passes (line 218). ✅


No regressions

The fix is scoped to the two new rules and their test cases. The 28 prior cases are unchanged.


Open Codex P1 — Match-all ignore rules pass undetected

dependabot-policy.mjs lines 154–258

entryFindings does not inspect entry.ignore. Per the Dependabot docs, an entry with ignore: [{ dependency-name: "*" }] tells Dependabot to suppress every version update for that ecosystem — equivalent to removing the entry entirely. A config that does this passes the gate cleanly.

This is the same class of bypass as cooldown.exclude: ["*"], which was caught in the b799c4d pass. The fix is analogous:

const ignoreList = Array.isArray(entry.ignore) ? entry.ignore : [];
const matchAllIgnore = ignoreList.some(
  (rule) =>
    rule !== null &&
    typeof rule === "object" &&
    !Array.isArray(rule) &&
    rule["dependency-name"] === "*",
);
if (matchAllIgnore) {
  findings.push(
    finding("updates-ignored", file, key, 'ignore contains a match-all dependency-name "*", so no version updates will be opened'),
  );
}

Or use the existing groups-missing framing and emit a new ignore-suppresses-updates rule — either name is fine as long as it's absent from WAIVE_BY_RULE. A test case: CONFORMANT + ignore: [{ dependency-name: "*" }] → emits the rule.

No active repo uses this pattern per the org audit, but the gap is structurally identical to the cooldown.exclude gap and the principle here is fail-closed. Fix this →


Standing observations (non-blocking, carried from prior passes)

All four remain. One update on the comment verbosity observation:

  • entryKey ordering — exception keys for directories entries are order-sensitive; a cosmetic YAML reorder triggers exception-inventory-drift. The README Exceptions section doesn't warn about this. Fix this →
  • file hardcoded to DEFAULT_DEPENDABOT_PATHauditRepository's dependabotPath param isn't reflected in findings. Not user-visible today.
  • CI job missing name:runner-policy at line 193 of ci.yml has name: Runner policy; the new dependabot-policy job (line 256) omits it, displaying as dependabot-policy in the checks list. Fix this →
  • dependabot.yml comment verbosity — the four-line materialization comments on component entries (lines 86–89) are now consistent with the concurrency-policy entry (lines 69–72) and close to the runner-policy entry (lines 53–55); the pattern is uniform across the file, so trimming this is a preference-only suggestion.

The two P2 findings are closed. The one open item is the Codex P1 on match-all ignore rules.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08a40635b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/dependabot-policy/dependabot-policy.mjs
Comment thread components/dependabot-policy/dependabot-policy.mjs Outdated
Two more policy-bypass vectors found in review, both the match-all-wildcard
shape:

- An ignore rule for dependency-name "*" with no version or update-type
  narrowing suppresses every update for the ecosystem; it is now reported as
  ignore-disables-updates. A narrow ignore (a specific dependency or a
  first-party owner) still passes.
- A version-updates group whose exclude-patterns is a match-all ("*") batches
  nothing, so groups-missing now also rejects a version group that excludes
  every dependency.

A third review comment proposed rejecting a match-all exclude-paths; that is not
a documented dependabot.yml option, so no rule was added.

Adds regression tests; suite is 34/34. Addresses the Codex re-review findings
on #153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 37s —— View job


Tasks

  • Gather context and understand the request
  • Read the diff for the latest commit (fca2194 — reject match-all ignore and group exclusions)
  • Verify the P1 finding (match-all ignore rules) is correctly addressed
  • Verify the P2 finding (group exclude-patterns match-all) is correctly addressed
  • Check exclude-paths resolution (no code change)
  • Look for new issues or regressions
  • Post final review

Review of fix commit fca2194

Both open findings from the previous review pass are correctly addressed. Here's the verdict on each.


Prior P1 — Match-all ignore rules ✅ Fixed

dependabot-policy.mjs lines 261–283

const disablingIgnore = ignore.some(
  (rule) =>
    rule !== null &&
    typeof rule === "object" &&
    !Array.isArray(rule) &&
    rule["dependency-name"] === "*" &&
    rule.versions === undefined &&
    rule["update-types"] === undefined,
);

The logic is correct. Requiring both versions === undefined and update-types === undefined means that { dependency-name: "*", versions: ["< 2.0.0"] } (ignores only pre-2.0 versions) and { dependency-name: "*", update-types: ["version-update:semver-patch"] } (ignores only patches) both pass — only the genuinely disabling case fires. ignore-disables-updates is correctly absent from WAIVE_BY_RULE. ✅

Three tests cover the critical paths: match-all fires, narrow dependency name passes, and (implicitly via the versions/update-types guards) narrowed wildcards pass.


Prior P2 — Group exclude-patterns match-all ✅ Fixed

dependabot-policy.mjs lines 220–229

const isVersionGroup = appliesTo === undefined || appliesTo === "version-updates";
const excludePatterns = group["exclude-patterns"];
const excludesEverything = Array.isArray(excludePatterns) && excludePatterns.includes("*");
return isVersionGroup && !excludesEverything;

excludePatterns.includes("*") mirrors the cooldown.exclude.includes("*") pattern from the prior soak-bypass fix and correctly catches ["specific-dep", "*"] too (since "*" in any position still excludes everything). If one group has a match-all exclusion but another doesn't, the some() correctly allows the entry to pass — the presence of a non-excluded group satisfies the requirement. ✅


exclude-paths ✅ Correctly resolved without code change

Kyle verified that exclude-paths is not a documented Dependabot option. The Codex finding cited exclude-paths from a different context; the actual Dependabot options reference confirms it doesn't exist. No rule added; no surface to protect. ✅


No regressions

The fix is scoped to the two new checks. Kyle reports 34/34. Nothing in the exception-drift, waiver-consumed, or structural-guard logic is affected.


Standing observations (non-blocking, carried from prior passes)

These four remain open and none block merge:

  • entryKey ordering — The README (line 51) notes that directories is joined, but doesn't warn that the join order must match the YAML order in dependabot.yml for exception keys to remain stable after a cosmetic reorder. Worth one sentence in the Exceptions section. Fix this →
  • file hardcoded to DEFAULT_DEPENDABOT_PATHauditRepository's dependabotPath param isn't reflected in findings. Not user-visible today; a comment on the export would clarify intent.
  • CI job missing name:runner-policy at line 193 of ci.yml has name: Runner policy; the dependabot-policy job at line 256 omits it and displays as dependabot-policy in the checks list. Fix this →
  • dependabot.yml comment verbosity — the four-line comments on component entries are now consistent across all three component entries (runner-policy, concurrency-policy, dependabot-policy); trimming is preference-only.

Both open findings are closed. No new issues found. The implementation is ready to merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fca21942f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/dependabot-policy/dependabot-policy.mjs
@kyle-sexton
kyle-sexton merged commit f77d805 into main Jul 16, 2026
41 checks passed
@kyle-sexton
kyle-sexton deleted the feat/dependabot-policy-component branch July 16, 2026 20:02
kyle-sexton added a commit that referenced this pull request Jul 16, 2026
…racts (#156)

## Summary

Register `runner-input` contracts in
`components/runner-policy/policy.json` for two floor-conversion
reusables, both pinning ci-workflows PR #124's squash-merge commit
`3dfb18452a8c6059a22e62456390d84feb10b42f` (the reviewed source that
carries the governed `runner` input):

- `link-check.yml` — `allowedInputs: ["runner", "args"]`,
`allowedCallerPermissions: {contents: read, issues: write}`.
- `pulumi-version-drift-check.yml` — `allowedInputs: ["runner"]`,
`allowedCallerPermissions: {contents: read, issues: write}`.

Each carries `allowedCallerPermissions` because its rolling
tracking-issue lane writes issues through the caller `GITHUB_TOKEN`; the
waiver is honored only while the call is genuinely selector-routed, and
the reusables' own permissions are unchanged. This also corrects the
now-false README claim that the Pulumi version-drift monitor "remains
fixed to explicit GitHub-hosted images," adds a review note for both
contracts, and extends the runner-policy golden fixture
(`runner-policy.test.mjs`) to assert them. `hostedExceptionReasons` is
**unchanged** — enum-tightening is the separate last wave and must not
land before every consumer has dropped its floor exceptions.

## Why

Wave 2 of the private-repo hosted-floor elimination
(melodic-software/github-iac#78, decision of record 2026-07-16).
ci-workflows PR #124 (Wave 1) added the governed `runner` input to these
reusables' source; this PR registers the reviewed contracts so each
private consumer (Wave 3) can add a `select-runner` job, route the
reusable to the fleet, and drop its `hosted-control-plane` /
`privileged-control-plane` exception. `policy.json` is the upstream
source of truth synced to five consumers, so the contract belongs here.

## Deferred — standards-sync

The third floor-conversion reusable, `standards-sync.yml`, is
**intentionally not registered here.** Its caller `GITHUB_TOKEN` is
read-only (`sync.yml` declares `permissions: contents: read`; all writes
go through a minted App token), but a `runner-input` contract that
passes secrets must carry `allowedCallerPermissions`, which the
validator requires to include at least one `write`
(`runner-policy.mjs:186-198`). There is no honest caller write to
declare, so registering it would bake a least-privilege inaccuracy into
the managed contract. This is a contract-model gap (the secrets waiver
is coupled to a write requirement) routed to the runner-policy component
owner for a "reviewed secrets + read-only caller" contract shape;
`standards-sync` keeps its hosted-only contract until that lands.

## Test plan

- [x] `npm run test:runner-policy` — passing with two new golden
assertions on the registered contracts: 143/143 on this branch base, and
146/146 verified against a local rebase onto current `main` (post #155
review-tier admission, #153 dependabot-policy component).
- [x] `npm run lint:runner-policy`
(`GITHUB_REPOSITORY=melodic-software/standards`) — `Runner policy
passed` on both the branch base and the current-`main` rebase (the #155
validator).
- [x] biome, markdownlint — clean (pre-commit).
- [x] Merge SHA finalized to `3dfb184…` (PR #124 squash-merge) across
`policy.json` (both contract keys), the golden-fixture constant, and the
README review note.

## Related

No linked standards issue. Cross-repo references:

- Wave 2 of melodic-software/github-iac#78 (private-repo hosted-floor
elimination epic).
- Depends on melodic-software/ci-workflows#124 (merged) — source of the
pinned reusable SHA.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adopt Dependabot groups + cooldown + PR-limit policy org-wide

1 participant