@juzser/blacksmith 0.4.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.
Files changed (131) hide show
  1. package/.claude/agents/{auditor.md → bs-auditor.md} +5 -5
  2. package/.claude/agents/{coder.md → bs-coder.md} +8 -7
  3. package/.claude/agents/{grader.md → bs-grader.md} +4 -4
  4. package/.claude/agents/{merger.md → bs-merger.md} +4 -4
  5. package/.claude/agents/{planner.md → bs-planner.md} +20 -6
  6. package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
  7. package/.claude/agents/{reviewer.md → bs-reviewer.md} +13 -5
  8. package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
  9. package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +6 -6
  10. package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +15 -10
  11. package/.claude/agents/{tester.md → bs-tester.md} +40 -12
  12. package/.claude/agents/bs-uiux.md +257 -0
  13. package/.claude/agents/{verifier.md → bs-verifier.md} +4 -4
  14. package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
  15. package/.claude/skills/bs/SKILL.md +99 -21
  16. package/.claude/skills/bs/audit.md +34 -27
  17. package/.claude/skills/bs/dispatch.md +100 -50
  18. package/.claude/skills/bs/lessons.md +28 -14
  19. package/.claude/skills/bs/mcp.md +4 -4
  20. package/.claude/skills/bs/new.md +3 -3
  21. package/.claude/skills/bs/plan.md +27 -18
  22. package/.claude/skills/bs/report.md +11 -11
  23. package/.claude/skills/bs/run.md +181 -49
  24. package/.claude/skills/bs/status.md +14 -7
  25. package/.claude/skills/bs/ui.md +21 -5
  26. package/.claude/skills/bs/waivers.md +78 -2
  27. package/.claude/skills/bs/wave.md +109 -53
  28. package/README.md +53 -32
  29. package/factory/orchestrator/dist/agentNames.js +43 -0
  30. package/factory/orchestrator/dist/agents-registry.js +11 -4
  31. package/factory/orchestrator/dist/agentsSync.js +48 -14
  32. package/factory/orchestrator/dist/artifacts.js +31 -0
  33. package/factory/orchestrator/dist/attribution.js +4 -0
  34. package/factory/orchestrator/dist/audit.js +125 -17
  35. package/factory/orchestrator/dist/budgetAlarm.js +132 -37
  36. package/factory/orchestrator/dist/budgets.js +302 -84
  37. package/factory/orchestrator/dist/claims.js +9 -19
  38. package/factory/orchestrator/dist/cli.js +790 -100
  39. package/factory/orchestrator/dist/cliName.js +27 -0
  40. package/factory/orchestrator/dist/crosscheck.js +136 -8
  41. package/factory/orchestrator/dist/daemon.js +154 -36
  42. package/factory/orchestrator/dist/db/projector.js +278 -29
  43. package/factory/orchestrator/dist/db/queries.js +1831 -94
  44. package/factory/orchestrator/dist/db/schema.js +57 -4
  45. package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
  46. package/factory/orchestrator/dist/delegation.js +86 -19
  47. package/factory/orchestrator/dist/diffstat.js +15 -8
  48. package/factory/orchestrator/dist/dispatchLint.js +13 -37
  49. package/factory/orchestrator/dist/effort.js +14 -2
  50. package/factory/orchestrator/dist/effortTiers.js +13 -0
  51. package/factory/orchestrator/dist/env.js +26 -0
  52. package/factory/orchestrator/dist/epic.js +323 -38
  53. package/factory/orchestrator/dist/epicBudget.js +93 -0
  54. package/factory/orchestrator/dist/epicWidth.js +22 -5
  55. package/factory/orchestrator/dist/errorIssues.js +10 -4
  56. package/factory/orchestrator/dist/escalation.js +2 -2
  57. package/factory/orchestrator/dist/events.js +105 -3
  58. package/factory/orchestrator/dist/feedback.js +169 -0
  59. package/factory/orchestrator/dist/findingContext.js +26 -0
  60. package/factory/orchestrator/dist/findings.js +83 -12
  61. package/factory/orchestrator/dist/gate.js +120 -10
  62. package/factory/orchestrator/dist/goalCheck.js +2 -2
  63. package/factory/orchestrator/dist/harness.js +25 -17
  64. package/factory/orchestrator/dist/hookDecision.js +92 -3
  65. package/factory/orchestrator/dist/integration.js +9 -7
  66. package/factory/orchestrator/dist/judgePreflight.js +25 -4
  67. package/factory/orchestrator/dist/judgeRoles.js +47 -0
  68. package/factory/orchestrator/dist/judgeStopHook.js +90 -32
  69. package/factory/orchestrator/dist/judges.js +419 -21
  70. package/factory/orchestrator/dist/lessons.js +190 -11
  71. package/factory/orchestrator/dist/mcp.js +6 -6
  72. package/factory/orchestrator/dist/paths.js +10 -7
  73. package/factory/orchestrator/dist/plan.js +269 -33
  74. package/factory/orchestrator/dist/planQuorum.js +5 -2
  75. package/factory/orchestrator/dist/policy.js +1275 -30
  76. package/factory/orchestrator/dist/projectName.js +25 -0
  77. package/factory/orchestrator/dist/promptCapture.js +306 -0
  78. package/factory/orchestrator/dist/promptHook.js +33 -0
  79. package/factory/orchestrator/dist/promptImport.js +281 -0
  80. package/factory/orchestrator/dist/prompts.js +13 -0
  81. package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
  82. package/factory/orchestrator/dist/providers/types.js +1 -1
  83. package/factory/orchestrator/dist/queue.js +4 -4
  84. package/factory/orchestrator/dist/quorum.js +18 -5
  85. package/factory/orchestrator/dist/results.js +25 -7
  86. package/factory/orchestrator/dist/roadmap.js +10 -5
  87. package/factory/orchestrator/dist/run-cli.js +15 -10
  88. package/factory/orchestrator/dist/runner.js +2 -2
  89. package/factory/orchestrator/dist/schemas.js +56 -8
  90. package/factory/orchestrator/dist/severity.js +8 -4
  91. package/factory/orchestrator/dist/spec.js +177 -13
  92. package/factory/orchestrator/dist/specChange.js +13 -9
  93. package/factory/orchestrator/dist/stack.js +6 -6
  94. package/factory/orchestrator/dist/storeRoot.js +90 -0
  95. package/factory/orchestrator/dist/symbols.js +261 -6
  96. package/factory/orchestrator/dist/taskEvents.js +179 -27
  97. package/factory/orchestrator/dist/taskStatus.js +14 -0
  98. package/factory/orchestrator/dist/testerAudit.js +31 -12
  99. package/factory/orchestrator/dist/testgate.js +16 -13
  100. package/factory/orchestrator/dist/uiuxGate.js +83 -0
  101. package/factory/orchestrator/dist/usage.js +97 -40
  102. package/factory/orchestrator/dist/waivers.js +39 -5
  103. package/factory/orchestrator/dist/waveBudget.js +2 -0
  104. package/factory/orchestrator/dist/waveConcurrency.js +143 -7
  105. package/factory/orchestrator/dist/workroot.js +1 -1
  106. package/factory/orchestrator/dist/worktree.js +61 -19
  107. package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
  108. package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
  109. package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
  110. package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
  111. package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
  112. package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
  113. package/factory/policies/budgets.yml +123 -33
  114. package/factory/policies/crosscheck.yml +51 -13
  115. package/factory/policies/delegation.yml +3 -3
  116. package/factory/policies/effort.yml +18 -10
  117. package/factory/policies/guardrails.yml +8 -6
  118. package/factory/policies/harness.yml +5 -5
  119. package/factory/policies/lessons.md +53 -2
  120. package/factory/policies/scheduler.yml +4 -4
  121. package/factory/policies/sensitive-paths.yml +1 -1
  122. package/factory/policies/severity.yml +13 -1
  123. package/factory/policies/stack.yml +6 -6
  124. package/factory/policies/taxonomy.yml +9 -3
  125. package/factory/policies/worktree.yml +15 -9
  126. package/factory/specs/roadmap.md +7 -7
  127. package/factory/specs/schema/event.schema.json +8 -1
  128. package/factory/specs/schema/result.schema.json +21 -8
  129. package/factory/specs/schema/task-spec.schema.json +12 -0
  130. package/package.json +16 -12
  131. 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 budgets.yml prices under `task.judges` — its line 53 comment is the
