Skip to content

Commit 57ef4e4

Browse files
github-actions[bot]claudinite[bot]claude
authored
Claudinite maintenance (#793)
* Claudinite maintenance: converge vendored mount, wiring, and migration notes * Baselining: fix gcec pack manifest to match pack-schema.mjs check_the_world flagged the gcec local pack's manifest for three schema-conformance blockers: no ruleRoutingGuidance (required field), "rules" (not a manifest field — renamed to worldRules, since all three gcec rules audit static repo state), and "run_daily" (dead legacy field, unused anywhere in the engine). No pending agentic migration note this cycle (2026-07-19-pack-independence landed before the prior stamp, so it was already handled in an earlier cycle; this repo's gcec pack composes no barrier from the shared engine either way, so it would have been a no-op). Advances claudinite.updated to reflect this cycle's judgment pass. Refs #794 * Baselining: mark dispatch issue reference (#794) Ties this maintenance cycle's judgment pass (gcec pack manifest fix, stamp advance in 66139bb) to its dispatch issue, satisfying this repo's task-lifecycle check. --------- Co-authored-by: claudinite[bot] <claudinite@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent b5e68ac commit 57ef4e4

69 files changed

Lines changed: 1804 additions & 469 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,8 +32,8 @@
3232
"delivery": "auto-merge"
3333
},
3434
"claudinite": {
35-
"updated": "2026-07-29T03:46:43.872Z",
36-
"ref": "3dea09ab5a3bcea11eee969cbe6340db725af1ac"
35+
"updated": "2026-07-30T03:45:18.000Z",
36+
"ref": "29e843cd69f355e2cf41aea3b91197aa5e5030da"
3737
},
3838
"badges": {
3939
"readme": "auto"

.claudinite/local/packs/gcec/pack.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ import npmTestGlobCoverage from './npm-test-glob-coverage.mjs';
1616
// here, not from the canon skills tree.
1717
export default {
1818
id: 'gcec',
19+
ruleRoutingGuidance: {
20+
belongs: "this project's standing rules — owner commands, testing invariants, codebase gotchas, extractor-pipeline rules, architecture rules of the road, capture policy",
21+
excludes: 'general software-engineering practices and portable procedures — those live in the vendored Claudinite canon, not here',
22+
},
1923
detect: null,
2024
marker: null,
2125
prose: 'RULES.md',
22-
rules: [testOfflineListSync, customSourcesFlat, npmTestGlobCoverage],
26+
worldRules: [testOfflineListSync, customSourcesFlat, npmTestGlobCoverage],
2327
skills: ['snapshot-approval', 'merge-and-ci', 'testing-guide'],
24-
run_daily: [],
2528
};

.claudinite/shared/engine/checks/check_the_world.mjs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { writeFileSync, existsSync } from 'node:fs';
1818
import { join } from 'node:path';
1919
import { buildContext } from './helpers/repo-context.mjs';
2020
import { discoverPacks, resolveDeclaredPacks } from '../pack_loader/pack-registry.mjs';
21-
import { runActivePackRules, contributedRules } from './run-active-pack-rules.mjs';
21+
import { runActivePackRules, packRules } from './run-active-pack-rules.mjs';
2222
import { reportFindings } from './report-findings.mjs';
2323

2424
const configError = (what, fix) => ({
@@ -33,12 +33,7 @@ const root = value('--root') || process.cwd();
3333

3434
if (has('--list')) {
3535
const { packs } = await discoverPacks({ localRoot: root });
36-
const rules = [
37-
...packs.flatMap((p) => p.rules ?? []),
38-
...packs.flatMap((p) => p.skillChecks ?? []),
39-
...packs.flatMap((p) => contributedRules(p, packs)),
40-
];
41-
for (const r of rules.sort((a, b) => a.id.localeCompare(b.id))) {
36+
for (const r of packRules(packs)) {
4237
console.log(`${r.id}\t${r.severity}\t${r.description}\t${r.doc}`);
4338
}
4439
process.exit(0);

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,35 @@ function vendoredSet(root, files) {
9292
// README — { readme: 'auto' | 'off' }. Materialized explicitly into every member
9393
// (the wiring converge writes it) rather than inferred from absence, so the knob
9494
// sits visibly in the file anyone would open to change it.
95-
export const CONFIG_KEYS = ['packs', 'rules', 'accept', 'sharedConstants', 'packConfig', 'maintenance', 'claudinite', 'taskScheduler', 'badges'];
95+
// `dormant` is the project's own declaration that it is out of the RECURRING work —
96+
// see isDormant below for exactly how much that covers.
97+
export const CONFIG_KEYS = ['packs', 'rules', 'accept', 'sharedConstants', 'packConfig', 'maintenance', 'claudinite', 'taskScheduler', 'badges', 'dormant'];
98+
99+
// Does this project declare itself DORMANT? A project goes dormant when it is
100+
// finished, parked, or simply not being worked on: it should stop paying the
101+
// upkeep of a project that IS being worked on.
102+
//
103+
// What dormancy means, exactly — it is narrow on purpose:
104+
// - NO RECURRING WORK. The vendored scheduler stops before it evaluates
105+
// anything (engine/scheduler/run.mjs), so no task is due, no dispatch issue
106+
// is filed, no agent session is started, and no maintenance PR is opened.
107+
// Nothing scheduled runs "for nothing" on a repo nobody is working on.
108+
// - NO FLEET CEREMONY. Whatever looks at this repo from the OUTSIDE reads the
109+
// same declaration and leaves it alone rather than reporting it as unhealthy —
110+
// a repo TOLD to stop keeping up must not then be nagged for not keeping up.
111+
// Which is why this predicate is exported for a raw declaration too (below).
112+
// - EVERYTHING ELSE STAYS ON. Claudinite is not switched off: the session
113+
// hooks, the checks engine, the mounted skills and pack prose all work
114+
// exactly as before the moment someone opens a session on the repo. Dormancy
115+
// is about unattended upkeep, never about what an interactive session may do.
116+
//
117+
// The predicate reads BOTH shapes deliberately: a raw parsed .claudinite-checks.json
118+
// and the normalized config loadConfig returns. A cross-repo reader fetches another
119+
// repo's declaration over the API with no engine loaded against that tree, and it
120+
// must decide dormancy by the same test that repo's own scheduler used — a second
121+
// notion of dormancy would nag exactly the repos that had already opted out. One
122+
// definition, both sides.
123+
export const isDormant = (config) => config?.dormant === true;
96124

97125
// The keys a `schedule` object may carry, and the canonical weekday vocabulary
98126
// (mirrored from engine/scheduler/slots.mjs WEEKDAYS — kept as a literal here so
@@ -128,7 +156,7 @@ export const PACK_ENTRY_KEYS = ['id', 'config', 'answers', 'rules', 'accept', 'v
128156
// read this one shape regardless of which form the file used.
129157
export function loadConfig(root) {
130158
const path = join(root, '.claudinite-checks.json');
131-
const empty = { packs: [], packEntries: [], rules: {}, accept: [], sharedConstants: [], packConfig: {}, taskScheduler: null, claudinite: null, maintenance: null, badges: null, errors: [] };
159+
const empty = { packs: [], packEntries: [], rules: {}, accept: [], sharedConstants: [], packConfig: {}, taskScheduler: null, claudinite: null, maintenance: null, badges: null, dormant: false, errors: [] };
132160
if (!existsSync(path)) return empty;
133161

134162
let raw;
@@ -267,6 +295,17 @@ export function loadConfig(root) {
267295
}
268296
}
269297

298+
// --- dormant: a plain boolean, validated as one. A string "true", a `{ since }`
299+
// object or a reason left in its place would all read as dormant to a truthiness
300+
// test and as active to this one, and the difference is a whole repo's scheduled
301+
// work — so the wrong TYPE is a settings error, not a value to coerce.
302+
if (raw.dormant !== undefined && typeof raw.dormant !== 'boolean') {
303+
errors.push({
304+
what: `"dormant" must be true or false, got ${JSON.stringify(raw.dormant)}`,
305+
fix: 'set "dormant": true to stop this project\'s recurring work, or remove the key',
306+
});
307+
}
308+
270309
return {
271310
packs,
272311
packEntries,
@@ -288,6 +327,10 @@ export function loadConfig(root) {
288327
claudinite: raw.claudinite ?? null,
289328
maintenance: raw.maintenance ?? null,
290329
badges: raw.badges ?? null,
330+
// Normalized to a boolean rather than passed through: everything downstream
331+
// asks "is this project dormant", and a tri-state (true / false / absent) would
332+
// invite each caller to answer the absent case for itself. Absent is active.
333+
dormant: isDormant(raw),
291334
errors,
292335
};
293336
}

.claudinite/shared/engine/checks/run-active-pack-rules.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ export function contributedRules(pack, fromPacks, onError = null) {
1818
catch (e) { onError?.(e); return []; }
1919
}
2020

21+
// The full rule catalog the given packs carry, id-sorted: a pack's own rules,
22+
// the checks its bundled skills own, and the rules built from contributions
23+
// addressed to it. The ONE place the three sources are summed — `--list`
24+
// prints it and the catalog's own tally guard counts it, so neither can
25+
// disagree with the runner about what rules exist, and a fourth source added
26+
// later lands in both for free.
27+
export function packRules(packs) {
28+
return [
29+
...packs.flatMap((p) => p.rules ?? []),
30+
...packs.flatMap((p) => p.skillChecks ?? []),
31+
...packs.flatMap((p) => contributedRules(p, packs)),
32+
].sort((a, b) => a.id.localeCompare(b.id));
33+
}
34+
2135
// Every finding from the active packs' rules that `includeRule` admits. A rule
2236
// turned `off` in settings is skipped. `onContributeError(pack, err)` is invoked
2337
// when a pack's contributedRules seam throws (the caller decides whether that

.claudinite/shared/engine/hooks/session-end-command.mjs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,21 @@
1111
// conformance checks); a step that writes anything per-session would fire once per
1212
// TURN there. SessionEnd fires once, when the session ends.
1313
//
14-
// BEST EFFORT, and that is enough. A container reclaimed by timeout never fires
15-
// this hook at all, so nothing may depend on it having run: every firing strictly
14+
// BEST EFFORT AS A HOOK, and that is enough there: a container reclaimed by timeout
15+
// never fires it, so nothing may depend on it having run every firing strictly
1616
// enriches the record, every miss leaves exactly the prior behaviour.
1717
//
18+
// AND EXPLICITLY INVOCABLE, which is what makes unattended sessions countable at
19+
// all. An UNATTENDED session — the scheduler's executor, running a dispatch in a
20+
// cloud container nobody is sitting in front of — ends when its container is
21+
// reclaimed, so the hook is exactly the firing that does not happen, and every one
22+
// of those sessions used to leave no record anywhere. So the executor runs this
23+
// runner itself as its last step (engine/scheduler/executor.md), naming the dispatch
24+
// issue in `CLAUDINITE_SESSION_ISSUE`. Steps must therefore tolerate being run
25+
// mid-session: the transcript is complete only up to the invocation, and the later
26+
// hook firing (when it happens at all) is a second event over the same session,
27+
// which the capture step's session-keyed delta already makes safe.
28+
//
1829
// FAIL-SOFT, absolutely. A session must never fail to end because of this hook, so
1930
// every error — a step that throws, a step that hangs, a broken pack registry — is
2031
// swallowed, recorded via hooklog, and exited 0. The per-step bound is a hard kill;
@@ -71,6 +82,12 @@ async function main() {
7182
// to re-derive it. Named for the engine, not for any one step's CLI flags.
7283
...(input.session_id ? { CLAUDINITE_SESSION_ID: String(input.session_id) } : {}),
7384
...(input.transcript_path ? { CLAUDINITE_TRANSCRIPT: String(input.transcript_path) } : {}),
85+
// The issue this session was ABOUT, when its launcher knew one. A hook
86+
// firing never does — nothing tells a SessionEnd hook what the session was
87+
// for — so this is only ever set by an explicit invocation (the executor
88+
// naming its dispatch). Passed through verbatim: core states the fact, and
89+
// a step decides what, if anything, to do with it.
90+
...(process.env.CLAUDINITE_SESSION_ISSUE ? { CLAUDINITE_SESSION_ISSUE: process.env.CLAUDINITE_SESSION_ISSUE } : {}),
7491
},
7592
});
7693
const tail = (run.stdout || run.stderr || '').trim().split('\n').pop() || '';

.claudinite/shared/engine/pack_loader/inject-pack-prose.mjs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,42 @@ try {
2525
// system rather than an explicit @import.
2626
const packs = await loadPacks({ localRoot: projectRoot });
2727

28+
// Nothing active means this repo runs no Claudinite: no prose, and no routing
29+
// table either. The hook stays silent rather than pushing a catalog of the
30+
// corpus into a session that declared none of it.
31+
const active = packs.filter((pack) => isActive(pack, { packs: declared }));
32+
if (!active.length) process.exit(0);
33+
2834
const sections = [];
29-
for (const pack of packs) {
30-
if (!pack.prose || !isActive(pack, { packs: declared })) continue;
35+
for (const pack of active) {
36+
if (!pack.prose) continue;
3137
// Resolve prose off the pack's OWN directory (canon or local_packs), not a
3238
// single shared root — so a local pack's RULES.md is found where it lives.
3339
const prosePath = join(pack.dir, pack.prose);
3440
if (!existsSync(prosePath)) continue;
3541
sections.push(`<!-- pack:${pack.id} -->\n${readFileSync(prosePath, 'utf8').trim()}`);
3642
}
3743

38-
if (sections.length) {
39-
process.stdout.write(
40-
`# Claudinite — active-pack guidance\n\nThe baseline plus the packs this project declares. Deeper per-pack reference (e.g. a pack's release doc) is linked from its prose and read on demand.\n\n${sections.join('\n\n---\n\n')}\n`
41-
);
44+
// The routing table: every pack's own statement of its boundary, so a session
45+
// holding a piece of content (a doc, a rule, a skill) routes it to the pack
46+
// that owns it instead of defaulting into the baseline. Emitted for every pack
47+
// DISCOVERED, not only the active ones — a consumer holds just the packs it
48+
// vendored, so the discovered set is already the set it can route into, and in
49+
// the canon every pack is a legitimate destination whether or not this repo
50+
// declares it. Rows are short by contract (the manifest spec caps each side at
51+
// 20 words — pack-schema.mjs), so the whole table stays a cheap session cost.
52+
const routed = packs.filter((p) => p.ruleRoutingGuidance?.belongs && p.ruleRoutingGuidance?.excludes);
53+
const routingTable = routed.length
54+
? `# Claudinite — where content goes (pack routing)\n\nEach pack states what it owns and what it does not. When a rule, doc, skill or check could live in more than one, this table decides it — and "no pack fits" means a new pack or the project's own \`local_packs/\`, never the baseline by default.\n\n| Pack | Belongs | Does not belong |\n|---|---|---|\n${routed
55+
.map((p) => `| \`${p.id}\`${p.local ? ' (local)' : ''} | ${p.ruleRoutingGuidance.belongs} | ${p.ruleRoutingGuidance.excludes} |`)
56+
.join('\n')}\n`
57+
: '';
58+
59+
if (sections.length || routingTable) {
60+
const guidance = sections.length
61+
? `# Claudinite — active-pack guidance\n\nThe baseline plus the packs this project declares. Deeper per-pack reference (e.g. a pack's release doc) is linked from its prose and read on demand.\n\n${sections.join('\n\n---\n\n')}\n`
62+
: '';
63+
process.stdout.write([guidance, routingTable].filter(Boolean).join('\n---\n\n'));
4264
}
4365
} catch {
4466
// fail soft — a broken loader must never block a session

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readdirSync, existsSync } from 'node:fs';
22
import { join, dirname, resolve } from 'node:path';
33
import { fileURLToPath, pathToFileURL } from 'node:url';
4+
import { validateManifest, normalizeManifest } from './pack-schema.mjs';
45

56
// This module lives at <canon>/engine/pack_loader/; the packs it scans at <canon>/packs/.
67
const canonRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
@@ -140,13 +141,34 @@ async function scanPackDir(dir, { local, subdir }, errors) {
140141
// import needed (the pack owns the interview HYGIENE check; the engine owns
141142
// the manifest-shape validation).
142143
for (const e of packQuestions(mod).errors) errors.push({ ...e, dir: packDir });
143-
const pack = { ...mod, dir: packDir, local };
144+
// The rest of the manifest against the spec (pack-schema.mjs). REPORTED, not
145+
// fatal: a pack whose declaration is incomplete still loads and still runs
146+
// its checks — silently disabling a repo's own rules is a worse failure than
147+
// the one being reported, and the blocking config error is what gets it fixed.
148+
for (const e of validateManifest(mod, { label: `the pack in ${rel}`, skillDirs: skillDirNames(packDir) })) {
149+
errors.push({ ...e, dir: packDir });
150+
}
151+
const pack = { ...normalizeManifest(mod), dir: packDir, local };
144152
pack.skillChecks = await scanSkillChecks(packDir, errors);
145153
out.push(pack);
146154
}
147155
return out;
148156
}
149157

158+
// The skill directory names a pack bundles — the tree side of the manifest's
159+
// `skills` declaration, which the spec holds to it in both directions. Absent or
160+
// unreadable skills/ reads as none: scanSkillChecks reports the unreadable case,
161+
// and the spec must not turn one broken directory into a wall of phantom findings.
162+
function skillDirNames(packDir) {
163+
const skillsRoot = join(packDir, 'skills');
164+
if (!existsSync(skillsRoot)) return [];
165+
try {
166+
return readdirSync(skillsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
167+
} catch {
168+
return [];
169+
}
170+
}
171+
150172
// A pack's skill-owned checks: any <pack>/skills/<skill>/checks.mjs (default
151173
// export = an array of rules). Isolated per import; run gated by the owning
152174
// pack being active, exactly like the pack's own rules — a skill is pack

0 commit comments

Comments
 (0)