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

@ -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();