5
- * enumeration: "# spec-reviewer, reviewer, verifier, grader".
6
- *
7
- * Hardcoded here rather than added to the YAML as a `roles:` list, for the same
8
- * reason escalation.ts derives BUILDER_ROLES from the policy's own prose: the
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 JUDGE_ROLES = ['spec-reviewer', 'reviewer', 'verifier', 'grader'];
64
+ const WORKER_ROLES = ['coder', 'researcher', 'tester'];
16
65
  function priceRole(role, policy) {
17
- if (role === 'coder')
18
- return { kind: 'worker', capTokens: policy.task.coder.capTokens };
19
- if (role === 'researcher')
20
- return { kind: 'worker', capTokens: policy.task.researcher.capTokens };
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(policy.task.coder.capTokens, policy.task.researcher.capTokens, policy.task.judges.capTokens);
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 usage = payload.token_usage;
124
- if (usage !== null && typeof usage === 'object') {
125
- const total = payloadNumber(usage, 'total_tokens');
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), ${fmt(check.projectedTokens)} projected, against a ` +
172
- `${fmt(check.alarmTokens)} alarm and a ${fmt(check.capTokens)} cap.`;
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
- const alarmTokens = Math.floor(capTokens * alarmRatio);
216
- const largestPrice = largestDeclaredPrice(policy);
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 > largestPrice)
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, policy);
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, alarmTokens, capTokens, rolesWithoutCap.length > 0 || unattributedDispatches > 0 || tasksOverPrice.length > 0);
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
+ }