@juzser/blacksmith 0.1.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 (172) hide show
  1. package/.claude/agents/auditor.md +146 -0
  2. package/.claude/agents/coder.md +164 -0
  3. package/.claude/agents/grader.md +110 -0
  4. package/.claude/agents/merger.md +107 -0
  5. package/.claude/agents/planner.md +92 -0
  6. package/.claude/agents/researcher.md +105 -0
  7. package/.claude/agents/reviewer.md +171 -0
  8. package/.claude/agents/scribe.md +72 -0
  9. package/.claude/agents/security-reviewer.md +130 -0
  10. package/.claude/agents/spec-reviewer.md +215 -0
  11. package/.claude/agents/tester.md +121 -0
  12. package/.claude/agents/uiux.md +126 -0
  13. package/.claude/agents/verifier.md +103 -0
  14. package/.claude/agents/wave-runner.md +74 -0
  15. package/LICENSE +21 -0
  16. package/README.md +389 -0
  17. package/factory/orchestrator/dist/actors.js +46 -0
  18. package/factory/orchestrator/dist/agents-registry.js +308 -0
  19. package/factory/orchestrator/dist/args.js +84 -0
  20. package/factory/orchestrator/dist/artifacts.js +111 -0
  21. package/factory/orchestrator/dist/attribution.js +160 -0
  22. package/factory/orchestrator/dist/audit.js +812 -0
  23. package/factory/orchestrator/dist/autonomy.js +151 -0
  24. package/factory/orchestrator/dist/budgetAlarm.js +366 -0
  25. package/factory/orchestrator/dist/budgets.js +196 -0
  26. package/factory/orchestrator/dist/claims.js +633 -0
  27. package/factory/orchestrator/dist/cli.js +3185 -0
  28. package/factory/orchestrator/dist/commit.js +152 -0
  29. package/factory/orchestrator/dist/coverage.js +188 -0
  30. package/factory/orchestrator/dist/crossFinding.js +479 -0
  31. package/factory/orchestrator/dist/crosscheck.js +396 -0
  32. package/factory/orchestrator/dist/daemon.js +707 -0
  33. package/factory/orchestrator/dist/db/projector.js +1317 -0
  34. package/factory/orchestrator/dist/db/queries.js +1714 -0
  35. package/factory/orchestrator/dist/db/schema.js +305 -0
  36. package/factory/orchestrator/dist/delegation.js +400 -0
  37. package/factory/orchestrator/dist/diffstat.js +164 -0
  38. package/factory/orchestrator/dist/dispatchAudit.js +275 -0
  39. package/factory/orchestrator/dist/dotenv.js +72 -0
  40. package/factory/orchestrator/dist/effort.js +202 -0
  41. package/factory/orchestrator/dist/epic.js +1018 -0
  42. package/factory/orchestrator/dist/epicWidth.js +238 -0
  43. package/factory/orchestrator/dist/errorIssues.js +249 -0
  44. package/factory/orchestrator/dist/errors.js +11 -0
  45. package/factory/orchestrator/dist/escalation.js +500 -0
  46. package/factory/orchestrator/dist/events.js +1015 -0
  47. package/factory/orchestrator/dist/findingAge.js +91 -0
  48. package/factory/orchestrator/dist/findingContext.js +103 -0
  49. package/factory/orchestrator/dist/findings.js +1156 -0
  50. package/factory/orchestrator/dist/gate.js +911 -0
  51. package/factory/orchestrator/dist/gh.js +146 -0
  52. package/factory/orchestrator/dist/git.js +87 -0
  53. package/factory/orchestrator/dist/goalCheck.js +380 -0
  54. package/factory/orchestrator/dist/graph.js +61 -0
  55. package/factory/orchestrator/dist/hookDecision.js +106 -0
  56. package/factory/orchestrator/dist/immutability.js +171 -0
  57. package/factory/orchestrator/dist/impact.js +280 -0
  58. package/factory/orchestrator/dist/integration.js +141 -0
  59. package/factory/orchestrator/dist/judgePreflight.js +155 -0
  60. package/factory/orchestrator/dist/judges.js +310 -0
  61. package/factory/orchestrator/dist/lessonAudit.js +399 -0
  62. package/factory/orchestrator/dist/lessons.js +1329 -0
  63. package/factory/orchestrator/dist/mcp.js +555 -0
  64. package/factory/orchestrator/dist/paths.js +82 -0
  65. package/factory/orchestrator/dist/plan.js +502 -0
  66. package/factory/orchestrator/dist/planQuorum.js +421 -0
  67. package/factory/orchestrator/dist/policy.js +1308 -0
  68. package/factory/orchestrator/dist/policyHook.js +28 -0
  69. package/factory/orchestrator/dist/preconditions.js +47 -0
  70. package/factory/orchestrator/dist/projects.js +134 -0
  71. package/factory/orchestrator/dist/prompts.js +57 -0
  72. package/factory/orchestrator/dist/provenance.js +201 -0
  73. package/factory/orchestrator/dist/providers/api-transport.js +117 -0
  74. package/factory/orchestrator/dist/providers/cli-transport.js +208 -0
  75. package/factory/orchestrator/dist/providers/index.js +54 -0
  76. package/factory/orchestrator/dist/providers/schema-validate.js +152 -0
  77. package/factory/orchestrator/dist/providers/types.js +49 -0
  78. package/factory/orchestrator/dist/queue.js +263 -0
  79. package/factory/orchestrator/dist/quorum.js +347 -0
  80. package/factory/orchestrator/dist/quorumEscalations.js +132 -0
  81. package/factory/orchestrator/dist/results.js +65 -0
  82. package/factory/orchestrator/dist/roadmap.js +437 -0
  83. package/factory/orchestrator/dist/runtime.js +86 -0
  84. package/factory/orchestrator/dist/sameMistakeKpi.js +243 -0
  85. package/factory/orchestrator/dist/sandbox.js +165 -0
  86. package/factory/orchestrator/dist/scaffold.js +373 -0
  87. package/factory/orchestrator/dist/scheduler.js +408 -0
  88. package/factory/orchestrator/dist/schemas.js +195 -0
  89. package/factory/orchestrator/dist/security.js +222 -0
  90. package/factory/orchestrator/dist/severity.js +293 -0
  91. package/factory/orchestrator/dist/spec.js +376 -0
  92. package/factory/orchestrator/dist/specChange.js +497 -0
  93. package/factory/orchestrator/dist/stack.js +324 -0
  94. package/factory/orchestrator/dist/symbols.js +966 -0
  95. package/factory/orchestrator/dist/taskEvents.js +399 -0
  96. package/factory/orchestrator/dist/taskId.js +126 -0
  97. package/factory/orchestrator/dist/taxonomy.js +113 -0
  98. package/factory/orchestrator/dist/testSelect.js +129 -0
  99. package/factory/orchestrator/dist/testerAudit.js +264 -0
  100. package/factory/orchestrator/dist/testgate.js +171 -0
  101. package/factory/orchestrator/dist/usage.js +874 -0
  102. package/factory/orchestrator/dist/waivers.js +228 -0
  103. package/factory/orchestrator/dist/waveBudget.js +223 -0
  104. package/factory/orchestrator/dist/waveConcurrency.js +250 -0
  105. package/factory/orchestrator/dist/waveNext.js +210 -0
  106. package/factory/orchestrator/dist/waveSchedule.js +146 -0
  107. package/factory/orchestrator/dist/worktree.js +207 -0
  108. package/factory/orchestrator/drizzle/0000_broken_rockslide.sql +183 -0
  109. package/factory/orchestrator/drizzle/0001_luxuriant_zaladane.sql +1 -0
  110. package/factory/orchestrator/drizzle/0002_yellow_wild_pack.sql +8 -0
  111. package/factory/orchestrator/drizzle/0003_damp_riptide.sql +12 -0
  112. package/factory/orchestrator/drizzle/0004_broad_lady_vermin.sql +1 -0
  113. package/factory/orchestrator/drizzle/0005_romantic_jocasta.sql +2 -0
  114. package/factory/orchestrator/drizzle/0006_bent_madelyne_pryor.sql +16 -0
  115. package/factory/orchestrator/drizzle/0007_overjoyed_goliath.sql +1 -0
  116. package/factory/orchestrator/drizzle/0008_smart_iron_patriot.sql +2 -0
  117. package/factory/orchestrator/drizzle/0009_agents_epic_id.sql +2 -0
  118. package/factory/orchestrator/drizzle/0010_milestone_kind.sql +1 -0
  119. package/factory/orchestrator/drizzle/meta/_journal.json +83 -0
  120. package/factory/policies/budgets.yml +262 -0
  121. package/factory/policies/crosscheck.yml +308 -0
  122. package/factory/policies/delegation.yml +44 -0
  123. package/factory/policies/effort.yml +142 -0
  124. package/factory/policies/guardrails.yml +306 -0
  125. package/factory/policies/lessons.md +239 -0
  126. package/factory/policies/scheduler.yml +90 -0
  127. package/factory/policies/sensitive-paths.yml +148 -0
  128. package/factory/policies/severity.yml +83 -0
  129. package/factory/policies/stack.yml +60 -0
  130. package/factory/policies/taxonomy.yml +340 -0
  131. package/factory/policies/worktree.yml +95 -0
  132. package/factory/scaffold/base/.github/workflows/ci.yml +34 -0
  133. package/factory/scaffold/base/.gitignore.tmpl +11 -0
  134. package/factory/scaffold/base/AGENTS.md +33 -0
  135. package/factory/scaffold/base/README.md +25 -0
  136. package/factory/scaffold/base/biome.json.tmpl +30 -0
  137. package/factory/scaffold/base/package.json +25 -0
  138. package/factory/scaffold/base/pnpm-workspace.yaml.tmpl +16 -0
  139. package/factory/scaffold/base/src/index.ts +4 -0
  140. package/factory/scaffold/base/test/index.test.ts +8 -0
  141. package/factory/scaffold/base/tsconfig.json +20 -0
  142. package/factory/scaffold/base/tsconfig.test.json +9 -0
  143. package/factory/scaffold/base/vitest.config.ts +29 -0
  144. package/factory/scaffold/mcp/mcp.manifest.json +22 -0
  145. package/factory/scaffold/mcp/package.fragment.json +9 -0
  146. package/factory/scaffold/mcp/src/mcp/README.md +63 -0
  147. package/factory/scaffold/mcp/src/mcp/guard.ts +110 -0
  148. package/factory/scaffold/mcp/src/mcp/redact.ts +69 -0
  149. package/factory/scaffold/mcp/src/mcp/server.ts +55 -0
  150. package/factory/scaffold/mcp/src/mcp/tools/health.ts +60 -0
  151. package/factory/scaffold/mcp/test/mcp/guard.test.ts +108 -0
  152. package/factory/scaffold/mcp/test/mcp/redact.test.ts +61 -0
  153. package/factory/scaffold/mcp/test/mcp/surface.test.ts +100 -0
  154. package/factory/scaffold/ui/index.html +12 -0
  155. package/factory/scaffold/ui/package.fragment.json +13 -0
  156. package/factory/scaffold/ui/src/App.vue +10 -0
  157. package/factory/scaffold/ui/src/main.ts +5 -0
  158. package/factory/scaffold/ui/src/shims.d.ts +11 -0
  159. package/factory/scaffold/ui/vite.config.ts +20 -0
  160. package/factory/scaffold/ui-tailwind/package.fragment.json +6 -0
  161. package/factory/scaffold/ui-tailwind/vite.config.ts +26 -0
  162. package/factory/specs/schema/event.schema.json +82 -0
  163. package/factory/specs/schema/finding-evidence.schema.json +45 -0
  164. package/factory/specs/schema/finding.schema.json +115 -0
  165. package/factory/specs/schema/grader-verdict.schema.json +50 -0
  166. package/factory/specs/schema/judge-verdict.schema.json +19 -0
  167. package/factory/specs/schema/lesson.schema.json +85 -0
  168. package/factory/specs/schema/mcp-manifest.schema.json +230 -0
  169. package/factory/specs/schema/result.schema.json +81 -0
  170. package/factory/specs/schema/spec-change-request.schema.json +46 -0
  171. package/factory/specs/schema/task-spec.schema.json +134 -0
  172. package/package.json +98 -0
