@juzser/blacksmith 0.3.0 → 0.5.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.
- package/.claude/agents/{auditor.md → bs-auditor.md} +8 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +16 -9
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +16 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +9 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +18 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +7 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +45 -31
- package/.claude/skills/bs/dispatch.md +133 -47
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +57 -16
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +190 -52
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +143 -50
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +70 -13
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +245 -20
- package/factory/orchestrator/dist/autonomy.js +6 -2
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +1087 -116
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crossFinding.js +20 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +352 -67
- package/factory/orchestrator/dist/db/projector.js +327 -54
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +108 -0
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +494 -42
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +182 -11
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +164 -14
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +108 -14
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/gh.js +19 -2
- package/factory/orchestrator/dist/git.js +20 -2
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +411 -39
- package/factory/orchestrator/dist/integration.js +20 -8
- package/factory/orchestrator/dist/issueReporter.js +81 -16
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +239 -0
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/logCache.js +291 -0
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +20 -7
- package/factory/orchestrator/dist/plan.js +399 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/projects.js +12 -1
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +471 -30
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/scheduler.js +3 -3
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +246 -12
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +192 -31
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/taxonomy.js +11 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +20 -16
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +113 -48
- package/factory/orchestrator/dist/waivers.js +99 -12
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/waveSchedule.js +84 -0
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +76 -21
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +27 -3
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- package/.claude/agents/uiux.md +0 -133
|
@@ -1,26 +1,72 @@
|
|
|
1
|
+
// Epic spend against budgets.yml's `epic.alarm_ratio` — the consumer that
|
|
2
|
+
// number never had (P9-33, closing D-12's "no" row).
|
|
3
|
+
//
|
|
4
|
+
// `epic.alarm_ratio: 0.7` has been in the policy since Phase 1. It parses into
|
|
5
|
+
// BudgetPolicy, budgets.test.ts asserts it, and no production path reads it.
|
|
6
|
+
// Its only real host was a sentence in SKILL.md telling a human to keep an eye
|
|
7
|
+
// on the number — which is to say the alarm fired when someone remembered to
|
|
8
|
+
// look.
|
|
9
|
+
//
|
|
10
|
+
// This module is the check that the two audits before it (dispatchAudit.ts,
|
|
11
|
+
// escalation.ts) established the shape for: a pure function over the event log,
|
|
12
|
+
// a status union with an explicit unverifiable, and a paired CLI verb that
|
|
13
|
+
// exits 1. It differs from them in one way that changes the whole design.
|
|
14
|
+
//
|
|
15
|
+
// MONOTONICITY. Those audits ask whether a rule was obeyed, and a gap in the
|
|
16
|
+
// record leaves the answer genuinely unknown in both directions. This one asks
|
|
17
|
+
// how much an epic spent, and every gap in the record can only make the real
|
|
18
|
+
// bill *bigger*. Measured spend is therefore a floor, never a total, and the
|
|
19
|
+
// two directions are not symmetric:
|
|
20
|
+
//
|
|
21
|
+
// * A crossing is a fact. If the tokens we can see already exceed the alarm,
|
|
22
|
+
// the tokens we cannot see do not un-exceed it. Report it, hole or no hole.
|
|
23
|
+
// * A non-crossing is a claim about the record, not about the world. "Under
|
|
24
|
+
// the alarm" is only honest when the *upper* bound is under it too.
|
|
25
|
+
//
|
|
26
|
+
// So the report carries two numbers per epic. `measuredTokens` is what the log
|
|
27
|
+
// actually recorded. `projectedTokens` adds, for every dispatch the measured
|
|
28
|
+
// number does not already account for, the cap budgets.yml declares for that
|
|
29
|
+
// role — which is one cap per re-dispatch, not one per task (D-171). A role
|
|
30
|
+
// the policy prices nowhere cannot be added to the ceiling at all — and a
|
|
31
|
+
// ceiling with a hole in it is not a ceiling, so the epic goes unverifiable
|
|
32
|
+
// rather than under.
|
|
33
|
+
//
|
|
34
|
+
// A cap is not a bound, though, and that is the third hole (D-188). The gate
|
|
35
|
+
// records a task budget overrun and does not block on one, so a dispatch is
|
|
36
|
+
// free to spend past its declared cap — dogfood's did, by an order of
|
|
37
|
+
// magnitude: `envkit-mcp-surface/task-2-path-guard` recorded a single coder
|
|
38
|
+
// round at 1,484,000 tokens against a 150,000 coder cap, and budgets.yml's own
|
|
39
|
+
// comment cites that run as the reason the epic cap was raised. Once an epic
|
|
40
|
+
// has been *measured* spending more on one task than the projection charges an
|
|
41
|
+
// unmeasured dispatch, its own log has falsified the price list the projection
|
|
42
|
+
// is built from, and the result is an estimate wearing a ceiling's name. So a
|
|
43
|
+
// task over that price counts as a hole too: the epic still gets its numbers,
|
|
44
|
+
// it just no longer gets to clear on them.
|
|
45
|
+
//
|
|
46
|
+
// The gap this makes visible is D-9's. On dogfood-envkit-1 the epic's declared
|
|
47
|
+
// task budgets summed to 545,000 against a 2,000,000 cap — 27%, comfortably
|
|
48
|
+
// under the 1,400,000 alarm — while the run's real projection was several times
|
|
49
|
+
// that. The plan-quorum budget trigger, the one automated check meant to catch
|
|
50
|
+
// "this plan is too expensive", measures the smaller half of the bill. Judge
|
|
51
|
+
// tokens are in no task result and never will be: a judge returns findings, not
|
|
52
|
+
// a Result. Anything that reads only `task-result-recorded` is reading the
|
|
53
|
+
// builder's half and calling it the total.
|
|
54
|
+
import { PRICED_ROLES, roleCapTokens } from './budgets.js';
|
|
1
55
|
import { eventTaskId } from './events.js';
|
|
2
56
|
import { bareTaskId, epicOfTaskId } from './taskId.js';
|
|
3
57
|
/**
|
|
4
|
-
* The roles
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* list is already written down, and a second copy in a different file is a
|
|
10
|
-
* second thing to keep true. What matters is that a role absent from *both*
|
|
11
|
-
* this list and the priced worker roles is reported rather than assumed free —
|
|
12
|
-
* see rolesWithoutCap. `security-reviewer` and `merger` are the live examples:
|
|
13
|
-
* both appear in budgets.yml's `narrowing_roles`, neither has a declared cap.
|
|
58
|
+
* The priced roles whose tokens reach a Result, and so `task-result-recorded`.
|
|
59
|
+
* Every other priced role (the judges, planner, security-reviewer, auditor)
|
|
60
|
+
* produces findings or a plan whose spend reaches no result event, so it is
|
|
61
|
+
* always projected. A role budgets.yml prices nowhere (merger, uiux, scribe,
|
|
62
|
+
* wave-runner) is reported rather than assumed free — see rolesWithoutCap.
|
|
14
63
|
*/
|
|
15
|
-
const
|
|
64
|
+
const WORKER_ROLES = ['coder', 'researcher', 'tester'];
|
|
16
65
|
function priceRole(role, policy) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (JUDGE_ROLES.includes(role))
|
|
22
|
-
return { kind: 'judge', capTokens: policy.task.judges.capTokens };
|
|
23
|
-
return { kind: 'unpriced' };
|
|
66
|
+
const capTokens = roleCapTokens(policy, role);
|
|
67
|
+
if (capTokens === null)
|
|
68
|
+
return { kind: 'unpriced' };
|
|
69
|
+
return { kind: WORKER_ROLES.includes(role) ? 'worker' : 'judge', capTokens };
|
|
24
70
|
}
|
|
25
71
|
/**
|
|
26
72
|
* The most the projection can charge one unmeasured dispatch.
|
|
@@ -30,7 +76,7 @@ function priceRole(role, policy) {
|
|
|
30
76
|
* otherwise let spend above the real ceiling pass unnamed.
|
|
31
77
|
*/
|
|
32
78
|
function largestDeclaredPrice(policy) {
|
|
33
|
-
return Math.max(
|
|
79
|
+
return Math.max(...PRICED_ROLES.map((role) => policy.task[role].capTokens));
|
|
34
80
|
}
|
|
35
81
|
function payloadString(payload, key) {
|
|
36
82
|
const value = payload[key];
|
|
@@ -120,22 +166,31 @@ export function readMeasuredSpend(events) {
|
|
|
120
166
|
continue;
|
|
121
167
|
const { payload, event_type: eventType } = event.record;
|
|
122
168
|
if (eventType === 'task-result-recorded') {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
if (total !== null)
|
|
127
|
-
record(taskId, total);
|
|
128
|
-
}
|
|
169
|
+
const read = readTokenUsage(payload.token_usage);
|
|
170
|
+
if (read.measured)
|
|
171
|
+
record(taskId, read.total);
|
|
129
172
|
continue;
|
|
130
173
|
}
|
|
131
174
|
if (eventType === 'budget-check-result') {
|
|
132
175
|
const used = payloadNumber(payload, 'tokensUsed');
|
|
133
|
-
if (used !== null)
|
|
176
|
+
if (used !== null && isPlausibleTokenCount(used))
|
|
134
177
|
record(taskId, used);
|
|
135
178
|
}
|
|
136
179
|
}
|
|
137
180
|
return spend;
|
|
138
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* The smallest `total_tokens` a real agent turn plausibly produces. A single
|
|
184
|
+
* turn runs into the thousands, so a recorded count below this is far more
|
|
185
|
+
* likely a dispatcher's untouched placeholder (0/0 or 1/1) than a
|
|
186
|
+
* measurement, and is treated exactly like `{measured: false}` — projected at
|
|
187
|
+
* the dispatch's cap rather than recorded as spend.
|
|
188
|
+
*/
|
|
189
|
+
const IMPLAUSIBLE_TOKEN_FLOOR = 100;
|
|
190
|
+
/** Shared by every other reader of `token_usage.total_tokens` (db/queries.ts), so the dashboard and stats agree with this alarm about what counts as measured. */
|
|
191
|
+
export function isPlausibleTokenCount(total) {
|
|
192
|
+
return total >= IMPLAUSIBLE_TOKEN_FLOOR;
|
|
193
|
+
}
|
|
139
194
|
const NUMBER = new Intl.NumberFormat('en-US');
|
|
140
195
|
function fmt(n) {
|
|
141
196
|
return NUMBER.format(n);
|
|
@@ -168,8 +223,9 @@ function decide(measured, projected, alarmTokens, capTokens, hasHoles) {
|
|
|
168
223
|
}
|
|
169
224
|
function describe(check, unattributed, largestPrice) {
|
|
170
225
|
const head = `${fmt(check.measuredTokens)} tokens measured across ${check.measuredTaskCount} of ` +
|
|
171
|
-
`${check.taskCount} task(s)
|
|
172
|
-
`${fmt(check.
|
|
226
|
+
`${check.taskCount} task(s) (${check.unmeasuredTaskCount} unmeasured), ` +
|
|
227
|
+
`${fmt(check.projectedTokens)} projected, against a ${fmt(check.alarmTokens)} alarm and a ` +
|
|
228
|
+
`${fmt(check.capTokens)} cap.`;
|
|
173
229
|
const holes = [];
|
|
174
230
|
if (check.rolesWithoutCap.length > 0) {
|
|
175
231
|
holes.push(`budgets.yml declares no cap for ${check.rolesWithoutCap.join(', ')}, so those ` +
|
|
@@ -212,8 +268,16 @@ function describe(check, unattributed, largestPrice) {
|
|
|
212
268
|
export function checkBudgetAlarm(events, policy, options) {
|
|
213
269
|
const capTokens = policy.epic.capTokens;
|
|
214
270
|
const alarmRatio = policy.epic.alarmRatio;
|
|
215
|
-
|
|
216
|
-
const
|
|
271
|
+
// Each epic is judged against budgets.yml sized for its own effort tier.
|
|
272
|
+
const epicPolicies = new Map();
|
|
273
|
+
const policyFor = (epicId) => {
|
|
274
|
+
let found = epicPolicies.get(epicId);
|
|
275
|
+
if (found === undefined) {
|
|
276
|
+
found = options.policyForEpic?.(epicId) ?? policy;
|
|
277
|
+
epicPolicies.set(epicId, found);
|
|
278
|
+
}
|
|
279
|
+
return found;
|
|
280
|
+
};
|
|
217
281
|
const membership = readEpicMembership(events);
|
|
218
282
|
const rawSpend = readMeasuredSpend(events);
|
|
219
283
|
const epicOf = (taskId) => membership.get(taskId) ?? membership.get(bareTaskId(taskId)) ?? null;
|
|
@@ -263,7 +327,7 @@ export function checkBudgetAlarm(events, policy, options) {
|
|
|
263
327
|
acc.tasks.add(bare);
|
|
264
328
|
acc.measuredTasks.add(bare);
|
|
265
329
|
acc.measuredTokens += tokens;
|
|
266
|
-
if (tokens >
|
|
330
|
+
if (tokens > largestDeclaredPrice(policyFor(epicId)))
|
|
267
331
|
acc.tasksOverPrice.add(bare);
|
|
268
332
|
}
|
|
269
333
|
let unattributedDispatches = 0;
|
|
@@ -288,7 +352,7 @@ export function checkBudgetAlarm(events, policy, options) {
|
|
|
288
352
|
const acc = accumulatorFor(epicId);
|
|
289
353
|
if (taskId !== null)
|
|
290
354
|
acc.tasks.add(bareTaskId(taskId));
|
|
291
|
-
const pricing = priceRole(role,
|
|
355
|
+
const pricing = priceRole(role, policyFor(epicId));
|
|
292
356
|
if (pricing.kind === 'unpriced') {
|
|
293
357
|
acc.rolesWithoutCap.add(role);
|
|
294
358
|
continue;
|
|
@@ -311,19 +375,24 @@ export function checkBudgetAlarm(events, policy, options) {
|
|
|
311
375
|
const epics = [...accumulators.entries()]
|
|
312
376
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
313
377
|
.map(([epicId, acc]) => {
|
|
378
|
+
const epicPolicy = policyFor(epicId);
|
|
379
|
+
const epicCap = epicPolicy.epic.capTokens;
|
|
380
|
+
const epicAlarm = Math.floor(epicCap * epicPolicy.epic.alarmRatio);
|
|
381
|
+
const largestPrice = largestDeclaredPrice(epicPolicy);
|
|
314
382
|
const projectedFrom = Object.fromEntries([...acc.projectedFrom.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
315
383
|
const projectedTokens = acc.measuredTokens + Object.values(projectedFrom).reduce((sum, n) => sum + n, 0);
|
|
316
384
|
const rolesWithoutCap = [...acc.rolesWithoutCap].sort();
|
|
317
385
|
const tasksOverPrice = [...acc.tasksOverPrice].sort();
|
|
318
|
-
const status = decide(acc.measuredTokens, projectedTokens,
|
|
386
|
+
const status = decide(acc.measuredTokens, projectedTokens, epicAlarm, epicCap, rolesWithoutCap.length > 0 || unattributedDispatches > 0 || tasksOverPrice.length > 0);
|
|
319
387
|
const partial = {
|
|
320
388
|
epicId,
|
|
321
|
-
capTokens,
|
|
322
|
-
alarmTokens,
|
|
389
|
+
capTokens: epicCap,
|
|
390
|
+
alarmTokens: epicAlarm,
|
|
323
391
|
measuredTokens: acc.measuredTokens,
|
|
324
392
|
projectedTokens,
|
|
325
393
|
taskCount: acc.tasks.size,
|
|
326
394
|
measuredTaskCount: acc.measuredTasks.size,
|
|
395
|
+
unmeasuredTaskCount: acc.tasks.size - acc.measuredTasks.size,
|
|
327
396
|
projectedFrom,
|
|
328
397
|
rolesWithoutCap,
|
|
329
398
|
tasksOverPrice,
|
|
@@ -332,14 +401,18 @@ export function checkBudgetAlarm(events, policy, options) {
|
|
|
332
401
|
return { ...partial, detail: describe(partial, unattributedDispatches, largestPrice) };
|
|
333
402
|
});
|
|
334
403
|
if (epics.length === 0) {
|
|
404
|
+
// An --epic that matched nothing is still reported against that epic's
|
|
405
|
+
// own tier cap: the default tier's cap would be a number it never had.
|
|
406
|
+
const emptyPolicy = options.epicId === undefined ? policy : policyFor(options.epicId);
|
|
335
407
|
const partial = {
|
|
336
408
|
epicId: '*',
|
|
337
|
-
capTokens,
|
|
338
|
-
alarmTokens,
|
|
409
|
+
capTokens: emptyPolicy.epic.capTokens,
|
|
410
|
+
alarmTokens: Math.floor(emptyPolicy.epic.capTokens * emptyPolicy.epic.alarmRatio),
|
|
339
411
|
measuredTokens: 0,
|
|
340
412
|
projectedTokens: 0,
|
|
341
413
|
taskCount: 0,
|
|
342
414
|
measuredTaskCount: 0,
|
|
415
|
+
unmeasuredTaskCount: 0,
|
|
343
416
|
projectedFrom: {},
|
|
344
417
|
rolesWithoutCap: [],
|
|
345
418
|
tasksOverPrice: [],
|
|
@@ -364,3 +437,25 @@ export function checkBudgetAlarm(events, policy, options) {
|
|
|
364
437
|
ok: epics.every((epic) => epic.status === 'under'),
|
|
365
438
|
};
|
|
366
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* The one rule for "was this result's token count measured". Lives beside
|
|
442
|
+
* `isPlausibleTokenCount` so every reader (this alarm, db/queries.ts, the
|
|
443
|
+
* gate) imports one module that already owns the floor. Measured means an
|
|
444
|
+
* object whose `total_tokens` is a plausible number; a bare number, a
|
|
445
|
+
* `{measured: false}`, a missing total and a placeholder under the floor are
|
|
446
|
+
* all unmeasured.
|
|
447
|
+
*/
|
|
448
|
+
export function readTokenUsage(tokenUsage) {
|
|
449
|
+
if (tokenUsage === null || typeof tokenUsage !== 'object')
|
|
450
|
+
return { measured: false };
|
|
451
|
+
const usage = tokenUsage;
|
|
452
|
+
const total = payloadNumber(usage, 'total_tokens');
|
|
453
|
+
if (total === null || !isPlausibleTokenCount(total))
|
|
454
|
+
return { measured: false };
|
|
455
|
+
return {
|
|
456
|
+
measured: true,
|
|
457
|
+
total,
|
|
458
|
+
input: payloadNumber(usage, 'input_tokens'),
|
|
459
|
+
output: payloadNumber(usage, 'output_tokens'),
|
|
460
|
+
};
|
|
461
|
+
}
|