Skip to content

Commit 4f66449

Browse files
missingbulbclaude
andauthored
Re-vendor Claudinite mount: close the stale-executor duplicate-dispatch hazard (#769)
This repo was carrying a 75-line `engine/scheduler/executor.md` — the copy as it stood at the stamped ref 61b90ee (#413, the commit that first shipped executor.md to consumers). Every other member of the fleet had since moved on; we had not. The gap was not cosmetic: - the **one-session/one-issue** concurrency guard was missing, so a run was free to sweep every due task instead of running only the issue that triggered it; and - the **earliest-claim-wins** tie-break was missing, so two sessions that both claimed the same issue would both proceed. Together those are precisely the guards that stop duplicate dispatch. Without them concurrent scheduler runs can file duplicate tracker issues, duplicate bug reports, and duplicate PRs making the same change — a live correctness hazard, not latent drift. Re-vendoring from current canon (526c991) brings executor.md to its present 188-line form and lands `engine/scheduler/resolve-dispatch.mjs` alongside it — the shell executor.md's step 1 now invokes. The two ship as one unit in the vendor set, so the mount stays import-closed. Stamp: 61b90ee -> 526c991. 189 files vendored (mount 192 -> 189; the net drop is the retired tidy-repo/repo-tidy task, not a truncation). Stamp lineage was checked before vendoring: 61b90ee is a genuine ancestor of origin/main, so the stamp was honest-but-stale and needed no correction. `converge-wiring.mjs` additionally corrected the scheduler workflow: `actions:` read -> write (creating a workflow_dispatch event needs write; `read` silently 403s the POST) and added the `report-failure` escalation job. No stamp fields changed and the cron minute was re-derived unchanged. check_the_world: 0 blocking before and after. Advisories 11 -> 10 — the refreshed wiring resolved `gha/scheduled-failure-escalation`. Nothing new was introduced and nothing had to be forced or suppressed. Refs missingbulb/Claudinite#394 Claude-Session: https://claude.ai/code/session_01LgokEeqgbFvhbLqbYwxHgx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8bfa43d commit 4f66449

88 files changed

Lines changed: 2095 additions & 1320 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
@@ -32,7 +32,7 @@
3232
"delivery": "auto-merge"
3333
},
3434
"claudinite": {
35-
"updated": "2026-07-23T16:35:48.146Z",
36-
"ref": "61b90ee879fa82a93ad42268dc3c4a81fa716b6c"
35+
"updated": "2026-07-27T09:51:24.654Z",
36+
"ref": "526c9910fb0dc276db293e64b8b16b57bc90f565"
3737
}
3838
}

.claudinite/shared/engine/checks/helpers/active-migrations.mjs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,27 @@ import { fileURLToPath } from 'node:url';
1313
// canonical shapes, no tolerance needed. The full registry
1414
// (migrations/registry.mjs) builds on this same surface canon-side.
1515
export const MIGRATIONS_SUBDIR = 'active_migrations';
16-
const specsDir = join(dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))), 'migrations', MIGRATIONS_SUBDIR); // <canon>/engine/checks/helpers/
16+
// The archive of migrations past their TTL (per-project-scheduling redesign): kept
17+
// CANON-ONLY (never vendored — a project up to speed on migrations carries few or
18+
// none), so a dormant project still BACKFILLS from it when baselining applies
19+
// migrations out of the fresh canon clone. `active_migrations` holds the recent
20+
// (within-TTL) records that ship in the mount and drive check-tolerance;
21+
// `migrations-old` holds the aged records that still APPLY but no longer tolerate.
22+
export const MIGRATIONS_OLD_SUBDIR = 'migrations-old';
23+
const migrationsRoot = join(dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))), 'migrations'); // <canon>/engine/checks/helpers/
24+
const specsDir = join(migrationsRoot, MIGRATIONS_SUBDIR);
25+
const oldSpecsDir = join(migrationsRoot, MIGRATIONS_OLD_SUBDIR);
1726
export const isSpec = (f) => f.endsWith('.mjs') && !f.endsWith('.test.mjs');
27+
const readSpecs = (d) => { try { return readdirSync(d).filter(isSpec).sort(); } catch { return []; } };
1828
// Tolerant of an absent/empty folder — a vendored consumer, or a canon
1929
// checkout after every record has retired.
20-
export const specFiles = () => { try { return readdirSync(specsDir).filter(isSpec).sort(); } catch { return []; } };
30+
export const specFiles = () => readSpecs(specsDir);
31+
// The aged (archived) records — canon-only, so this is empty in any vendored
32+
// consumer mount. Used by the APPLY/backfill path (registry.loadMigrations), NOT
33+
// by check-tolerance: a migration's legacy tolerance ENDS when it ages out of
34+
// active_migrations (all up-to-date repos converged within the TTL), while its
35+
// apply logic persists here for a dormant project's backfill.
36+
export const oldSpecFiles = () => readSpecs(oldSpecsDir);
2137

