feat(campaign): refresh-tokens live cross-repo token sweep (v5.9 B2b-2) [skip-docs]

The IO half of the machine-wide token roll-up (B2a shipped the pure data model).
New campaign-write-cli subcommand 'refresh-tokens': for every tracked repo it runs
readActiveConfig → buildManifest → splitManifestByOwnership, stores each repo's
per-repo delta via setRepoTokens, and captures the HOME-derived shared global layer
ONCE (from the first repo that reads cleanly) via setSharedGlobal.

Capturing shared once is the structural counted-once guard: a repo that fails to
read is recorded in 'skipped' and its delta omitted, never aborting the sweep.
Idempotent — a re-sweep replaces (setters overwrite), never accumulates. Determinism
unchanged: the clock is still read only via --reference-date.

TDD: 3 integration tests first (RED → GREEN) against a fixture with a dominant global
CLAUDE.md + two repos. Counted-once guard asserts each stored delta stays a small
fraction of the shared layer (a double-fold would make each delta EXCEED shared).
Caught a per-repo-MCP misclassification (fixed in 82f881a) + a test-side shape slip
(rollUp flattens its token buckets to numbers; stored summaries keep {tokens,count}).
Suite 1198 green (1195 + 3). Manifest + all frozen snapshots untouched.

[skip-docs]: internal sweep CLI (campaign plumbing), no user-facing surface yet. The
rendered token-bill in commands/campaign.md + CLAUDE.md rows land in B2b-3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 17:23:15 +02:00
commit d664b70520
2 changed files with 170 additions and 7 deletions

View file

@ -26,10 +26,20 @@
* node campaign-write-cli.mjs set-status <path> <status>
* [--findings '<json>'] [--session <id>]
* [--ledger-file <p>] [--reference-date <d>]
* node campaign-write-cli.mjs refresh-tokens [--ledger-file <p>] [--reference-date <d>]
* (all accept [--output-file <p>] to write the result payload to a file instead of stdout)
*
* Exit codes: 0 = write performed, 1 = advisory no-op (init when already initialized),
* 3 = error (unknown subcommand, bad args, invalid status, untracked repo, no/corrupt ledger).
* `refresh-tokens` is the live cross-repo token sweep (v5.9 B2b): for every tracked
* repo it runs the manifest's always-loaded accounting (readActiveConfig buildManifest)
* and splits each source into the shared global layer vs the repo's per-repo delta
* (splitManifestByOwnership). The shared layer is HOME-derived and identical across
* repos, so it is captured ONCE (from the first successful read) and stored at the
* ledger root; each repo gets only its delta. This is the IO half of the machine-wide
* token roll-up whose pure data model shipped in B2a.
*
* Exit codes: 0 = write performed (or no repos to sweep, a benign no-op), 1 = advisory
* no-op (init when already initialized), 3 = error (unknown subcommand, bad args,
* invalid status, untracked repo, no/corrupt ledger).
*/
import { resolve } from 'node:path';
@ -38,11 +48,15 @@ import {
createLedger,
addRepo,
setRepoStatus,
setSharedGlobal,
setRepoTokens,
rollUp,
loadLedger,
saveLedger,
defaultLedgerPath,
} from './lib/campaign-ledger.mjs';
import { readActiveConfig } from './lib/active-config-reader.mjs';
import { buildManifest, splitManifestByOwnership } from './manifest.mjs';
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
@ -88,7 +102,7 @@ async function emit(payload, outputFile, exitCode) {
async function main() {
const { subcommand, rest, flags } = parseArgs(process.argv.slice(2));
if (!subcommand) fail('a subcommand is required: init | add | set-status');
if (!subcommand) fail('a subcommand is required: init | add | set-status | refresh-tokens');
const ledgerPath = resolve(flags.ledgerFile || defaultLedgerPath());
if (flags.referenceDate && !DATE_RE.test(flags.referenceDate)) fail('--reference-date must be YYYY-MM-DD');
@ -183,7 +197,49 @@ async function main() {
);
}
fail(`unknown subcommand "${subcommand}" — expected init | add | set-status`);
if (subcommand === 'refresh-tokens') {
const ledger0 = await loadOrFail(ledgerPath);
if (ledger0 === null) fail(`no ledger at ${ledgerPath} — run "init" or "add" first`);
let ledger = ledger0;
const swept = [];
const skipped = [];
// The shared global layer is HOME-derived and identical across every repo, so it
// is captured ONCE (from the first repo that reads cleanly) and stored at the
// ledger root — the structural guard against the historic shared-layer double-count.
let sharedSummary = null;
for (const repo of ledger0.repos) {
let split;
try {
const activeConfig = await readActiveConfig(repo.path, { verbose: false });
const { sources } = buildManifest(activeConfig);
split = splitManifestByOwnership(sources);
} catch (err) {
skipped.push({ path: repo.path, reason: err.message });
continue;
}
if (sharedSummary === null) sharedSummary = split.shared;
ledger = setRepoTokens(ledger, repo.path, split.delta, { now });
swept.push(repo.path);
}
if (sharedSummary !== null) ledger = setSharedGlobal(ledger, sharedSummary, { now });
const written = swept.length > 0;
if (written) await saveLedger(ledgerPath, ledger);
return emit(
{
status: 'ok', action: 'refresh-tokens', written, ledgerPath,
swept, skipped, sharedGlobal: ledger.sharedGlobal ?? null, rollUp: rollUp(ledger),
},
flags.outputFile,
0,
);
}
fail(`unknown subcommand "${subcommand}" — expected init | add | set-status | refresh-tokens`);
}
const isDirectRun =

View file

