claude-flow 3.14.4 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/package.json +1 -1
  3. package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.d.ts +139 -0
  4. package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.js +358 -0
  5. package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.d.ts +47 -0
  6. package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.js +65 -0
  7. package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.d.ts +96 -0
  8. package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.js +225 -0
  9. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +20 -5
  10. package/v3/@claude-flow/cli/dist/src/mcp-client.js +21 -0
  11. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.d.ts +28 -0
  12. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.js +394 -0
  13. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.d.ts +36 -0
  14. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.js +253 -0
  15. package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.d.ts +20 -0
  16. package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.js +169 -0
  17. package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.d.ts +55 -0
  18. package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.js +329 -0
  19. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +4 -0
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +8 -0
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.d.ts +6 -0
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.js +83 -4
  23. package/v3/@claude-flow/cli/package.json +4 -1
@@ -4,7 +4,8 @@
4
4
  "Bash(mkdir -p /tmp/ruflo-smoke)",
5
5
  "Bash(rm -rf /tmp/ruflo-smoke/*)",
6
6
  "Read(//tmp/**)",
7
- "Bash(npm pack *)"
7
+ "Bash(npm pack *)",
8
+ "WebFetch(domain:www.npmjs.com)"
8
9
  ]
9
10
  }
10
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.14.4",
3
+ "version": "3.16.0",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -0,0 +1,139 @@
1
+ /**
2
+ * BbsRoomBudgetTracker — Atomic reserve / commit / release token-bucket
3
+ * backed by better-sqlite3 (ADR-164.1 §3-§5).
4
+ *
5
+ * Closes the read-then-write race in ADR-164's first-draft tracker by routing
6
+ * every state transition through a single `BEGIN IMMEDIATE` transaction that
7
+ * acquires the SQLite write lock atomically with the gate check. The
8
+ * `_lock_bump` column write is a belt-and-suspenders explicit write inside
9
+ * the transaction so the lock acquisition is visible to code review and
10
+ * query-log analysis (§3.2 peer-review clarification).
11
+ *
12
+ * Architectural constraints (ADR-164.1):
13
+ * - Default OFF: this module is opt-in behind `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`.
14
+ * The pod-tick.mjs file-based stub remains the default until the flag is
15
+ * flipped per ADR-164.1 §12.4.
16
+ * - `committed_post_expiry` state machine implemented per §5.3 — late
17
+ * commits on expired reservations ARE accepted (the API spend already
18
+ * happened), transition to `committed_post_expiry`, charge the budget,
19
+ * and return `warned: 'COMMIT_AFTER_EXPIRY'`. This closes the §8.1
20
+ * Expired Commit Leak surfaced by peer review on 2026-06-29.
21
+ * - `expires_at` is clamped to [5_000, 300_000] ms (§3.2).
22
+ * - WAL + synchronous=NORMAL + busy_timeout=500 for write throughput.
23
+ *
24
+ * The sweeper interval (`sweepExpired()`) is exposed as a manual method;
25
+ * callers are expected to drive it on a setInterval — that integration lives
26
+ * at the call site, not in this file, per ADR-164.1 §7.1.
27
+ *
28
+ * @module @claude-flow/cli/business-pods/bbs-budget-tracker
29
+ */
30
+ export interface SqliteDatabase {
31
+ pragma(name: string): unknown;
32
+ prepare(sql: string): SqliteStatement;
33
+ exec(sql: string): void;
34
+ close(): void;
35
+ }
36
+ export interface SqliteStatement {
37
+ run(...params: unknown[]): {
38
+ changes: number;
39
+ lastInsertRowid: number | bigint;
40
+ };
41
+ get(...params: unknown[]): Record<string, unknown> | undefined;
42
+ all(...params: unknown[]): Record<string, unknown>[];
43
+ }
44
+ export declare const RESERVATION_EXPIRY_FLOOR_MS = 5000;
45
+ export declare const RESERVATION_EXPIRY_CEILING_MS = 300000;
46
+ export declare const RESERVATION_EXPIRY_DEFAULT_MS = 60000;
47
+ export declare function clampReservationExpiry(raw: number | undefined): number;
48
+ export type ReserveResult = {
49
+ ok: true;
50
+ reservationId: string;
51
+ remainingAfterReserve: number;
52
+ } | {
53
+ ok: false;
54
+ error: 'BUDGET_EXCEEDED' | 'ROOM_NOT_FOUND';
55
+ };
56
+ export type CommitResult = {
57
+ ok: true;
58
+ committed: true;
59
+ finalRemaining: number;
60
+ } | {
61
+ ok: true;
62
+ warned: 'COMMIT_AFTER_EXPIRY';
63
+ finalRemaining: number;
64
+ } | {
65
+ ok: false;
66
+ error: 'NOT_FOUND' | 'ALREADY_FINALIZED';
67
+ };
68
+ export type ReleaseResult = {
69
+ ok: true;
70
+ released: true;
71
+ } | {
72
+ ok: false;
73
+ error: 'NOT_FOUND' | 'ALREADY_FINALIZED';
74
+ };
75
+ export declare const SCHEMA_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA busy_timeout = 500;\n\nCREATE TABLE IF NOT EXISTS bbs_budget_rooms (\n room_id TEXT NOT NULL PRIMARY KEY,\n monthly_cap_usd REAL NOT NULL CHECK (monthly_cap_usd >= 0),\n billing_month TEXT NOT NULL,\n _lock_bump INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE TABLE IF NOT EXISTS bbs_budget_reservations (\n reservation_id TEXT NOT NULL PRIMARY KEY,\n room_id TEXT NOT NULL REFERENCES bbs_budget_rooms(room_id),\n caller_node_id TEXT NOT NULL,\n estimated_usd REAL NOT NULL CHECK (estimated_usd >= 0),\n actual_usd REAL,\n state TEXT NOT NULL\n CHECK (state IN ('reserved','committed','released','expired','committed_post_expiry')),\n reserved_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n committed_at INTEGER,\n audit_envelope_id TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_reservations_room_month\n ON bbs_budget_reservations (room_id, reserved_at);\n\nCREATE INDEX IF NOT EXISTS idx_reservations_expiry\n ON bbs_budget_reservations (state, expires_at);\n";
76
+ export interface BudgetAuditSink {
77
+ emit(eventType: 'reservation.committed_post_expiry' | 'reservation.committed' | 'reservation.released' | 'reservation.reserved' | 'reservation.budget_exceeded', payload: Record<string, unknown>): void;
78
+ }
79
+ export interface AtomicBbsRoomBudgetTrackerOptions {
80
+ /** SQLite database handle (caller-owned, opened in WAL mode). */
81
+ db: SqliteDatabase;
82
+ /** Override the default 60s reservation window (clamped to [5s, 300s]). */
83
+ defaultExpiryMs?: number;
84
+ /** Audit sink — receives reservation lifecycle events. */
85
+ audit?: BudgetAuditSink;
86
+ /** Inject Date.now() for tests. */
87
+ clock?: () => number;
88
+ }
89
+ export declare class AtomicBbsRoomBudgetTracker {
90
+ private readonly db;
91
+ private readonly defaultExpiryMs;
92
+ private readonly audit;
93
+ private readonly clock;
94
+ constructor(opts: AtomicBbsRoomBudgetTrackerOptions);
95
+ /**
96
+ * Register (or update) a room and its monthly cap. Idempotent.
97
+ */
98
+ registerRoom(roomId: string, monthlyCapUsd: number): void;
99
+ /**
100
+ * Atomically check budget and insert a reservation row in a single
101
+ * BEGIN IMMEDIATE transaction. See ADR-164.1 §5.2.
102
+ */
103
+ reserve(roomId: string, callerId: string, estimatedUsd: number, opts?: {
104
+ auditEnvelopeId?: string;
105
+ expiryMs?: number;
106
+ }): ReserveResult;
107
+ /**
108
+ * Commit the reservation with the actual cost. Late commits (expired
109
+ * before commit landed) ARE accepted, transitioned to
110
+ * 'committed_post_expiry', charged to the budget, and surfaced via
111
+ * `warned: 'COMMIT_AFTER_EXPIRY'` plus a `reservation.committed_post_expiry`
112
+ * audit emit. See ADR-164.1 §5.3 + §8.1.
113
+ */
114
+ commit(reservationId: string, actualUsd: number): CommitResult;
115
+ /**
116
+ * Release a reservation (caller decided not to proceed). State must be
117
+ * 'reserved'; any other state is ALREADY_FINALIZED.
118
+ */
119
+ release(reservationId: string): ReleaseResult;
120
+ /**
121
+ * Sweep expired reservations: transition 'reserved' rows whose expiry has
122
+ * passed to 'expired'. Callers should drive this on a setInterval per
123
+ * ADR-164.1 §7.1 (default cadence 5s; this method does NOT install the
124
+ * timer — that's the integration site's responsibility).
125
+ * Returns the number of rows updated.
126
+ */
127
+ sweepExpired(): number;
128
+ /**
129
+ * Diagnostic read: list reservations for a room. Not on the hot path.
130
+ */
131
+ listReservations(roomId: string): Array<Record<string, unknown>>;
132
+ }
133
+ /**
134
+ * Feature-flagged factory per ADR-164.1 §12.3. Returns an atomic tracker
135
+ * when `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`, otherwise returns null so the
136
+ * caller can keep using the file-based stub in pod-tick.mjs.
137
+ */
138
+ export declare function createAtomicTrackerIfEnabled(opts: AtomicBbsRoomBudgetTrackerOptions): AtomicBbsRoomBudgetTracker | null;
139
+ //# sourceMappingURL=bbs-budget-tracker.d.ts.map
@@ -0,0 +1,358 @@
1
+ /**
2
+ * BbsRoomBudgetTracker — Atomic reserve / commit / release token-bucket
3
+ * backed by better-sqlite3 (ADR-164.1 §3-§5).
4
+ *
5
+ * Closes the read-then-write race in ADR-164's first-draft tracker by routing
6
+ * every state transition through a single `BEGIN IMMEDIATE` transaction that
7
+ * acquires the SQLite write lock atomically with the gate check. The
8
+ * `_lock_bump` column write is a belt-and-suspenders explicit write inside
9
+ * the transaction so the lock acquisition is visible to code review and
10
+ * query-log analysis (§3.2 peer-review clarification).
11
+ *
12
+ * Architectural constraints (ADR-164.1):
13
+ * - Default OFF: this module is opt-in behind `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`.
14
+ * The pod-tick.mjs file-based stub remains the default until the flag is
15
+ * flipped per ADR-164.1 §12.4.
16
+ * - `committed_post_expiry` state machine implemented per §5.3 — late
17
+ * commits on expired reservations ARE accepted (the API spend already
18
+ * happened), transition to `committed_post_expiry`, charge the budget,
19
+ * and return `warned: 'COMMIT_AFTER_EXPIRY'`. This closes the §8.1
20
+ * Expired Commit Leak surfaced by peer review on 2026-06-29.
21
+ * - `expires_at` is clamped to [5_000, 300_000] ms (§3.2).
22
+ * - WAL + synchronous=NORMAL + busy_timeout=500 for write throughput.
23
+ *
24
+ * The sweeper interval (`sweepExpired()`) is exposed as a manual method;
25
+ * callers are expected to drive it on a setInterval — that integration lives
26
+ * at the call site, not in this file, per ADR-164.1 §7.1.
27
+ *
28
+ * @module @claude-flow/cli/business-pods/bbs-budget-tracker
29
+ */
30
+ import { randomUUID } from 'node:crypto';
31
+ // ---------- expiry-window clamp -------------------------------------------
32
+ export const RESERVATION_EXPIRY_FLOOR_MS = 5_000;
33
+ export const RESERVATION_EXPIRY_CEILING_MS = 300_000;
34
+ export const RESERVATION_EXPIRY_DEFAULT_MS = 60_000;
35
+ export function clampReservationExpiry(raw) {
36
+ if (raw === undefined || !Number.isFinite(raw)) {
37
+ const envVal = Number(process.env.CLAUDE_FLOW_BBS_RESERVATION_EXPIRY_MS);
38
+ if (Number.isFinite(envVal) && envVal > 0) {
39
+ return Math.max(RESERVATION_EXPIRY_FLOOR_MS, Math.min(RESERVATION_EXPIRY_CEILING_MS, envVal));
40
+ }
41
+ return RESERVATION_EXPIRY_DEFAULT_MS;
42
+ }
43
+ return Math.max(RESERVATION_EXPIRY_FLOOR_MS, Math.min(RESERVATION_EXPIRY_CEILING_MS, raw));
44
+ }
45
+ // ---------- schema migration ----------------------------------------------
46
+ export const SCHEMA_SQL = `
47
+ PRAGMA journal_mode = WAL;
48
+ PRAGMA synchronous = NORMAL;
49
+ PRAGMA busy_timeout = 500;
50
+
51
+ CREATE TABLE IF NOT EXISTS bbs_budget_rooms (
52
+ room_id TEXT NOT NULL PRIMARY KEY,
53
+ monthly_cap_usd REAL NOT NULL CHECK (monthly_cap_usd >= 0),
54
+ billing_month TEXT NOT NULL,
55
+ _lock_bump INTEGER NOT NULL DEFAULT 0
56
+ );
57
+
58
+ CREATE TABLE IF NOT EXISTS bbs_budget_reservations (
59
+ reservation_id TEXT NOT NULL PRIMARY KEY,
60
+ room_id TEXT NOT NULL REFERENCES bbs_budget_rooms(room_id),
61
+ caller_node_id TEXT NOT NULL,
62
+ estimated_usd REAL NOT NULL CHECK (estimated_usd >= 0),
63
+ actual_usd REAL,
64
+ state TEXT NOT NULL
65
+ CHECK (state IN ('reserved','committed','released','expired','committed_post_expiry')),
66
+ reserved_at INTEGER NOT NULL,
67
+ expires_at INTEGER NOT NULL,
68
+ committed_at INTEGER,
69
+ audit_envelope_id TEXT NOT NULL
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_reservations_room_month
73
+ ON bbs_budget_reservations (room_id, reserved_at);
74
+
75
+ CREATE INDEX IF NOT EXISTS idx_reservations_expiry
76
+ ON bbs_budget_reservations (state, expires_at);
77
+ `;
78
+ // ---------- helpers --------------------------------------------------------
79
+ function currentBillingMonth(now) {
80
+ const d = new Date(now);
81
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
82
+ }
83
+ function billingMonthStartMs(billingMonth) {
84
+ const [y, m] = billingMonth.split('-').map((n) => Number(n));
85
+ return Date.UTC(y, (m - 1), 1, 0, 0, 0, 0);
86
+ }
87
+ const noopAudit = { emit: () => undefined };
88
+ export class AtomicBbsRoomBudgetTracker {
89
+ db;
90
+ defaultExpiryMs;
91
+ audit;
92
+ clock;
93
+ constructor(opts) {
94
+ this.db = opts.db;
95
+ this.defaultExpiryMs = clampReservationExpiry(opts.defaultExpiryMs);
96
+ this.audit = opts.audit ?? noopAudit;
97
+ this.clock = opts.clock ?? Date.now;
98
+ // Ensure schema is present. exec() is a no-op for tables that already
99
+ // exist due to IF NOT EXISTS.
100
+ this.db.exec(SCHEMA_SQL);
101
+ }
102
+ /**
103
+ * Register (or update) a room and its monthly cap. Idempotent.
104
+ */
105
+ registerRoom(roomId, monthlyCapUsd) {
106
+ const billingMonth = currentBillingMonth(this.clock());
107
+ this.db
108
+ .prepare(`INSERT INTO bbs_budget_rooms (room_id, monthly_cap_usd, billing_month, _lock_bump)
109
+ VALUES (?, ?, ?, 0)
110
+ ON CONFLICT(room_id) DO UPDATE SET monthly_cap_usd = excluded.monthly_cap_usd`)
111
+ .run(roomId, monthlyCapUsd, billingMonth);
112
+ }
113
+ /**
114
+ * Atomically check budget and insert a reservation row in a single
115
+ * BEGIN IMMEDIATE transaction. See ADR-164.1 §5.2.
116
+ */
117
+ reserve(roomId, callerId, estimatedUsd, opts) {
118
+ if (!Number.isFinite(estimatedUsd) || estimatedUsd < 0) {
119
+ throw new Error('estimatedUsd must be a non-negative finite number');
120
+ }
121
+ const auditEnvelopeId = opts?.auditEnvelopeId ?? `audit-${randomUUID()}`;
122
+ const expiryMs = clampReservationExpiry(opts?.expiryMs ?? this.defaultExpiryMs);
123
+ const nowMs = this.clock();
124
+ const billingMonth = currentBillingMonth(nowMs);
125
+ const billingMonthStart = billingMonthStartMs(billingMonth);
126
+ const beginStmt = this.db.prepare('BEGIN IMMEDIATE');
127
+ const commitStmt = this.db.prepare('COMMIT');
128
+ const rollbackStmt = this.db.prepare('ROLLBACK');
129
+ beginStmt.run();
130
+ let transactionOpen = true;
131
+ try {
132
+ // Step 1: touch the room header row (the _lock_bump write makes the
133
+ // lock acquisition visible per §3.2 peer-review note).
134
+ const bumpRes = this.db
135
+ .prepare(`UPDATE bbs_budget_rooms
136
+ SET _lock_bump = _lock_bump + 1,
137
+ billing_month = CASE WHEN billing_month = ? THEN billing_month ELSE ? END
138
+ WHERE room_id = ?`)
139
+ .run(billingMonth, billingMonth, roomId);
140
+ if (bumpRes.changes === 0) {
141
+ rollbackStmt.run();
142
+ transactionOpen = false;
143
+ return { ok: false, error: 'ROOM_NOT_FOUND' };
144
+ }
145
+ const roomRow = this.db
146
+ .prepare(`SELECT monthly_cap_usd, billing_month FROM bbs_budget_rooms WHERE room_id = ?`)
147
+ .get(roomId);
148
+ if (!roomRow) {
149
+ rollbackStmt.run();
150
+ transactionOpen = false;
151
+ return { ok: false, error: 'ROOM_NOT_FOUND' };
152
+ }
153
+ const monthlyCap = roomRow.monthly_cap_usd;
154
+ // Step 3: compute committed + live-reserved totals within this billing month.
155
+ const totals = this.db
156
+ .prepare(`SELECT
157
+ COALESCE(SUM(CASE WHEN state IN ('committed','committed_post_expiry') THEN actual_usd ELSE 0 END), 0) AS committed_total,
158
+ COALESCE(SUM(CASE WHEN state = 'reserved' AND expires_at > ? THEN estimated_usd ELSE 0 END), 0) AS reserved_total
159
+ FROM bbs_budget_reservations
160
+ WHERE room_id = ? AND reserved_at >= ?`)
161
+ .get(nowMs, roomId, billingMonthStart);
162
+ const committedTotal = totals.committed_total ?? 0;
163
+ const reservedTotal = totals.reserved_total ?? 0;
164
+ const projected = committedTotal + reservedTotal + estimatedUsd;
165
+ if (projected > monthlyCap + 1e-9) {
166
+ rollbackStmt.run();
167
+ transactionOpen = false;
168
+ this.audit.emit('reservation.budget_exceeded', {
169
+ roomId, callerId, estimatedUsd, committedTotal, reservedTotal, monthlyCap,
170
+ });
171
+ return { ok: false, error: 'BUDGET_EXCEEDED' };
172
+ }
173
+ // Step 5: insert the reservation.
174
+ const reservationId = `res-${randomUUID()}`;
175
+ const expiresAt = nowMs + expiryMs;
176
+ this.db
177
+ .prepare(`INSERT INTO bbs_budget_reservations
178
+ (reservation_id, room_id, caller_node_id, estimated_usd, actual_usd,
179
+ state, reserved_at, expires_at, committed_at, audit_envelope_id)
180
+ VALUES (?, ?, ?, ?, NULL, 'reserved', ?, ?, NULL, ?)`)
181
+ .run(reservationId, roomId, callerId, estimatedUsd, nowMs, expiresAt, auditEnvelopeId);
182
+ commitStmt.run();
183
+ transactionOpen = false;
184
+ const remaining = monthlyCap - (committedTotal + reservedTotal + estimatedUsd);
185
+ this.audit.emit('reservation.reserved', {
186
+ reservationId, roomId, callerId, estimatedUsd, expiresAt, remaining,
187
+ });
188
+ return { ok: true, reservationId, remainingAfterReserve: remaining };
189
+ }
190
+ catch (err) {
191
+ if (transactionOpen) {
192
+ try {
193
+ rollbackStmt.run();
194
+ }
195
+ catch { /* already-rolled */ }
196
+ }
197
+ throw err;
198
+ }
199
+ }
200
+ /**
201
+ * Commit the reservation with the actual cost. Late commits (expired
202
+ * before commit landed) ARE accepted, transitioned to
203
+ * 'committed_post_expiry', charged to the budget, and surfaced via
204
+ * `warned: 'COMMIT_AFTER_EXPIRY'` plus a `reservation.committed_post_expiry`
205
+ * audit emit. See ADR-164.1 §5.3 + §8.1.
206
+ */
207
+ commit(reservationId, actualUsd) {
208
+ if (!Number.isFinite(actualUsd) || actualUsd < 0) {
209
+ throw new Error('actualUsd must be a non-negative finite number');
210
+ }
211
+ const nowMs = this.clock();
212
+ const beginStmt = this.db.prepare('BEGIN IMMEDIATE');
213
+ const commitStmt = this.db.prepare('COMMIT');
214
+ const rollbackStmt = this.db.prepare('ROLLBACK');
215
+ beginStmt.run();
216
+ let transactionOpen = true;
217
+ try {
218
+ const row = this.db
219
+ .prepare(`SELECT state, room_id, estimated_usd, reserved_at, expires_at
220
+ FROM bbs_budget_reservations
221
+ WHERE reservation_id = ?`)
222
+ .get(reservationId);
223
+ if (!row) {
224
+ rollbackStmt.run();
225
+ transactionOpen = false;
226
+ return { ok: false, error: 'NOT_FOUND' };
227
+ }
228
+ if (row.state === 'committed' || row.state === 'committed_post_expiry' ||
229
+ row.state === 'released' || row.state === 'expired') {
230
+ rollbackStmt.run();
231
+ transactionOpen = false;
232
+ return { ok: false, error: 'ALREADY_FINALIZED' };
233
+ }
234
+ // state is 'reserved' here. Decide committed vs committed_post_expiry.
235
+ const lateCommit = row.expires_at <= nowMs;
236
+ const newState = lateCommit ? 'committed_post_expiry' : 'committed';
237
+ this.db
238
+ .prepare(`UPDATE bbs_budget_reservations
239
+ SET state = ?, actual_usd = ?, committed_at = ?
240
+ WHERE reservation_id = ?`)
241
+ .run(newState, actualUsd, nowMs, reservationId);
242
+ // Re-compute remaining for the return value.
243
+ const billingMonth = currentBillingMonth(nowMs);
244
+ const billingMonthStart = billingMonthStartMs(billingMonth);
245
+ const totals = this.db
246
+ .prepare(`SELECT
247
+ COALESCE(SUM(CASE WHEN state IN ('committed','committed_post_expiry') THEN actual_usd ELSE 0 END), 0) AS committed_total,
248
+ COALESCE(SUM(CASE WHEN state = 'reserved' AND expires_at > ? THEN estimated_usd ELSE 0 END), 0) AS reserved_total
249
+ FROM bbs_budget_reservations
250
+ WHERE room_id = ? AND reserved_at >= ?`)
251
+ .get(nowMs, row.room_id, billingMonthStart);
252
+ const roomRow = this.db
253
+ .prepare(`SELECT monthly_cap_usd FROM bbs_budget_rooms WHERE room_id = ?`)
254
+ .get(row.room_id);
255
+ const monthlyCap = roomRow?.monthly_cap_usd ?? 0;
256
+ const finalRemaining = monthlyCap - ((totals.committed_total ?? 0) + (totals.reserved_total ?? 0));
257
+ commitStmt.run();
258
+ transactionOpen = false;
259
+ if (lateCommit) {
260
+ // Emit the high-priority audit envelope per §8.1.
261
+ this.audit.emit('reservation.committed_post_expiry', {
262
+ reservationId, roomId: row.room_id, actualUsd, finalRemaining,
263
+ reservedAt: row.reserved_at, expiresAt: row.expires_at, committedAt: nowMs,
264
+ });
265
+ return { ok: true, warned: 'COMMIT_AFTER_EXPIRY', finalRemaining };
266
+ }
267
+ this.audit.emit('reservation.committed', {
268
+ reservationId, roomId: row.room_id, actualUsd, finalRemaining,
269
+ });
270
+ return { ok: true, committed: true, finalRemaining };
271
+ }
272
+ catch (err) {
273
+ if (transactionOpen) {
274
+ try {
275
+ rollbackStmt.run();
276
+ }
277
+ catch { /* already-rolled */ }
278
+ }
279
+ throw err;
280
+ }
281
+ }
282
+ /**
283
+ * Release a reservation (caller decided not to proceed). State must be
284
+ * 'reserved'; any other state is ALREADY_FINALIZED.
285
+ */
286
+ release(reservationId) {
287
+ const beginStmt = this.db.prepare('BEGIN IMMEDIATE');
288
+ const commitStmt = this.db.prepare('COMMIT');
289
+ const rollbackStmt = this.db.prepare('ROLLBACK');
290
+ beginStmt.run();
291
+ let transactionOpen = true;
292
+ try {
293
+ const row = this.db
294
+ .prepare(`SELECT state, room_id FROM bbs_budget_reservations WHERE reservation_id = ?`)
295
+ .get(reservationId);
296
+ if (!row) {
297
+ rollbackStmt.run();
298
+ transactionOpen = false;
299
+ return { ok: false, error: 'NOT_FOUND' };
300
+ }
301
+ if (row.state !== 'reserved') {
302
+ rollbackStmt.run();
303
+ transactionOpen = false;
304
+ return { ok: false, error: 'ALREADY_FINALIZED' };
305
+ }
306
+ this.db
307
+ .prepare(`UPDATE bbs_budget_reservations SET state = 'released' WHERE reservation_id = ?`)
308
+ .run(reservationId);
309
+ commitStmt.run();
310
+ transactionOpen = false;
311
+ this.audit.emit('reservation.released', { reservationId, roomId: row.room_id });
312
+ return { ok: true, released: true };
313
+ }
314
+ catch (err) {
315
+ if (transactionOpen) {
316
+ try {
317
+ rollbackStmt.run();
318
+ }
319
+ catch { /* already-rolled */ }
320
+ }
321
+ throw err;
322
+ }
323
+ }
324
+ /**
325
+ * Sweep expired reservations: transition 'reserved' rows whose expiry has
326
+ * passed to 'expired'. Callers should drive this on a setInterval per
327
+ * ADR-164.1 §7.1 (default cadence 5s; this method does NOT install the
328
+ * timer — that's the integration site's responsibility).
329
+ * Returns the number of rows updated.
330
+ */
331
+ sweepExpired() {
332
+ const result = this.db
333
+ .prepare(`UPDATE bbs_budget_reservations
334
+ SET state = 'expired'
335
+ WHERE state = 'reserved' AND expires_at < ?`)
336
+ .run(this.clock());
337
+ return result.changes;
338
+ }
339
+ /**
340
+ * Diagnostic read: list reservations for a room. Not on the hot path.
341
+ */
342
+ listReservations(roomId) {
343
+ return this.db
344
+ .prepare(`SELECT * FROM bbs_budget_reservations WHERE room_id = ? ORDER BY reserved_at`)
345
+ .all(roomId);
346
+ }
347
+ }
348
+ /**
349
+ * Feature-flagged factory per ADR-164.1 §12.3. Returns an atomic tracker
350
+ * when `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`, otherwise returns null so the
351
+ * caller can keep using the file-based stub in pod-tick.mjs.
352
+ */
353
+ export function createAtomicTrackerIfEnabled(opts) {
354
+ if (process.env.CLAUDE_FLOW_BBS_ATOMIC_BUDGET !== '1')
355
+ return null;
356
+ return new AtomicBbsRoomBudgetTracker(opts);
357
+ }
358
+ //# sourceMappingURL=bbs-budget-tracker.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Domain-affinity routing policy — ADR-164 §3.4.
3
+ *
4
+ * Surfaces a deterministic routing decision for `@metaharness/router` to use
5
+ * before its KRR cost-optimal step. The policy is intentionally tiny —
6
+ * three branches over `preferLocalExecution` and `budgetUsdMonthly` — so the
7
+ * decision is auditable from the pod template alone, no learned-weight side
8
+ * effects.
9
+ *
10
+ * Rules (per the Phase-3 brief):
11
+ * 1. `preferLocalExecution === true` → 'local-stdio'
12
+ * 2. `preferLocalExecution === false` AND `budgetUsdMonthly >= 50` → 'cloud-managed'
13
+ * 3. otherwise → 'remote-peer'
14
+ *
15
+ * The boundary `>= 50` matches the Phase-2 sales template (budget 50/month) —
16
+ * it routes the canonical reference pod to the cloud-managed backend. Pods
17
+ * below the threshold default to the federation peer-node backend.
18
+ *
19
+ * Wire point: `@metaharness/router` reads the result and supplies it as a
20
+ * routing hint *before* its KRR step. The router is free to override on
21
+ * latency/cost grounds — domain affinity is a policy floor, not a hard gate.
22
+ *
23
+ * @module @claude-flow/cli/business-pods/domain-affinity-policy
24
+ */
25
+ import type { PodTemplate } from './pod-schema.js';
26
+ /** Three backends @metaharness/router can target for a pod tick. */
27
+ export type AgentBackend = 'local-stdio' | 'remote-peer' | 'cloud-managed';
28
+ /** Threshold for `cloud-managed` routing. Pods at or above this monthly
29
+ * spend default to cloud-managed when they are *not* local-pinned. */
30
+ export declare const CLOUD_BUDGET_THRESHOLD_USD = 50;
31
+ /** Structured routing decision. `reason` is rendered into the routing
32
+ * rationale via `hooks_explain` so operators can see why each pod was
33
+ * routed where it was. */
34
+ export interface BackendDecision {
35
+ backend: AgentBackend;
36
+ reason: string;
37
+ }
38
+ /**
39
+ * Decide which backend `@metaharness/router` should prefer for a pod tick.
40
+ *
41
+ * @param pod A validated PodTemplate. Validation is the caller's
42
+ * responsibility; this function will throw on malformed input
43
+ * (specifically on missing `preferLocalExecution` or
44
+ * `budgetUsdMonthly`).
45
+ */
46
+ export declare function selectAgentBackend(pod: PodTemplate): BackendDecision;
47
+ //# sourceMappingURL=domain-affinity-policy.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Domain-affinity routing policy — ADR-164 §3.4.
3
+ *
4
+ * Surfaces a deterministic routing decision for `@metaharness/router` to use
5
+ * before its KRR cost-optimal step. The policy is intentionally tiny —
6
+ * three branches over `preferLocalExecution` and `budgetUsdMonthly` — so the
7
+ * decision is auditable from the pod template alone, no learned-weight side
8
+ * effects.
9
+ *
10
+ * Rules (per the Phase-3 brief):
11
+ * 1. `preferLocalExecution === true` → 'local-stdio'
12
+ * 2. `preferLocalExecution === false` AND `budgetUsdMonthly >= 50` → 'cloud-managed'
13
+ * 3. otherwise → 'remote-peer'
14
+ *
15
+ * The boundary `>= 50` matches the Phase-2 sales template (budget 50/month) —
16
+ * it routes the canonical reference pod to the cloud-managed backend. Pods
17
+ * below the threshold default to the federation peer-node backend.
18
+ *
19
+ * Wire point: `@metaharness/router` reads the result and supplies it as a
20
+ * routing hint *before* its KRR step. The router is free to override on
21
+ * latency/cost grounds — domain affinity is a policy floor, not a hard gate.
22
+ *
23
+ * @module @claude-flow/cli/business-pods/domain-affinity-policy
24
+ */
25
+ /** Threshold for `cloud-managed` routing. Pods at or above this monthly
26
+ * spend default to cloud-managed when they are *not* local-pinned. */
27
+ export const CLOUD_BUDGET_THRESHOLD_USD = 50;
28
+ /**
29
+ * Decide which backend `@metaharness/router` should prefer for a pod tick.
30
+ *
31
+ * @param pod A validated PodTemplate. Validation is the caller's
32
+ * responsibility; this function will throw on malformed input
33
+ * (specifically on missing `preferLocalExecution` or
34
+ * `budgetUsdMonthly`).
35
+ */
36
+ export function selectAgentBackend(pod) {
37
+ if (pod === null || typeof pod !== 'object') {
38
+ throw new TypeError('selectAgentBackend: pod must be a validated PodTemplate object');
39
+ }
40
+ if (typeof pod.preferLocalExecution !== 'boolean') {
41
+ throw new TypeError('selectAgentBackend: pod.preferLocalExecution must be boolean (validate via pod-schema first)');
42
+ }
43
+ if (typeof pod.budgetUsdMonthly !== 'number' || !Number.isFinite(pod.budgetUsdMonthly)) {
44
+ throw new TypeError('selectAgentBackend: pod.budgetUsdMonthly must be a finite number (validate via pod-schema first)');
45
+ }
46
+ if (pod.preferLocalExecution) {
47
+ return {
48
+ backend: 'local-stdio',
49
+ reason: `pod "${pod.name}" preferLocalExecution=true — domain-affinity policy pins to local stdio`,
50
+ };
51
+ }
52
+ if (pod.budgetUsdMonthly >= CLOUD_BUDGET_THRESHOLD_USD) {
53
+ return {
54
+ backend: 'cloud-managed',
55
+ reason: `pod "${pod.name}" preferLocalExecution=false and budgetUsdMonthly=${pod.budgetUsdMonthly} ` +
56
+ `>= ${CLOUD_BUDGET_THRESHOLD_USD} — routes to cloud Managed Agents`,
57
+ };
58
+ }
59
+ return {
60
+ backend: 'remote-peer',
61
+ reason: `pod "${pod.name}" preferLocalExecution=false and budgetUsdMonthly=${pod.budgetUsdMonthly} ` +
62
+ `< ${CLOUD_BUDGET_THRESHOLD_USD} — routes to federation peer node`,
63
+ };
64
+ }
65
+ //# sourceMappingURL=domain-affinity-policy.js.map