Skip to content

Commit 4efb882

Browse files
missingbulbclaude
andauthored
Claudinite: refresh the vendored mount to canon 7e89c0c (#87)
Out-of-band refresh, pushed from a canon session, because this repo could not perform it itself. Its scheduler has been running green and skipping baselining every night with "no vendored mount (no stamp)" — on a repo whose .claudinite-checks.json carries one. The vendored `loadConfig` validated `claudinite` as a legal settings key and then dropped it from the object it returned, so the `stamp` signal read undefined and baselining took the branch meant for the canon and pre-adoption repos. That made the fix unreachable by the normal route: baselining is what updates the mount, and the bug was in the mount. Hence this manual converge. Carried by this refresh (canon PRs #511, #516, #518): - loadConfig keeps `claudinite` and `maintenance`, so the stamp is readable again - the executor resolves its dispatch from the CCR trigger, not just the Actions payload, and no longer guesses an issue when it cannot identify one - FORCE_TASKS: a manual scheduler run can force a task past its slot gate Mount converged whole-set and the wiring re-converged from the vendored stub (the scheduler workflow gains the `overrides` input). Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7c7263d commit 4efb882

71 files changed

Lines changed: 1475 additions & 1216 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claudinite-checks.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"monthlyDay": 1
3232
},
3333
"claudinite": {
34-
"updated": "2026-07-26T20:37:14.733Z",
35-
"ref": "cae740044dc814e48c41120106b39b68ec2e91fa"
34+
"updated": "2026-07-28T10:27:38.404Z",
35+
"ref": "7e89c0cfe40c133739fdeb351e9d463f1d4b3097"
3636
}
3737
}