@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { resolve, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
import { readFileSync, mkdtempSync, existsSync } from 'node:fs';
import { readFileSync, mkdtempSync, existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import {
loadLedger,
@ -19,9 +19,13 @@ const NOW = '2026-06-22';
// Every test passes an explicit --ledger-file in a temp dir + an injected --reference-date,
// so the write-CLI never touches the real default path under HOME and never reads the clock.
// The suite is hermetic + deterministic by construction.
function runWrite(args) {
function runWrite(args, env = {}) {
try {
const stdout = execFileSync('node', [CLI, ...args], { encoding: 'utf-8', timeout: 15000 });
const stdout = execFileSync('node', [CLI, ...args], {
encoding: 'utf-8',
timeout: 30000,
env: { ...process.env, ...env },
});
return { status: 0, stdout };
} catch (err) {
return { status: err.status, stdout: err.stdout || '', stderr: err.stderr || '' };
@ -184,6 +188,109 @@ describe('campaign-write-cli — set-status', () => {
});
});
describe('campaign-write-cli — refresh-tokens (live cross-repo sweep, v5.9 B2b)', () => {
// Fixture: a fake HOME with a LARGE global CLAUDE.md (the shared layer, made to
// dominate so the counted-once guard is meaningful) + two repos OUTSIDE that HOME,
// each with a small project CLAUDE.md (the per-repo delta). repos sit beside the
// fake home so the cascade walk never picks up the global file as a project file.
function buildSweepFixture() {
const root = mkdtempSync(join(tmpdir(), 'camp-sweep-'));
const fakeHome = join(root, 'home');
mkdirSync(join(fakeHome, '.claude'), { recursive: true });
writeFileSync(
join(fakeHome, '.claude', 'CLAUDE.md'),
`# Global user config\n\n${'Shared instruction line that every repo pays for. '.repeat(120)}\n`,
);
const repoA = join(root, 'repo-a');
const repoB = join(root, 'repo-b');
const bodies = [
[repoA, '# Repo A\n\nProject A note.\n'],
[repoB, '# Repo B\n\nProject B has a little more local text here.\n'],
];
for (const [repo, body] of bodies) {
mkdirSync(join(repo, '.git'), { recursive: true });
writeFileSync(join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n');
writeFileSync(join(repo, 'CLAUDE.md'), body);
}
return { root, fakeHome, repoA, repoB };
}
it('sweeps every tracked repo, counts the shared layer ONCE, attributes deltas per repo', async () => {
const { root, fakeHome, repoA, repoB } = buildSweepFixture();
try {
const file = join(root, 'ledger.json');
runWrite(['add', repoA, repoB, '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome });
const { status, stdout } = runWrite(
['refresh-tokens', '--ledger-file', file, '--reference-date', NOW],
{ HOME: fakeHome },
);
assert.equal(status, 0, stdout);
const out = JSON.parse(stdout);
assert.equal(out.action, 'refresh-tokens');
assert.equal(out.written, true);
assert.deepEqual(out.swept.slice().sort(), [resolve(repoA), resolve(repoB)].sort());
assert.deepEqual(out.skipped, []);
// NOTE on shapes (B2a contract): the STORED summaries — out.sharedGlobal and each
// repo.tokens — carry {always:{tokens,count}}, but rollUp().tokens flattens its
// sharedGlobal/perRepoDelta/machineWide buckets to {always:<number>} (bucketTokens).
const t = out.rollUp.tokens;
assert.ok(t.sharedGlobal.always > 0, 'shared global layer should be non-empty');
assert.equal(t.reposWithTokens, 2);
assert.equal(t.byRepo.length, 2);
// Persisted ledger carries the new token fields and stays schema-valid.
const ledgerOut = await loadLedger(file);
assert.ok(validateLedger(ledgerOut).valid, validateLedger(ledgerOut).errors.join('; '));
assert.deepEqual(ledgerOut.sharedGlobal, out.sharedGlobal);
// Counted-ONCE guard: each repo's stored delta must NOT contain the dominant
// shared global CLAUDE.md. Had the sweep folded shared into the per-repo tokens,
// each delta would EXCEED the shared layer instead of being a small fraction.
for (const repo of ledgerOut.repos) {
assert.ok(repo.tokens, `${repo.name} should have tokens`);
assert.ok(
repo.tokens.always.tokens < t.sharedGlobal.always,
`${repo.name} delta (${repo.tokens.always.tokens}) must be < shared (${t.sharedGlobal.always})`,
);
}
// rollUp invariant: machineWide = shared (once) + sum of per-repo deltas.
const deltaSum = ledgerOut.repos.reduce((s, r) => s + r.tokens.always.tokens, 0);
assert.equal(t.perRepoDelta.always, deltaSum);
assert.equal(t.machineWide.always, t.sharedGlobal.always + deltaSum);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it('is idempotent — a second sweep replaces, never accumulates', async () => {
const { root, fakeHome, repoA, repoB } = buildSweepFixture();
try {
const file = join(root, 'ledger.json');
runWrite(['add', repoA, repoB, '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome });
const first = JSON.parse(
runWrite(['refresh-tokens', '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome }).stdout,
);
const second = JSON.parse(
runWrite(['refresh-tokens', '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome }).stdout,
);
assert.deepEqual(second.rollUp.tokens, first.rollUp.tokens);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it('exits 3 when no ledger exists yet', () => {
const dir = newDir();
const { status } = runWrite([
'refresh-tokens', '--ledger-file', join(dir, 'nope.json'), '--reference-date', NOW,
]);
assert.equal(status, 3);
});
});
describe('campaign-write-cli — determinism + --output-file', () => {
it('stamps updatedDate from --reference-date and writes the payload to --output-file', async () => {
const dir = newDir();