board/claims

Source: src/board/claims.ts

One owner per thing, decided here rather than agreed between members.

A board lets a member announce what it is taking. Measured, that is not enough and cannot be made enough: three members read the board within 130ms of each other, were each handed nothing because nobody had posted yet, and all three then claimed the same file. Announcing into a medium that was empty when you looked is a race whatever the prompt says.

So a claim is granted, never declared. First to ask holds it, everyone else is refused and told who holds it, and the refusal is the useful part: it turns contention into somebody to talk to (“ask scout#3, it holds src/parser.ts”) instead of two members doing one job.

The keys are a list, not free text. The same run posted console.ts, src/reporters/console.ts and I will handle src/reporters/console.ts for one file. A lease keyed on what a model writes would have granted all three and arbitrated nothing, so a caller that knows what there is to claim says so, and a key that is not on the list is refused with the list - the same discipline as an agent name that is not on the roster.

Pure data, like ledger.ts next door: no pi, no disk, no bus. Whoever hands the mechanism to a member announces it.

Claims

type

export type Claims = {
	/**
	 * Grants `key` to `member`, or refuses and says who has it.
	 *
	 * Taking what you already hold is granted: it is not contention, and a
	 * member told "you cannot have it, you have it" learns nothing.
	 */
	take(member: string, key: string): TakeOutcome;
	/** Gives `key` up. Only its holder can, and a key nobody holds is `false`. */
	release(member: string, key: string): boolean;
	/**
	 * Everything `member` was holding, given up at once, and what that was.
	 *
	 * For the workflow above, when a member dies or is stopped: claims left
	 * hanging by a member that is gone are work nobody will do and nobody can
	 * take. The keys come back so the run can say which ones, rather than
	 * leaving a reader to notice the gap.
	 */
	releaseAll(member: string): readonly string[];
	/** Who holds `key`, if anyone. */
	owner(key: string): string | undefined;
	/**
	 * What is left to take, when the caller said what there was.
	 *
	 * `undefined` when it did not: a list of what is free cannot be derived from
	 * a list of what is held, and inventing one would be this file guessing at
	 * the size of the work. It is what turns a refusal into a next move.
	 */
	free(): readonly string[] | undefined;
	/** What is held right now, in the order it was taken. */
	open(): readonly Holding[];
};

The leases, and the rules that govern them.

ClaimsOptions

type

export type ClaimsOptions = {
	/**
	 * The things that may be claimed.
	 *
	 * Left out, any key may be taken - which is honest for a caller that cannot
	 * enumerate the work, and weaker: two spellings of one thing are then two
	 * things, and both are granted.
	 */
	keys?: readonly string[];
	/**
	 * How much one member may hold at once. Unbounded by default.
	 *
	 * Measured, on three members and six keys: with no bound one of them took
	 * everything and the other two spent their turns being refused, and telling
	 * them in the prompt to take one at a time changed nothing - four held at
	 * once in both arms, the same as with no rule. A bound of one held it to one,
	 * and the work still went round: more grants, not fewer.
	 *
	 * It is not the default because it is not free. The same measurement put the
	 * bounded arm 29% slower and 64% heavier in input tokens, since taking,
	 * releasing and being refused are all calls. Reach for it when there is
	 * contention, which is when it pays for itself.
	 */
	maxPerMember?: number;
};

What there is to claim, when the caller knows.

createClaims

function

export function createClaims(options: ClaimsOptions = {}): Claims { /* … */ }

An empty set of claims.

Keys are compared exactly, after trimming. Nothing else is normalised: a key is whatever the caller decided it is, and folding case or stripping a ./ would be this file guessing at what two strings have in common.

heldList

function

export function heldList(holdings: readonly Holding[]): string { /* … */ }

What is held, as a member reads it: one line each, in the order taken.

Holding

type

export type Holding = {
	/** What is held, exactly as it was taken. */
	key: string;
	/** The member holding it. */
	heldBy: string;
};

Something held, and who holds it.

TakeOutcome

type

export type TakeOutcome = { ok: true } | { ok: false; error: string; heldBy?: string };

What a take did, or why it did nothing.

heldBy is set only when the refusal is contention, because that is the one a member can act on: it names somebody to ask.