Pack manifest as the single source: routing guidance, skills, scoped rules - #555
Merged
Merged
Conversation
…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
force-pushed
the
claude/github-actions-scheduling-yupnki
branch
from
July 29, 2026 13:49
97bf619 to
56fe327
Compare
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
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
This was referenced Jul 30, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mjsdeclares every field apack.mjsmay 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 blockingconfigerror 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.rule:,skill:) fails loudly instead of being ignored forever.ruleRoutingGuidance— what belongs here, and what does notBoth sides required, each capped at 20 words — a session-context budget, not a style rule:
inject-pack-proseemits 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).excludesnames 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 placementThe single
rulesarray splits in two, so which list a rule sits in is what makes it world- or work-scoped. The loader flattens both into therulesarray the runners walk, stamping each rule's scope from its list. A rule module may still carryscope— therunRuledispatch 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, declaredskills: ['merge-to-main', ...], held to theskills/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 itsSKILL.mdfrontmatter 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.mdlisted askill-ownershipcheck the pack does not carry and omittedcatalog-completenessandcomment-classification. Both tables are stripped from every pack README, and the five files that were nothing else are deleted:github-actionsplus 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.mdprose, none of it in the manifest.catalog-completenessnow 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.mdand intopacks/github-actions/skills/github-actions-scheduling/— the pack that owns Actions platform behaviour. GitHub'sschedule: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.mdis unchanged frommain— the scheduling doctrine stays put, only the platform truth moved.Verification
node engine/checks/check_the_world.mjs→ 0 blocking, 8 advisory — the same pre-existingfile-placementadvisories inengine/, none new.pack-schema.mjsbeside the registry, the vendor set grew by that one file, and the demo local packs declareruleRoutingGuidance.