Skip to content

Pack manifest as the single source: routing guidance, skills, scoped rules - #555

Merged
missingbulb merged 5 commits into
mainfrom
claude/github-actions-scheduling-yupnki
Jul 29, 2026
Merged

Pack manifest as the single source: routing guidance, skills, scoped rules#555
missingbulb merged 5 commits into
mainfrom
claude/github-actions-scheduling-yupnki

Conversation

@missingbulb

@missingbulb missingbulb commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Nothing in the corpus said where a piece of content belongs, so when a rule, doc, skill or check could plausibly live in more than one pack the decision defaulted to basics — which is exactly how the GitHub Actions cron doctrine landed there in this PR's first commit. Fixing that turned into making the pack manifest state what a pack is, and retiring the places that said it a second time.

The manifest spec (#559)

engine/pack_loader/pack-schema.mjs declares every field a pack.mjs may carry, which are required, and each one's constraints. The loader validates every manifest against it — canon and consumer local packs alike — so an incomplete declaration is a blocking config error at load, the same class as invalid JSON in .claudinite-checks.json. A required manifest field is part of the pack contract, not a conformance opinion about a repo's content: a check would have to be declared by a pack, run only when that pack is active, and re-derive the manifest by reading its source text.

  • Reported, not fatal — the pack still loads and still runs its checks; silently disabling a repo's own rules is a worse failure than the one being reported.
  • The vocabulary is closed, so a typo (rule:, skill:) fails loudly instead of being ignored forever.

ruleRoutingGuidance — what belongs here, and what does not

ruleRoutingGuidance: {
  belongs: 'workflow YAML and Actions runner platform behaviour: triggers, secrets, permissions, scheduling, artifacts, reusable workflows and their pitfalls',
  excludes: 'git and GitHub command procedure — git-github; release pipeline content for one product — its release pack',
},

Both sides required, each capped at 20 words — a session-context budget, not a style rule: inject-pack-prose emits the set as a routing table at session start, one row per pack, for every pack discovered (a consumer holds only what it vendored, so that set is already what it can route into). excludes names the pack that owns the other side, so the table routes rather than merely refuses; a boundary true of every pack carries no routing information and wastes the row. Filled in for all 27 canon packs and the home's two local packs.

worldRules / workRules — a rule's scope is its placement

The single rules array splits in two, so which list a rule sits in is what makes it world- or work-scoped. The loader flattens both into the rules array the runners walk, stamping each rule's scope from its list. A rule module may still carry scope — the runRule dispatch seam reads it off the rule object to hand work rules their fluent surface — but the two must agree, or it's a load error.

skills — the bundle, declared

skills: ['merge-to-main', ...], held to the skills/ tree in both directions: an undeclared directory is a skill nothing announces, a declared name with no directory is a manifest that lies. What each skill covers stays in its SKILL.md frontmatter rather than being copied.

Pack READMEs

With skills and scoped rules on the manifest, each pack README's checks table and skills table were a second copy — and had already drifted: basics/README.md listed a skill-ownership check the pack does not carry and omitted catalog-completeness and comment-classification. Both tables are stripped from every pack README, and the five files that were nothing else are deleted: github-actions plus the four stub packs, whose one non-derivable sentence (expected first source) moved into the manifest header comment.

The other 22 stay — provenance, design rationale, scope notes against sibling packs, an index of their RULES.md prose, none of it in the manifest. catalog-completeness now looks for the pack directory in the catalog rather than a README inside it.

The cron doctrine, rehomed (#556)

Out of packs/basics/scheduled-tasks.md and into packs/github-actions/skills/github-actions-scheduling/ — the pack that owns Actions platform behaviour. GitHub's schedule: trigger is a request to queue, not a promise to run: late fires, dropped fires with no failure and nothing in the run ledger, and the 60-day inactivity disable (Upptime's measurement). What follows: never let correctness depend on a firing happening or on when it happened; stay idempotent and self-catching; catch up the most recent slot only; keep off :00; say "about hourly, best-effort"; don't debug a late run as a bug before checking whether GitHub fired at all.

packs/basics/scheduled-tasks.md is unchanged from main — the scheduling doctrine stays put, only the platform truth moved.

Verification

  • 945 tests, 945 pass across the CI test roots, including 12 new tests for the manifest spec and a corpus-wide assertion that every discovered pack loads clean through it.
  • node engine/checks/check_the_world.mjs0 blocking, 8 advisory — the same pre-existing file-placement advisories in engine/, none new.
  • Fixtures updated where the change reached them: fake-corpus test harnesses copy pack-schema.mjs beside the registry, the vendor set grew by that one file, and the demo local packs declare ruleRoutingGuidance.

…fort

GitHub's schedule: trigger is a request to queue, not a promise to run —
firings land late and are sometimes dropped outright, with no failure and
nothing in the ledger. The engine already assumes this (ledger-based slot
math, most-recent-slot-only catch-up, the hashed :10-:50 minute), but the
fact lived only in a risk-table row in MIGRATION.md, so nothing told an
author or a session how to design and speak by it.

Adds a section to the scheduling doctrine covering the platform behaviour
(with the Upptime measurement and the 60-day inactivity disable), and the
four consequences: don't design against punctuality, an hourly task must
be idempotent and self-catching since hourly slots never catch up, say
"about hourly, best-effort" to a human and don't debug a late run as a
bug, and deadline-bound work does not belong on this cron.

Closes #556

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeXiMjUB2tcQ1gJ7TqTrHB
@missingbulb
missingbulb force-pushed the claude/github-actions-scheduling-yupnki branch from 97bf619 to 56fe327 Compare July 29, 2026 13:49
Two halves of one problem: nothing said where a piece of content belongs,
so it defaulted into the baseline pack — which is where the GitHub Actions
cron doctrine landed in the first place.

Routing (#559). Every pack.mjs now declares routing: { belongs, excludes },
each side capped at 20 words, with excludes naming the pack that owns the
other side. A new blocking check (pack-routing-declared, basics) holds
canon and consumer local packs alike to it, relevance-first on a pack.mjs
in the sweep. The pack-prose loader emits the whole set as a routing table
at session start, for every pack discovered rather than only the active
ones — a consumer holds just the packs it vendored, so the discovered set
is already what it can route into. Declarations filled in for all 27 canon
packs and the home's two local packs.

Cron doctrine (#556). Moved out of packs/basics/scheduled-tasks.md into a
bundled skill in the pack that owns Actions platform behaviour:
github-actions/skills/github-actions-scheduling. It states what schedule:
actually guarantees (late fires, dropped fires, the 60-day inactivity
disable, the Upptime measurement) and what follows — never depend on a
firing happening or on when it happened, stay idempotent and self-catching,
catch up the most recent slot only, keep off :00, say "about hourly,
best-effort", and don't debug a late run as a bug before checking whether
GitHub fired at all.

Closes #559
Refs #556

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeXiMjUB2tcQ1gJ7TqTrHB
@missingbulb missingbulb changed the title Scheduled tasks: state the platform truth that GitHub cron is best-effort Pack routing declarations, and the cron doctrine as a github-actions skill Jul 29, 2026
claude added 3 commits July 29, 2026 14:28
The two local packs both excluded "anything portable to a consuming repo",
which is the local-pack rule restated — true of either by definition, so
the row said nothing about which of the two a piece of content belongs to.
Point them at each other instead: canon-curation is the fleet-facing
curation of the shared corpus, claudinite is the home's own non-portable
working rules, and each excludes names the other.

Adds the general rule to the routing section in packs/README.md: state
what separates a pack from its nearest neighbours, since a boundary that
holds for every pack carries no routing information and wastes the row.

Refs #559

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeXiMjUB2tcQ1gJ7TqTrHB
…ecked

Renames the routing field to ruleRoutingGuidance, and moves its enforcement
off a conformance check and onto the pack definition itself.

engine/pack_loader/pack-schema.mjs is the spec: every field a pack.mjs may
carry, which are required, and the constraints on each. The loader validates
every manifest against it — canon and consumer local packs alike — so an
incomplete declaration is a blocking config error at load, the same class as
invalid JSON in .claudinite-checks.json. Reported, not fatal: a pack with an
incomplete declaration still loads and still runs its checks. The vocabulary
is closed, so a typo like `rule:` fails loudly instead of being ignored.
packs/basics/pack-routing-declared.mjs and its test are gone.

The manifest now also carries what the pack README used to state:

- `skills: [...]` names the bundled skills, held to the skills/ tree in both
  directions — an undeclared directory is a skill nothing announces, and a
  declared name with no directory is a manifest that lies.
- `worldRules` / `workRules` replace the single `rules` array, so a rule's
  scope is its placement. The loader flattens both into the `rules` array the
  runners walk, stamping each rule's scope from the list it came from. A rule
  module may still carry `scope` for the dispatch seam that reads it off the
  rule object, but the two must agree — a mismatch is a load error.

Test fixtures that build a fake corpus now copy pack-schema.mjs beside the
registry, and the vendor set grew by that one file.

Refs #559

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeXiMjUB2tcQ1gJ7TqTrHB
With skills and scope-split rules declared on pack.mjs, a pack README's
checks table and skills table are a second copy of the manifest — and they
had already drifted: basics/README.md listed a skill-ownership check the
pack does not carry, and omitted catalog-completeness and
comment-classification.

Strips both tables from every pack README, and deletes the five files that
were nothing else: github-actions (activation line + two tables) and the
four stub packs (android, ios, app-store-release, play-store-release),
whose one non-derivable sentence — the expected first source — moves into
the manifest header comment where the rest of the stub note already lives.

The other 22 READMEs stay: they carry provenance, design rationale, scope
notes against sibling packs, and an index of their RULES.md prose — none
of which the manifest holds. "Most packs lose the README" turned out to be
"most packs lose the duplicated tables".

catalog-completeness now looks for the pack DIRECTORY in the catalog rather
than a README inside it, since a pack need not have one, and the catalog's
five affected rows link the directory instead.

Refs #559

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeXiMjUB2tcQ1gJ7TqTrHB
@missingbulb missingbulb changed the title Pack routing declarations, and the cron doctrine as a github-actions skill Pack manifest as the single source: routing guidance, skills, scoped rules Jul 29, 2026
@missingbulb
missingbulb merged commit 78c8751 into main Jul 29, 2026
1 check passed
@missingbulb
missingbulb deleted the claude/github-actions-scheduling-yupnki branch July 29, 2026 16:15
missingbulb added a commit that referenced this pull request Jul 30, 2026
* docs: record the fleet-settings alignment as a future direction

Two repo settings decide whether a member's baselining can deliver at all,
and converging them alongside the sheepdog freshness sweep would be the
natural home — a sweep keeps them true where adoption only sets them once.
It is blocked on credential scope: FLEET_GITHUB_TOKEN is not an admin
token, and both endpoints require admin on the member.

Recorded in docs/future-directions.md rather than the sheepdog pack's
RULES.md. RULES.md is injected into every session that declares the pack,
so it is the wrong place for an idea nobody can act on — a deferred
direction should cost no context. The new file is documentation only, and
carries no link from any vendored pack file (a pack reaching into the
canon's docs/ would dangle in every consumer).

Refs #590

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

* basics: name the path-filtered-conformance trap as a rule

auto-merge is a queue for CHECKS, so three CI shapes exist and only one
of them silently fails:

  no pull_request workflow  -> baselining merges directly (#588). Coherent.
  an UNFILTERED sweep       -> runs, auto-merge lands on green. Correct.
  a PATH-FILTERED sweep     -> the arm succeeds and no check ever runs.

The third is invisible: the repo looks like it has CI, hasPrCi is true,
the arm reports success, and the maintenance PR simply never lands. TLDR
sat in it — all three of its test workflows filter to the product tree, so
a mount-only diff matched nothing, and the same filters had hidden two
blocking claudinite-isolation findings on its main.

ADVISORY on purpose. Shipping it blocking would red every member lacking
the workflow on its very next baselining — which is precisely the #555
failure this rule exists to make less likely. It becomes blocking once the
fleet carries the workflow; the promotion is a one-line change.

Relevance-first on the vendored mount, so it is inert in any non-member.
A repo with no pull_request workflow at all is deliberately not flagged.

Refs #592

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
missingbulb pushed a commit that referenced this pull request Jul 30, 2026
Phase 2 of #593. Four fixture consumers, each a shape the fleet actually
has, converged in CI with the REAL scripts against the working tree:
apply-vendor-set, converge-wiring, migrations/apply, then selftest and
check_the_world. Two modes — fresh (the nightly path) and stale (pinned in
the past, so migration SELECTION actually fires and a record that is
missing, misdated or non-idempotent shows up).

Why it exists: canon CI proves the canon is healthy, which is not evidence
about members. The canon's own packs are always already migrated, so a
change that breaks every consumer passes it cleanly.

Verified by reproducing #555 faithfully rather than approximately — the
schema gains a required manifest field AND the same commit updates every
canon pack, which is what actually happened on 2026-07-29. Under that
mutation:

  canon CI       0 blocking   (green, exactly as it was)
  the rehearsal  FAILS        naming the consumer pack that no longer validates

The self-test runs before the content sweep, deliberately: a broken machine
reports no findings precisely because it is not running, so a content check
consulted first answers green and means nothing.

Each fixture runs with the FIXTURE as cwd and as a real git repo — a sweep
inheriting the canon's cwd resolves git against the canon and cheerfully
reports on the wrong repo, which is how the first draft passed while
testing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECifRRammkF9uD7zdEGFpb
missingbulb pushed a commit that referenced this pull request Jul 30, 2026
…n diff

Phase 4 of #593. consumer-safe-changes.md has always SAID that a change to
a vendored contract must carry consumers across it. Nothing enforced that,
and #555 merged green while eleven consumer packs stopped validating —
because the canon's own packs were updated in the same commit and canon CI
has nothing else to look at.

So this is a check over the diff, not prose about the diff. Work-scoped:
the branch's change is what it judges, and judging the whole tree would be
meaningless — the tree always contains a manifest field; the question is
whether THIS change added one.

Fires on three surfaces, each one a consumer holds a copy of and cannot be
asked to change in the same commit: pack-schema.mjs (#555's exact file), a
rule that is blocking, and the scheduler stub. Discharged by EITHER a
migration record or a rehearsal fixture — they answer different questions
('members are moved across' vs 'members are unharmed') and both are
legitimate; a genuinely additive change needs only the second.

Deliberately narrow. A rule that cried wolf on every canon commit would be
turned off within a week, and then be worth nothing on the day it mattered
— so ordinary engine and pack edits, advisory rules, and test files are all
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECifRRammkF9uD7zdEGFpb
missingbulb added a commit that referenced this pull request Jul 31, 2026
Two lessons mined from the 15 conversation logs captured 2026-07-30, both
from the fleet-baselining session behind #585.

- Tightening a contract that member-owned files must satisfy has no carrier:
  vendoring refreshes only .claudinite/shared/, and a baseline migration is a
  path relocation, so it cannot express a schema change to a member's own
  pack.mjs. #555 tightened the manifest vocabulary with no record, and
  normalizeManifest rebuilds `rules` from the scoped keys alone — a legacy
  manifest's checks silently stop running. Eleven local packs across ten
  members were rewritten by hand on 2026-07-30.

- The fleet machinery reports success from reaching the end of the code path,
  not from the artifact. baselining's deliver() never read the PR-open POST's
  status, so a 403 left the run logging `ok` while eleven of twelve consumers
  sat frozen on the 07-28 canon ref behind 27 orphan maintenance branches.
  Read the status of every API write you branch on; judge the fleet by
  members' stamps, not scheduler run conclusions.

Refs #598.

Co-authored-by: Claude <noreply@anthropic.com>
missingbulb added a commit that referenced this pull request Jul 31, 2026
…ary, and the rule (#595)

* engine: a startup self-test — can Claudinite run here?

Deliberately not a conformance check. check_the_world judges the repo's
CONTENT against the rules; this judges whether the MACHINERY that would run
those rules is intact. A repo can be perfectly conformant and still have a
broken mount, a hook pointing at a deleted file, or a pack the loader
silently drops — and none of that is visible to a content check, because a
rule that never runs reports nothing.

Every fleet-wide failure this corpus has had was that shape, and each is a
probe here:

  #397  inject-pack-prose imported a module that did not exist, so every
        active pack's RULES.md went un-injected fleet-wide for days
  #518  loadConfig dropped the mount stamp, so baselining self-skipped
        everywhere and no run ever said so
  #555  a required manifest field landed with no migration; consumer packs
        stopped validating, and since a pack that fails validation
        contributes no rules, check_the_world kept reporting green about a
        corpus it was no longer running
  #424  five mounted skill links dangled on main, unnoticed

None was caught by canon CI, because the canon's own tree is always already
correct. All four would have fired on the first session after the change,
on the member, where the damage was.

Runs in three places. SessionStart, FIRST and report-only — never --strict
there, because a non-zero exit makes Claude Code discard the hook's stdout,
which would throw away the very report it exists to deliver. And baselining
preprocessing, against the just-converged tree, where a failure now
escalates to the agent: broken machinery is the judgment case, and merging
it would spread the breakage to a repo that was working an hour ago.

Pure probes over injected io, so the whole set runs with no tree on disk.
Not-applicable is a distinct result from failure throughout — conflating
them is how a check earns a reputation for crying wolf. Mutation-tested:
neutering the stamp probe and the hook-target probe each turn the suite red.

Refs #592, #593

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

* baselining: rehearsal mode — converge against a canon branch, stamp nothing

Phase 0 of #593. A run can be pointed at a canon BRANCH
(CLAUDINITE_CANON_REF, plus CLAUDINITE_CANON_URL for a fork) so a canon
change can be tried against a REAL repo before it merges — the layer that
fixtures cannot provide, because a fixture only tests what someone
remembered to model.

The stamp is why this needs a decision of its own rather than an extra
clone argument. A branch head is not on trunk, and stamping it leaves the
member in exactly the ref-not-on-trunk shape vendoring's #328 anti-rewind
guard then refuses to converge over — a rehearsal that wedges the repo it
was rehearsing on. That is not hypothetical: it happened by hand on
2026-07-30, to all thirteen members at once, from a converge run out of an
unmerged branch.

So a rehearsal converges, reports, and restores: no commit, no branch, no
PR, no stamp. It exits non-zero when the self-test fails or the checks go
red, which is the whole point — that exit is what fails the canon PR.

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

* packs-tests: the baselining rehearsal — the gate #555 needed

Phase 2 of #593. Four fixture consumers, each a shape the fleet actually
has, converged in CI with the REAL scripts against the working tree:
apply-vendor-set, converge-wiring, migrations/apply, then selftest and
check_the_world. Two modes — fresh (the nightly path) and stale (pinned in
the past, so migration SELECTION actually fires and a record that is
missing, misdated or non-idempotent shows up).

Why it exists: canon CI proves the canon is healthy, which is not evidence
about members. The canon's own packs are always already migrated, so a
change that breaks every consumer passes it cleanly.

Verified by reproducing #555 faithfully rather than approximately — the
schema gains a required manifest field AND the same commit updates every
canon pack, which is what actually happened on 2026-07-29. Under that
mutation:

  canon CI       0 blocking   (green, exactly as it was)
  the rehearsal  FAILS        naming the consumer pack that no longer validates

The self-test runs before the content sweep, deliberately: a broken machine
reports no findings precisely because it is not running, so a content check
consulted first answers green and means nothing.

Each fixture runs with the FIXTURE as cwd and as a real git repo — a sweep
inheriting the canon's cwd resolves git against the canon and cheerfully
reports on the wrong repo, which is how the first draft passed while
testing nothing.

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

* claudinite: enforce the consumer-safe-change rule over the canon's own diff

Phase 4 of #593. consumer-safe-changes.md has always SAID that a change to
a vendored contract must carry consumers across it. Nothing enforced that,
and #555 merged green while eleven consumer packs stopped validating —
because the canon's own packs were updated in the same commit and canon CI
has nothing else to look at.

So this is a check over the diff, not prose about the diff. Work-scoped:
the branch's change is what it judges, and judging the whole tree would be
meaningless — the tree always contains a manifest field; the question is
whether THIS change added one.

Fires on three surfaces, each one a consumer holds a copy of and cannot be
asked to change in the same commit: pack-schema.mjs (#555's exact file), a
rule that is blocking, and the scheduler stub. Discharged by EITHER a
migration record or a rehearsal fixture — they answer different questions
('members are moved across' vs 'members are unharmed') and both are
legitimate; a genuinely additive change needs only the second.

Deliberately narrow. A rule that cried wolf on every canon commit would be
turned off within a week, and then be worth nothing on the day it mattered
— so ordinary engine and pack edits, advisory rules, and test files are all
untouched.

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

* canary: a live rehearsal against a real repo, and the procedure doc

Phase 3 of #593, and the procedure the owner asked for.

The fixture rehearsal is the fast gate; its honest weakness is that a
fixture only tests what someone remembered to model. The canary is the
other half: a REAL repo, really adopting Claudinite, converged against a
canon ref and asked whether it still works. Manual dispatch, plus
automatic on merge to main — post-merge is still much better than not
knowing, and #585 went two days unnoticed.

A DEDICATED disposable repo, not a real project: a rehearsal rewrites the
mount and can leave a failed converge behind, and pointing that at
EdFringeNow means a bad canon commit leaves a real project dirty exactly
when attention is elsewhere. The workflow no-ops with a warning until
CLAUDINITE_CANARY_REPO names one, so it is inert rather than broken before
the repo exists.

It runs the CANARY's own vendored worker, not this checkout's — that is
the copy a member runs, and testing anything else tests a path no consumer
ever executes.

Also makes the rehearsal fixtures independent of the HOST's git signing
config. They commit, and a machine with signing wired up fails those
commits for reasons unrelated to the canon change under test — the
rehearsal would then report a fleet-breaking result because a signing
helper was unavailable. Found exactly that way: this environment's signing
helper ran out of file descriptors mid-session.

consumer-safe-changes.md gains the three-layer procedure, including the
part that answers 'with and without a migration': note selection fires
against the stamp's day, so only the stale-mode fixture exercises it.

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

* canary: declare the canary repo in settings, not a repository variable

The binding between this canon and its canary is a fact about the project,
so it belongs in the tracked declaration — on the local/claudinite pack
entry's config.canaryRepo, the same pack-entry config mechanism the
sheepdog pack already uses for its fleet scope.

A GitHub repository variable was the wrong home: invisible in the tree,
absent from every diff, and unreviewable beside the workflow that acts on
it. That last point is the sharp one — this value names a repo that CI
checks out and rewrites, and a reader of this repo should be able to see
which.

Chose the pack-entry config over a new top-level settings key on purpose:
CONFIG_KEYS is a contract every consumer's declaration is validated
against, and widening it fleet-wide to hold one canon-only fact would be
the same kind of change this PR exists to make harder.

A drift guard keeps the two in step — the declaration must name a canary in
owner/name form, the workflow must resolve it from there, and it may not
hardcode the name (which would work while leaving the declaration
authoritative-looking and ignored). Mutation-checked: reintroducing the
repository variable turns the guard red.

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

---------

Co-authored-by: Claude <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.

2 participants