2238
// True while a migration whose file name carries `slug` is still present — a
2339
// check consults it to know whether an in-flight transition's legacy shape is

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

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { join, resolve, sep } from 'node:path';
44
import { parseEntries } from './session-transcript.mjs';
55
import { SHARED_SUBDIR, packEntryId } from '../../pack_loader/pack-registry.mjs';
66

7-
function sh(root, cmd, args, { allowFail = false, input = undefined } = {}) {
8-
const r = spawnSync(cmd, args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, input });
7+
function sh(root, cmd, args, { allowFail = false, input = undefined, timeout = undefined } = {}) {
8+
const r = spawnSync(cmd, args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, input, timeout });
99
if (r.status !== 0 && !allowFail) {
1010
throw new Error(`${cmd} ${args.join(' ')} failed (${r.status}): ${r.stderr}`);
1111
}
@@ -22,6 +22,31 @@ function resolveBaseRef(root) {
2222
return null;
2323
}
2424

25+
const FETCH_TIMEOUT_MS = 8000;
26+
27+
// A remote-tracking base ref is only as fresh as the last fetch, and a cloud session's
28+
// clone freezes it at container-creation time — so every commit the base branch gained
29+
// since lands inside `mergeBase..HEAD` and gets billed to the work. That is a wrong
30+
// verdict, not a stale one: the delta rules (squash-merge-history above all, a *blocking*
31+
// rule) report other people's commits as introduced by this change, and `--changed`
32+
// widens to files the change never touched. Refreshing the ref once per run is what makes
33+
// "the work" mean the work.
34+
//
35+
// Best-effort by construction — no network, no remote, a lock held, a slow server: the
36+
// fetch fails or times out and the run continues against the ref as it stands, exactly as
37+
// before this existed. It writes ONLY the remote-tracking ref (explicit refspec, no tags):
38+
// no local branch, no index, no working tree, nothing the session could be surprised by.
39+
// Set CLAUDINITE_CHECKS_NO_FETCH=1 to skip it (sealed sandboxes, or to pin a base).
40+
function refreshBaseRef(root, ref) {
41+
if (!ref || process.env.CLAUDINITE_CHECKS_NO_FETCH === '1') return;
42+
const m = /^([^/]+)\/(.+)$/.exec(ref);
43+
if (!m) return; // a local branch is already as current as the checkout — nothing to fetch
44+
const [, remote, branch] = m;
45+
sh(root, 'git', ['fetch', '--quiet', '--no-tags', remote,
46+
`+refs/heads/${branch}:refs/remotes/${remote}/${branch}`],
47+
{ allowFail: true, timeout: FETCH_TIMEOUT_MS });
48+
}
49+
2550
function lines(out) {
2651
return (out || '').split('\n').filter(Boolean);
2752
}
@@ -253,9 +278,15 @@ export function loadConfig(root) {
253278
export function buildContext({ root, mode = 'changed', baseOverride = null, transcriptPath = null }) {
254279
root = resolve(root);
255280
const baseRef = baseOverride || resolveBaseRef(root);
281+
refreshBaseRef(root, baseRef);
256282
const mergeBase = baseRef ? (gitTry(root, 'merge-base', 'HEAD', baseRef) || '').trim() || null : null;
257283
// Diffing against HEAD keeps uncommitted work in scope even when no base branch resolves.
258284
const diffBase = mergeBase || 'HEAD';
285+
// Is this commit already on the base branch? `--is-ancestor` exits non-zero for "no",
286+
// which gitTry surfaces as null. No base ref to compare against ⇒ nothing is known to
287+
// be on it, so nothing is filtered out.
288+
const onBaseBranch = (sha) =>
289+
!!baseRef && gitTry(root, 'merge-base', '--is-ancestor', sha, baseRef) !== null;
259290

260291
const tracked = lines(gitTry(root, 'ls-files'));
261292
const untracked = lines(gitTry(root, 'ls-files', '--others', '--exclude-standard'));
@@ -394,13 +425,20 @@ export function buildContext({ root, mode = 'changed', baseOverride = null, tran
394425
// chain since the merge-base with the base branch (the squash-only effect
395426
// check, scoped to the work). Empty when no base resolves or the branch is
396427
// even with it; pre-existing merges already on the base are out of range.
428+
//
429+
// The range alone trusts the merge-base to be right, and it isn't always: a
430+
// shallow clone's grafted boundary or a criss-cross history can hand back a
431+
// merge-base that sweeps the base branch's own merges into the range. So each
432+
// candidate is confirmed against the base ref itself — a merge already on the
433+
// base branch was never introduced here, whatever the range says. Offline and
434+
// exact, and it holds when refreshBaseRef couldn't run.
397435
introducedMergeCommits() {
398436
if (!mergeBase) return [];
399437
const out = gitTry(root, 'log', '--merges', '--first-parent', '--format=%h %s', `${mergeBase}..HEAD`);
400438
return lines(out).map((l) => {
401439
const i = l.indexOf(' ');
402440
return { sha: l.slice(0, i), subject: l.slice(i + 1) };
403-
});
441+
}).filter(({ sha }) => !onBaseBranch(sha));
404442
},
405443

406444
// Fixed-string search across tracked files; git grep exits 1 on no match.

.claudinite/shared/engine/checks/helpers/work.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class Work {
6969
get changedFiles() { return this.ctx.changedFiles; }
7070
get tracked() { return this.ctx.tracked; }
7171
read(path) { return this.ctx.read(path); }
72+
readBase(path) { return this.ctx.readBase(path); }
7273
exists(path) { return this.ctx.exists(path); }
7374
packConfig(id) { return this.ctx.config?.packConfig?.[id]; }
7475

.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/converge-wiring.mjs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,36 @@ export const CLAUDE_MD = 'CLAUDE.md';
3535
// The whole line (and its trailing newline) is removed wherever it appears.
3636
const CORPUS_IMPORT_RE = /^.*@\.claudinite\/shared\/CLAUDE\.md.*\n?/m;
3737

38+
// The repo Actions secrets its scheduled tasks declare via `required_secrets`,
39+
// deduped and sorted. Async because task discovery is; pure otherwise.
40+
export async function declaredSecrets(root, config) {
41+
const { discoverTasks } = await import('./discover.mjs');
42+
const { tasks } = await discoverTasks(root, config);
43+
return [...new Set(tasks.flatMap((t) => t.decl?.required_secrets ?? []))].sort();
44+
}
45+
46+
// Stamp the declared secrets into the scheduler workflow's engine step, beside
47+
// GITHUB_TOKEN. This is the whole delivery mechanism: GitHub Actions requires each
48+
// secret to be named statically in the workflow, and a task's `required_secrets` is
49+
// exactly that list — so the wiring converge writes it, and a worker then reads
50+
// `process.env.<NAME>` like any other environment variable. No bundle, no parsing,
51+
// no engine-side selection. Regenerated from the stub each converge, so the list
52+
// tracks the declarations rather than accumulating.
53+
export function withDeclaredSecrets(stubText, names = []) {
54+
if (!names.length) return stubText;
55+
const lines = names.map((n) => ` ${n}: \${{ secrets.${n} }}`).join('\n');
56+
return stubText.replace(/^(\s*GITHUB_TOKEN: \$\{\{ github\.token \}\})$/m, `$1\n${lines}`);
57+
}
58+
3859
// Re-converge the scheduler workflow to the vendored stub, with the cron minute set
3960
// to this repo's stable hashed value (never guessed — hash-minute.mjs, a pure
40-
// function of the full name, so re-vendors and this convergence agree). `stubText`
41-
// is the vendored stub's content (the caller reads it from the mount). Returns true
42-
// when the file was written (absent, or drifted from the target).
43-
export function convergeSchedulerWorkflow(root, fullName, stubText) {
44-
const target = stubText.replace(/cron:\s*'[^']*'/, `cron: '${hashedCron(fullName)}'`);
61+
// function of the full name, so re-vendors and this convergence agree) and the
62+
// declared `required_secrets` stamped into the engine step's env. `stubText` is the
63+
// vendored stub's content (the caller reads it from the mount). Returns true when
64+
// the file was written (absent, or drifted from the target).
65+
export function convergeSchedulerWorkflow(root, fullName, stubText, secretNames = []) {
66+
const target = withDeclaredSecrets(stubText, secretNames)
67+
.replace(/cron:\s*'[^']*'/, `cron: '${hashedCron(fullName)}'`);
4568
const path = join(root, SCHEDULER_WORKFLOW);
4669
const current = existsSync(path) ? readFileSync(path, 'utf8') : null;
4770
if (current === target) return false;
@@ -92,9 +115,9 @@ export function removeRetiredCorpusImport(root) {
92115

93116
// Converge every wiring surface, returning a flat summary of what changed (empty
94117
// when the repo was already converged). `stubText` is the vendored scheduler stub.
95-
export function convergeWiring(root, fullName, stubText) {
118+
export function convergeWiring(root, fullName, stubText, secretNames = []) {
96119
const changed = [];
97-
if (convergeSchedulerWorkflow(root, fullName, stubText)) changed.push(SCHEDULER_WORKFLOW);
120+
if (convergeSchedulerWorkflow(root, fullName, stubText, secretNames)) changed.push(SCHEDULER_WORKFLOW);
98121
const hooks = ensureHooks(root);
99122
for (const h of hooks.added) changed.push(`hook:${h}`);
100123
if (removeRetiredCorpusImport(root)) changed.push(`removed retired ${CLAUDE_MD} corpus import`);
@@ -111,7 +134,9 @@ async function main() {
111134
const root = process.env.CLAUDINITE_REPO_ROOT || process.cwd();
112135
const stubPath = join(root, '.claudinite/shared/engine/scheduler/stubs/claudinite-scheduler.yml');
113136
if (!existsSync(stubPath)) { console.error(`converge-wiring: vendored stub not found at ${stubPath}`); process.exit(1); }
114-
const { changed, error } = convergeWiring(root, fullName, readFileSync(stubPath, 'utf8'));
137+
const { loadConfig } = await import('../checks/helpers/repo-context.mjs');
138+
const secretNames = await declaredSecrets(root, loadConfig(root));
139+
const { changed, error } = convergeWiring(root, fullName, readFileSync(stubPath, 'utf8'), secretNames);
115140
if (error) console.log(`! ${error}`);
116141
console.log(changed.length ? `converge-wiring: ${changed.join(', ')}` : 'converge-wiring: already converged');
117142
}

0 commit comments

Comments
 (0)