.claudinite/shared/engine/checks/helpers/repo-context.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const PACK_ENTRY_KEYS = ['id', 'config', 'answers', 'rules', 'accept', 'v
124124
// read this one shape regardless of which form the file used.
125125
export function loadConfig(root) {
126126
const path = join(root, '.claudinite-checks.json');
127-
const empty = { packs: [], packEntries: [], rules: {}, accept: [], sharedConstants: [], packConfig: {}, taskScheduler: null, errors: [] };
127+
const empty = { packs: [], packEntries: [], rules: {}, accept: [], sharedConstants: [], packConfig: {}, taskScheduler: null, claudinite: null, maintenance: null, errors: [] };
128128
if (!existsSync(path)) return empty;
129129

130130
let raw;
@@ -271,6 +271,18 @@ export function loadConfig(root) {
271271
sharedConstants: Array.isArray(raw.sharedConstants) ? raw.sharedConstants : [],
272272
packConfig,
273273
taskScheduler,
274+
// Passed through as declared, not normalized: `claudinite` is the
275+
// vendored-mount provenance stamp the `stamp` signal reads, and `maintenance`
276+
// is the delivery preference. Both are in CONFIG_KEYS, so declaring them is
277+
// legal and raises no error — and omitting them from THIS shape is what made
278+
// the stamp invisible to the scheduler and silently killed baselining across
279+
// the whole fleet: every repo self-skipped as "no vendored mount (no stamp)"
280+
// while its scheduler runs went green. A key that validates but does not
281+
// survive the load is the worst kind — legal to write, impossible to read,
282+
// silent in both directions. The CONFIG_KEYS-survive-loadConfig test pins the
283+
// whole set so no future key can go the same way.
284+
claudinite: raw.claudinite ?? null,
285+
maintenance: raw.maintenance ?? null,
274286
errors,
275287
};
276288
}

.claudinite/shared/engine/pack_loader/pack-registry.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export const declTokenFor = (pack) =>
248248
// A `packs` declaration entry is either a plain id string or an entry object
249249
// `{ id, config?, rules?, accept?, via? }` carrying that pack's own settings
250250
// (see engine/checks/README.md). This is the one id-extractor every reader shares, so
251-
// raw-JSON consumers (the SessionStart hooks, the fleet routines) and the
251+
// raw-JSON consumers (the SessionStart hooks, the fleet signal probe) and the
252252
// engine agree on both shapes — and on both declaration forms: it returns the
253253
// BARE pack id, stripping a `local_packs/` namespace where one is declared.
254254
// Returns undefined for a malformed entry.

.claudinite/shared/engine/scheduler/executor.md

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Claudinite executor
22

3+
> **Before anything else — name your one issue.** This session runs exactly one
4+
> dispatch. Step 1 resolves it **in code** from the trigger this session carries
5+
> and validates it; state the number it prints before you touch anything.
6+
> If you cannot name exactly one, **run nothing and end the session** — the
7+
> scheduler re-arms an unrun dispatch on its next hourly pass, so stopping costs a
8+
> delay while guessing costs duplicated work. **There is no fallback**: never
9+
> select a dispatch by listing the queue and taking one. Never process a second
10+
> issue in one session, under any circumstance — including when a listing shows
11+
> several waiting. Those are other sessions' dispatches, already running.
12+
313
You are the per-repo **executor** — you run the scheduled **tasks** dispatched to
414
this repo (per-project-scheduling DESIGN §5). A routine wired to a dispatch label
515
event started this session: the scheduler Action evaluated a task's precondition,
@@ -46,46 +56,83 @@ so use `engine/scheduler/`.
4656

4757
## Procedure
4858

49-
1. **Your work list is exactly one issue: the one whose labeling triggered this
50-
session.** Run it and nothing else. Do **not** list, claim, or process any
51-
other open issue — not under your ready label, and not under the other one.
52-
53-
This is load-bearing, not a style preference. One scheduler run files every due
54-
task's dispatch issue within a couple of seconds, each already carrying its
55-
ready label, so **one run emits one label event per issue and starts one session
56-
per event**. An executor that also swept its siblings had every one of those
57-
sessions build the same N-issue work list and race over it — and the claim in
58-
step 3 could not prevent it, because all the sessions read the list before any
59-
of them claimed. That produced the same dispatch executed two and three times
60-
over: duplicate tracker issues, duplicate bug reports, duplicate PRs making the
61-
same changes. One session, one issue, and the concurrency is safe by
62-
construction.
59+
1. **Resolve and validate your one dispatch — in code, first thing.**
60+
61+
```bash
62+
node <engine>/scheduler/resolve-dispatch.mjs self # `fleet` if your trigger is ready-for-agent-fleet
63+
```
64+
65+
(`<engine>` per Engine command paths above.) It finds the `issues.labeled`
66+
trigger that started this session — from **either** transport, see below —
67+
and asserts in code, before any judgment of yours, that the issue body names a
68+
legal task path, the file exists at HEAD, its pack is declared, and its
69+
`task.mjs` sibling parses to a valid declaration. It makes no GitHub calls of
70+
its own.
71+
72+
**Your trigger arrives one of two ways, and the shell reads both:**
73+
74+
- **GitHub Actions** writes the whole webhook payload to `$GITHUB_EVENT_PATH`
75+
— issue number, label, and body all in one file, so this resolves in a
76+
single command.
77+
- **Claude Code on the web (CCR)** writes no payload file. It delivers the
78+
same trigger as environment variables — `CCR_TRIGGER_SOURCE`,
79+
`CCR_TRIGGER_EVENT`, `CCR_TRIGGER_REPO`, `CCR_TRIGGER_ISSUE_NUMBER` — which
80+
**name your issue but carry neither its labels nor its body**. So this
81+
resolves in two commands: the shell exits `13` telling you the number, you
82+
fetch **that one issue** over MCP, and you re-run with what you fetched.
83+
84+
**Act on its exit code — that is the interface**, not the prose it prints:
85+
86+
| exit | verdict | what you do |
87+
| --- | --- | --- |
88+
| `0` | valid dispatch, your scope | Go to step 2. The printed block is your brief: issue, task path, pack, task, model, outcome ceiling, `executionTimeout`. |
89+
| `13` | CCR trigger, issue named, body needed | Fetch **the printed issue and only it** over MCP — its body and its current labels — write the body verbatim to a file, and re-run: `node <engine>/scheduler/resolve-dispatch.mjs self --issue-body-file <path> --issue-labels <csv>`. Then act on *that* run's exit code. |
90+
| `10` | invalid dispatch | It never runs. Comment the printed `reason`, remove the ready label, add `needs-human`, end the session. |
91+
| `11` | not yours | The trigger label is the *other* executor's, no ready label at all, or the issue no longer carries a ready label (another session already claimed it). **Stop**: change nothing, comment nothing, end the session. |
92+
| `12` | no trigger at all | **Stop the session immediately.** See below. |
93+
| `2`, `1` | bad invocation, internal fault | Comment what you saw, add `needs-human` if you know the issue, end the session. Do not proceed on a guess. |
94+
95+
**State the issue number before you act**, so everything after this has one
96+
unambiguous subject. Run that issue and nothing else: do **not** list, claim,
97+
or process any other open issue — not under your ready label, and not under
98+
the other one.
99+
100+
**Why one issue, and never a sweep.** One scheduler run files every due task's
101+
dispatch within a couple of seconds, each already carrying its ready label, so
102+
**one run emits one label event per issue and starts one session per event**.
103+
An executor that also swept its siblings had every one of those sessions build
104+
the same N-issue work list and race over it — and the step-2 claim cannot
105+
prevent that, because all the sessions read the list before any of them claim.
106+
That ran the same dispatch two and three times over: duplicate tracker issues,
107+
duplicate bug reports, duplicate PRs making the same changes. One session, one
108+
issue, and the concurrency is safe by construction.
63109

64110
A dispatch whose label event never landed is **not yours to rescue**. The
65111
scheduler re-arms it in code on its next hourly run (`dispatch.mjs`
66112
`rearmDispatchIssues`) and escalates it to `needs-human` if it stays unrun past
67113
~2 of its scheduling periods. Leave it alone.
68114

69-
*If, and only if, you genuinely cannot determine which issue triggered this
70-
session*, take the **single oldest** open issue under your ready label —
71-
`ready-for-agent` for a self session, `ready-for-agent-fleet` for a fleet
72-
session — and run that one alone. Never take more than one, and never take an
73-
issue under the other ready label; that is the other executor's to run.
74-
75-
2. **Validate deterministically before any judgment.** Run
76-
`node <engine>/scheduler/validate-dispatch.mjs <issue-number>` (`<engine>` is
77-
`.claudinite/shared/engine` in a consumer, `engine` in the canon — see Engine
78-
command paths above). It checks in code that the first line is a legal task path
79-
(`packs/<pack>/tasks/<task>/task.md`, optionally under a
80-
`.claudinite/shared/` or `.claudinite/local/` prefix — the canon's own packs
81-
are root-relative, a consumer's are under its mount), the file exists at HEAD,
82-
its pack is declared, and its `task.mjs` sibling parses to a valid declaration;
83-
it prints the resolved **model**, **outcome** ceiling, and the task's
84-
**executionTimeout** (seconds).
85-
- Invalid → comment naming what failed, remove the ready label, add
86-
`needs-human`, and end the session. A forged or mangled dispatch never runs.
87-
88-
3. **Claim the issue — read, swap, then re-read to confirm you won.** The same
115+
**Exit 12 — stop the session. There is no fallback.** The trigger is how you
116+
know which issue is yours. Without one you do not have a dispatch to run, and
117+
**selecting one yourself is forbidden**: do not list the queue, do not take the
118+
oldest, do not take *any*. Run nothing, change nothing, comment nothing, end
119+
the session.
120+
121+
This is not caution for its own sake — a session that picks its own issue is
122+
the duplicate-execution bug reached from the other direction. One scheduler run
123+
files every due dispatch seconds apart, so **every** session that cannot name
124+
its trigger builds the *same* list and races the others over it; the step-2
125+
claim cannot save them, because they all read the list before any of them
126+
claims. That is exactly how #772 was claimed twice, one second apart, on
127+
2026-07-28. Stopping is cheap: the scheduler re-arms an unrun dispatch on its
128+
next hourly pass and escalates it to `needs-human` if it stays unrun. Guessing
129+
is not.
130+
131+
An exit 12 in a session that *was* triggered by a label event means the trigger
132+
did not reach the shell — a real defect worth a human noticing. Say so plainly
133+
in your final message rather than working around it.
134+
135+
2. **Claim the issue — read, swap, then re-read to confirm you won.** The same
89136
issue can still be labeled twice (a re-arm that overlapped a slow session, a
90137
human re-applying the label), so the claim is a lease you must verify, not a
91138
write you may assume. GitHub has no compare-and-swap on labels; these three
@@ -102,22 +149,25 @@ so use `engine/scheduler/`.
102149
session without dispatching** — do not remove `agent-running` (the winner
103150
is running behind it) and do not converge the issue.
104151

105-
Only past step 3.3 may you dispatch anything.
152+
Only past step 2.3 may you dispatch anything.
106153

107-
4. **Dispatch a subagent at the declared model.** The subagent reads the
154+
3. **Dispatch a subagent at the declared model.** The subagent reads the
108155
task file (`task.md`) and follows it exactly. The issue's **Context** section
109156
is **binding scope** — never re-decide or widen it: if the precondition ruled
110157
something out, it stays out. **Give the subagent its run bound**: tell it
111158
plainly *"you have N minutes (this task's `executionTimeout`); if you exceed
112159
it, stop, comment what's done, and converge this issue to `needs-human` rather
113160
than pressing on."* This is best-effort — there is no platform wall-clock kill
114161
for this session (agent-preprocessing DESIGN §6) — so the value comes from the
115-
**task declaration** printed by validate-dispatch, never from the issue body.
116-
117-
5. **Verify the outcome in code, then converge — then stop.** Determine what the
118-
run did to pull requests and check it against the ceiling with
119-
`verify-outcome.mjs` — a `none` task that opened a PR, or an `open-pr` task
120-
that merged one, **fails the run**. Then:
162+
**task declaration** printed by step 1, never from the issue body.
163+
164+
4. **Verify the outcome in code, then converge — then stop.** The declared
165+
`expected_outcome` is a **ceiling, not a target**: it is the most a task may do,
166+
and **"no change" is always legal** — a run that found nothing worth changing is
167+
a success, never a reason to manufacture work. Determine what the run did to
168+
pull requests and check it against that ceiling with `verify-outcome.mjs` — a
169+
`none` task that opened a PR, or an `open-pr` task that merged one, **fails the
170+
run**. Then:
121171
- Success within ceiling → comment the result and **close** the issue.
122172
- Failure (task failed, or ceiling violated) → comment naming what failed,
123173
remove `agent-running`, add `needs-human`. Do not close.

.claudinite/shared/engine/scheduler/hash-minute.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const MINUTE_MAX = 50;
2020
const BAND = MINUTE_MAX - MINUTE_MIN + 1; // 41
2121

2222
// FNV-1a over the lowercased full name → a minute in [MINUTE_MIN, MINUTE_MAX].
23-
// Same hash family as routines/fleet/schedule.mjs's weekday bucket: deterministic,
23+
// Same hash family as the retired central planner's weekday bucket: deterministic,
2424
// well-spread, and Math.imul keeps the multiply in 32-bit range. Keyed on the repo
2525
// full name ("owner/repo") so the value is stable across re-vendors and re-derivable
2626
// anywhere the name is known.

0 commit comments

Comments
 (0)