@@ -0,0 +1,196 @@
1
+ // factory/policies/budgets.yml loader. Same parse-with-defaults pattern as
2
+ // crosscheck.ts/scheduler.ts: a RawXYaml on-disk snake_case shape mapped onto a
3
+ // typed camelCase policy.
4
+ //
5
+ // `epic.cap_tokens` is the field src/planQuorum.ts's budget trigger
6
+ // (crosscheck.yml plan_quorum.budget_ratio) reads, via the sum of *declared*
7
+ // task budgets. The per-role caps below are modeled as of P9-18 so the gate can
8
+ // compare a declared task budget against what the task actually spent — see
9
+ // checkTaskBudget. `escalation_ladder` is parsed as of P9-32 and asserted
10
+ // against the event log by src/escalation.ts. `context_window` and
11
+ // `effort_scaling` are still prompt-level: agents read them from their own
12
+ // templates, and nothing here parses them.
13
+ import { readFileSync } from 'node:fs';
14
+ import { parse as parseYaml } from 'yaml';
15
+ import { SmithError } from './errors.js';
16
+ import { BUDGETS_POLICY_PATH } from './paths.js';
17
+ export class BudgetError extends SmithError {
18
+ }
19
+ /**
20
+ * Deliberately NOT the number budgets.yml declares (4,000,000 since
21
+ * 2026-08-11) — the one default here that does not mirror the policy file.
22
+ *
23
+ * The other DEFAULT_* constants below match budgets.yml because nobody has
24
+ * ever had cause to diverge them. This one has cause: the repo's cap was
25
+ * raised by an operator decision sized from a single dogfood epic that
26
+ * measured 1,529,963 tokens for two of four tasks, a figure inflated by
27
+ * rework `smith escalation check` recorded as ladder violations. That is a
28
+ * judgement about this repo's epics, not a floor to hand a project that
29
+ * shipped no policy at all — such a project gets the conservative number and
30
+ * an alarm that fires early, which is the failure direction to prefer.
31
+ *
32
+ * Do not "fix" the mismatch by syncing them.
33
+ */
34
+ const DEFAULT_EPIC_CAP_TOKENS = 2_000_000;
35
+ const DEFAULT_EPIC_ALARM_RATIO = 0.7;
36
+ const DEFAULT_CODER_CAP_TOKENS = 150_000;
37
+ const DEFAULT_CODER_CAP_DIFF_LINES = 400;
38
+ const DEFAULT_RESEARCHER_CAP_TOKENS = 60_000;
39
+ const DEFAULT_JUDGES_CAP_TOKENS = 40_000;
40
+ const DEFAULT_PRE_CODE_SHARE_MAX = 0.15;
41
+ function optionalString(value) {
42
+ return typeof value === 'string' && value.length > 0 ? value : null;
43
+ }
44
+ /**
45
+ * The ladder in ascending threshold order.
46
+ *
47
+ * There is no default ladder. A budgets.yml that declares none yields `[]`,
48
+ * and src/escalation.ts reports that as unverifiable — inventing the three
49
+ * rungs here would let a policy file nobody wrote produce a clean audit.
50
+ * Rungs with no numeric threshold sort last, after every rung that has one.
51
+ */
52
+ function parseEscalationLadder(raw) {
53
+ const rungs = (raw ?? []).map((entry, index) => ({
54
+ rung: typeof entry.rung === 'number' ? entry.rung : index + 1,
55
+ failedRounds: typeof entry.failed_rounds === 'number' ? entry.failed_rounds : null,
56
+ trigger: optionalString(entry.trigger) ?? '',
57
+ action: optionalString(entry.action),
58
+ enforce: optionalString(entry.enforce),
59
+ }));
60
+ return rungs.sort((a, b) => {
61
+ if (a.failedRounds === b.failedRounds)
62
+ return a.rung - b.rung;
63
+ if (a.failedRounds === null)
64
+ return 1;
65
+ if (b.failedRounds === null)
66
+ return -1;
67
+ return a.failedRounds - b.failedRounds;
68
+ });
69
+ }
70
+ /**
71
+ * Every cap, checked rather than trusted -- the same guard crosscheck.ts and
72
+ * scheduler.ts carry for their own policy files, and the likeliest of the
73
+ * three to actually be typed: `200k` and `80%` are how a human writes these
74
+ * numbers everywhere except here.
75
+ *
76
+ * `??` fills in on null/undefined only, so a YAML typo arrived at the
77
+ * comparison as itself while the declared type still said `number`. Nothing
78
+ * threw, and a cap that no comparison can read is not a loose cap -- it is no
79
+ * cap at all:
80
+ *
81
+ * - `cap_tokens: 200k` -- `10_000_000 >= '200k'` is false, so an epic ten
82
+ * times over budget is reported `under`.
83
+ * - `alarm_ratio: 80%` -- checkBudgetAlarm computes
84
+ * `Math.floor(capTokens * alarmRatio)`, so the alarm threshold is NaN and
85
+ * every comparison against it is false.
86
+ * - `cap_diff_lines: four hundred` -- a 99,999-line diff is under cap.
87
+ *
88
+ * The alarm case is the one that outlives its typo: NaN serialises to null,
89
+ * so the report records that there was no alarm threshold rather than that
90
+ * the policy could not be read. Same reason cli.ts refuses a `--now` it
91
+ * cannot parse instead of writing `Math.floor(NaN)` into an append-only log
92
+ * (D-209).
93
+ *
94
+ * A wrong *number* still passes: a 10-token cap stops the very first task and
95
+ * says so. A wrong *type* stops nothing and says nothing.
96
+ */
97
+ const NO_CAP_AT_ALL = 'A cap the comparison cannot read is not a loose cap, it is no cap at all.';
98
+ function capNumber(field, value) {
99
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
100
+ throw new BudgetError('budgets.invalid-policy', `budgets.yml ${field} must be a finite number; got ${JSON.stringify(value)}. ${NO_CAP_AT_ALL}`, { field, value });
101
+ }
102
+ return value;
103
+ }
104
+ /**
105
+ * Same guard as capNumber, for the one field allowed to say "no cap" rather
106
+ * than declare a number: `epic.max_in_flight_tasks` is null by default (see
107
+ * budgets.yml's own comment on the field), and null has to reach
108
+ * checkWaveBudget as the deliberate "no fan-out limit" it is, not as a value
109
+ * that failed capNumber's finite-number check. Anything else that is not a
110
+ * finite number — `"none"`, `80%`, a typo'd string — is the same "no cap at
111
+ * all" failure capNumber guards against, so it throws the same way.
112
+ */
113
+ function capNumberOrNull(field, value) {
114
+ if (value === null)
115
+ return null;
116
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
117
+ throw new BudgetError('budgets.invalid-policy', `budgets.yml ${field} must be a finite number or null; got ${JSON.stringify(value)}. ${NO_CAP_AT_ALL}`, { field, value });
118
+ }
119
+ return value;
120
+ }
121
+ export function parseBudgetPolicy(yamlText) {
122
+ const doc = (parseYaml(yamlText) ?? {});
123
+ return {
124
+ epic: {
125
+ capTokens: capNumber('epic.cap_tokens', doc.epic?.cap_tokens ?? DEFAULT_EPIC_CAP_TOKENS),
126
+ alarmRatio: capNumber('epic.alarm_ratio', doc.epic?.alarm_ratio ?? DEFAULT_EPIC_ALARM_RATIO),
127
+ maxInFlightTasks: capNumberOrNull('epic.max_in_flight_tasks', doc.epic?.max_in_flight_tasks ?? null),
128
+ },
129
+ task: {
130
+ coder: {
131
+ capTokens: capNumber('task.coder.cap_tokens', doc.task?.coder?.cap_tokens ?? DEFAULT_CODER_CAP_TOKENS),
132
+ capDiffLines: capNumber('task.coder.cap_diff_lines', doc.task?.coder?.cap_diff_lines ?? DEFAULT_CODER_CAP_DIFF_LINES),
133
+ },
134
+ researcher: {
135
+ capTokens: capNumber('task.researcher.cap_tokens', doc.task?.researcher?.cap_tokens ?? DEFAULT_RESEARCHER_CAP_TOKENS),
136
+ },
137
+ judges: {
138
+ capTokens: capNumber('task.judges.cap_tokens', doc.task?.judges?.cap_tokens ?? DEFAULT_JUDGES_CAP_TOKENS),
139
+ },
140
+ },
141
+ preCodeBudget: {
142
+ shareOfEpicBudgetMax: capNumber('pre_code_budget.share_of_epic_budget_max', doc.pre_code_budget?.share_of_epic_budget_max ?? DEFAULT_PRE_CODE_SHARE_MAX),
143
+ },
144
+ escalationLadder: parseEscalationLadder(doc.escalation_ladder?.rungs),
145
+ };
146
+ }
147
+ export function loadBudgetPolicy(filePath = BUDGETS_POLICY_PATH) {
148
+ return parseBudgetPolicy(readFileSync(filePath, 'utf8'));
149
+ }
150
+ /**
151
+ * For each field a task budget may declare, the mechanism that reads it — or
152
+ * `null` when nothing does.
153
+ *
154
+ * This registry exists so the answer is checkable rather than remembered.
155
+ * D-29's finding was that all three fields looked enforced and none were; the
156
+ * fix is not only to wire two of them up but to make the third's emptiness
157
+ * visible, so plan validation can say "you wrote down a limit nobody can
158
+ * apply" instead of the plan quietly implying otherwise.
159
+ *
160
+ * `max_turns` is null on purpose, and not because the wiring is pending: a turn
161
+ * limit has to live in the dispatch harness, and the Agent tool exposes no
162
+ * turns parameter (agent-interviews.md M-4, answered (c) on 2026-08-05 — the
163
+ * number is carried into the dispatch prompt by the /bs skill, where it is a
164
+ * request to the agent, not a limit on it).
165
+ */
166
+ export const TASK_BUDGET_FIELD_READERS = Object.freeze({
167
+ tokens: 'gate: compared against the result’s token_usage.total_tokens',
168
+ diff_lines: 'gate: compared against the measured git diff (src/diffstat.ts)',
169
+ max_turns: null,
170
+ });
171
+ /**
172
+ * Which of the fields this budget declares have no mechanical reader. Order
173
+ * follows TASK_BUDGET_FIELD_READERS so the message is stable.
174
+ */
175
+ export function unreadTaskBudgetFields(budget) {
176
+ return Object.keys(TASK_BUDGET_FIELD_READERS).filter((field) => budget[field] !== undefined && TASK_BUDGET_FIELD_READERS[field] === null);
177
+ }
178
+ /**
179
+ * Declared budget vs. what the task actually spent.
180
+ *
181
+ * A field with no measurement is skipped rather than compared against zero: an
182
+ * unmeasured budget and a budget that came in at nothing are different facts,
183
+ * and only one of them is evidence of anything. Spend exactly at the cap is
184
+ * within it — a cap of 400 permits 400.
185
+ */
186
+ export function checkTaskBudget(input) {
187
+ const overruns = [];
188
+ const { budget, tokensUsed, diffLines } = input;
189
+ if (tokensUsed !== undefined && tokensUsed > budget.tokens) {
190
+ overruns.push({ field: 'tokens', cap: budget.tokens, measured: tokensUsed });
191
+ }
192
+ if (diffLines !== undefined && diffLines > budget.diff_lines) {
193
+ overruns.push({ field: 'diff_lines', cap: budget.diff_lines, measured: diffLines });
194
+ }
195
+ return overruns;
196
+ }