@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.
Files changed (141) hide show
  1. package/.claude/agents/{auditor.md → bs-auditor.md} +8 -5
  2. package/.claude/agents/{coder.md → bs-coder.md} +8 -7
  3. package/.claude/agents/{grader.md → bs-grader.md} +16 -9
  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} +16 -5
  8. package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
  9. package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +9 -6
  10. package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +18 -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} +7 -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 +45 -31
  17. package/.claude/skills/bs/dispatch.md +133 -47
  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 +57 -16
  22. package/.claude/skills/bs/report.md +11 -11
  23. package/.claude/skills/bs/run.md +190 -52
  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 +143 -50
  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 +70 -13
  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 +245 -20
  35. package/factory/orchestrator/dist/autonomy.js +6 -2
  36. package/factory/orchestrator/dist/budgetAlarm.js +132 -37
  37. package/factory/orchestrator/dist/budgets.js +302 -84
  38. package/factory/orchestrator/dist/claims.js +9 -19
  39. package/factory/orchestrator/dist/cli.js +1087 -116
  40. package/factory/orchestrator/dist/cliName.js +27 -0
  41. package/factory/orchestrator/dist/crossFinding.js +20 -0
  42. package/factory/orchestrator/dist/crosscheck.js +136 -8
  43. package/factory/orchestrator/dist/daemon.js +352 -67
  44. package/factory/orchestrator/dist/db/projector.js +327 -54
  45. package/factory/orchestrator/dist/db/queries.js +1831 -94
  46. package/factory/orchestrator/dist/db/schema.js +57 -4
  47. package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
  48. package/factory/orchestrator/dist/delegation.js +86 -19
  49. package/factory/orchestrator/dist/diffstat.js +15 -8
  50. package/factory/orchestrator/dist/dispatchLint.js +108 -0
  51. package/factory/orchestrator/dist/effort.js +14 -2
  52. package/factory/orchestrator/dist/effortTiers.js +13 -0
  53. package/factory/orchestrator/dist/env.js +26 -0
  54. package/factory/orchestrator/dist/epic.js +494 -42
  55. package/factory/orchestrator/dist/epicBudget.js +93 -0
  56. package/factory/orchestrator/dist/epicWidth.js +22 -5
  57. package/factory/orchestrator/dist/errorIssues.js +182 -11
  58. package/factory/orchestrator/dist/escalation.js +2 -2
  59. package/factory/orchestrator/dist/events.js +164 -14
  60. package/factory/orchestrator/dist/feedback.js +169 -0
  61. package/factory/orchestrator/dist/findingContext.js +26 -0
  62. package/factory/orchestrator/dist/findings.js +108 -14
  63. package/factory/orchestrator/dist/gate.js +120 -10
  64. package/factory/orchestrator/dist/gh.js +19 -2
  65. package/factory/orchestrator/dist/git.js +20 -2
  66. package/factory/orchestrator/dist/goalCheck.js +2 -2
  67. package/factory/orchestrator/dist/harness.js +25 -17
  68. package/factory/orchestrator/dist/hookDecision.js +411 -39
  69. package/factory/orchestrator/dist/integration.js +20 -8
  70. package/factory/orchestrator/dist/issueReporter.js +81 -16
  71. package/factory/orchestrator/dist/judgePreflight.js +25 -4
  72. package/factory/orchestrator/dist/judgeRoles.js +47 -0
  73. package/factory/orchestrator/dist/judgeStopHook.js +239 -0
  74. package/factory/orchestrator/dist/judges.js +419 -21
  75. package/factory/orchestrator/dist/lessons.js +190 -11
  76. package/factory/orchestrator/dist/logCache.js +291 -0
  77. package/factory/orchestrator/dist/mcp.js +6 -6
  78. package/factory/orchestrator/dist/paths.js +20 -7
  79. package/factory/orchestrator/dist/plan.js +399 -33
  80. package/factory/orchestrator/dist/planQuorum.js +5 -2
  81. package/factory/orchestrator/dist/policy.js +1275 -30
  82. package/factory/orchestrator/dist/projectName.js +25 -0
  83. package/factory/orchestrator/dist/projects.js +12 -1
  84. package/factory/orchestrator/dist/promptCapture.js +306 -0
  85. package/factory/orchestrator/dist/promptHook.js +33 -0
  86. package/factory/orchestrator/dist/promptImport.js +281 -0
  87. package/factory/orchestrator/dist/prompts.js +13 -0
  88. package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
  89. package/factory/orchestrator/dist/providers/types.js +1 -1
  90. package/factory/orchestrator/dist/queue.js +471 -30
  91. package/factory/orchestrator/dist/quorum.js +18 -5
  92. package/factory/orchestrator/dist/results.js +25 -7
  93. package/factory/orchestrator/dist/roadmap.js +10 -5
  94. package/factory/orchestrator/dist/run-cli.js +15 -10
  95. package/factory/orchestrator/dist/runner.js +2 -2
  96. package/factory/orchestrator/dist/scheduler.js +3 -3
  97. package/factory/orchestrator/dist/schemas.js +56 -8
  98. package/factory/orchestrator/dist/severity.js +8 -4
  99. package/factory/orchestrator/dist/spec.js +246 -12
  100. package/factory/orchestrator/dist/specChange.js +13 -9
  101. package/factory/orchestrator/dist/stack.js +6 -6
  102. package/factory/orchestrator/dist/storeRoot.js +90 -0
  103. package/factory/orchestrator/dist/symbols.js +261 -6
  104. package/factory/orchestrator/dist/taskEvents.js +192 -31
  105. package/factory/orchestrator/dist/taskStatus.js +14 -0
  106. package/factory/orchestrator/dist/taxonomy.js +11 -0
  107. package/factory/orchestrator/dist/testerAudit.js +31 -12
  108. package/factory/orchestrator/dist/testgate.js +20 -16
  109. package/factory/orchestrator/dist/uiuxGate.js +83 -0
  110. package/factory/orchestrator/dist/usage.js +113 -48
  111. package/factory/orchestrator/dist/waivers.js +99 -12
  112. package/factory/orchestrator/dist/waveBudget.js +2 -0
  113. package/factory/orchestrator/dist/waveConcurrency.js +143 -7
  114. package/factory/orchestrator/dist/waveSchedule.js +84 -0
  115. package/factory/orchestrator/dist/workroot.js +1 -1
  116. package/factory/orchestrator/dist/worktree.js +76 -21
  117. package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
  118. package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
  119. package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
  120. package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
  121. package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
  122. package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
  123. package/factory/policies/budgets.yml +123 -33
  124. package/factory/policies/crosscheck.yml +51 -13
  125. package/factory/policies/delegation.yml +3 -3
  126. package/factory/policies/effort.yml +18 -10
  127. package/factory/policies/guardrails.yml +8 -6
  128. package/factory/policies/harness.yml +5 -5
  129. package/factory/policies/lessons.md +53 -2
  130. package/factory/policies/scheduler.yml +4 -4
  131. package/factory/policies/sensitive-paths.yml +1 -1
  132. package/factory/policies/severity.yml +13 -1
  133. package/factory/policies/stack.yml +6 -6
  134. package/factory/policies/taxonomy.yml +9 -3
  135. package/factory/policies/worktree.yml +15 -9
  136. package/factory/specs/roadmap.md +27 -3
  137. package/factory/specs/schema/event.schema.json +8 -1
  138. package/factory/specs/schema/result.schema.json +21 -8
  139. package/factory/specs/schema/task-spec.schema.json +12 -0
  140. package/package.json +16 -12
  141. package/.claude/agents/uiux.md +0 -133
@@ -3,7 +3,7 @@ import { SmithError } from './errors.js';
3
3
  import { runGit as git } from './git.js';
4
4
  export class WorktreeError extends SmithError {
5
5
  }
6
- /** The task-id segment reserved for an epic's integration branch (smith/<epic>/integration) —
6
+ /** The task-id segment reserved for an epic's integration branch (<prefix>/<epic>/integration) —
7
7
  * never a real task. Exported for db/projector.ts's foldTasks() guard and epic.ts's
8
8
  * runEpicVerdict(), both of which need to recognise `<epic>/integration` as non-task. */
9
9
  export const RESERVED_TASK_ID = 'integration';
@@ -16,6 +16,19 @@ function localBranchExists(projectDir, branch) {
16
16
  return false;
17
17
  }
18
18
  }
19
+ /**
20
+ * Whether a task's own branch (`taskBranchName`) already exists locally —
21
+ * i.e. whether the task has already been cut. Exported for cli.ts's
22
+ * `worktree create --from` guard: spec.ts's `successorChainPath` hands back
23
+ * the bare hop ids between a `--from` predecessor and its target purely from
24
+ * the event log, with no notion of git state; this is the git-aware
25
+ * predicate the guard pairs it with to refuse skipping past a hop that was
26
+ * already cut (`createTaskWorktree` branches straight off `--from`'s own
27
+ * branch, so a cut hop's commits would otherwise vanish silently).
28
+ */
29
+ export function taskBranchExists(projectDir, epic, taskId) {
30
+ return localBranchExists(projectDir, taskBranchName(epic, taskId, epicBranchPrefix(projectDir, epic)));
31
+ }
19
32
  /**
20
33
  * Best-effort default-branch detection: origin/HEAD symbolic ref first (the
21
34
  * normal case after a clone), then whatever branch is currently checked out,
@@ -92,13 +105,39 @@ export function taskWorktreeDir(projectDir, epic, taskId) {
92
105
  const project = path.resolve(projectDir);
93
106
  return path.join(path.dirname(project), '.wt', path.basename(project), bareTaskId(epic, taskId));
94
107
  }
95
- /** `smith/<epic>/integration` — the one branch an epic assembles onto. Exported for
96
- * integration.ts, which runs the check suite against it (D-42/P9-26). */
97
- export function integrationBranchName(epic) {
98
- return `smith/${epic}/integration`;
108
+ /** The branch prefix every new epic gets (bs-rename operator decision 1). */
109
+ export const BRANCH_PREFIX = 'bs';
110
+ /** The prefix `BRANCH_PREFIX` replaced. Still recognised so an epic already
111
+ * cut under it keeps landing new branches there (operator decision 3) rather
112
+ * than splitting one epic's worktrees across two branch families. */
113
+ export const LEGACY_BRANCH_PREFIX = 'smith';
114
+ /**
115
+ * Which prefix THIS epic's factory branches use: `smith` when
116
+ * `smith/<epic>/integration` already exists locally, `bs` otherwise —
117
+ * including for a brand-new epic, which has neither yet. Git-aware on
118
+ * purpose: this is the one place that decides, so integrationBranchName and
119
+ * taskBranchName can stay pure string joins driven by whatever prefix their
120
+ * caller resolved (or hands the bs default straight through, for the
121
+ * git-free callers — taskEvents.ts and db/projector.ts's fallback — that
122
+ * have no projectDir to ask).
123
+ */
124
+ export function epicBranchPrefix(projectDir, epic) {
125
+ return localBranchExists(projectDir, `${LEGACY_BRANCH_PREFIX}/${epic}/${RESERVED_TASK_ID}`)
126
+ ? LEGACY_BRANCH_PREFIX
127
+ : BRANCH_PREFIX;
128
+ }
129
+ /** `<prefix>/<epic>/integration` — the one branch an epic assembles onto.
130
+ * `prefix` defaults to BRANCH_PREFIX (`bs`); a caller with a projectDir
131
+ * passes `epicBranchPrefix(projectDir, epic)` to get the epic's actual
132
+ * prefix, legacy `smith/` included. Exported for integration.ts, which runs
133
+ * the check suite against it (D-42/P9-26). */
134
+ export function integrationBranchName(epic, prefix = BRANCH_PREFIX) {
135
+ return `${prefix}/${epic}/integration`;
99
136
  }
100
137
  /**
101
- * `smith/<epic>/<bare-task-id>` — the branch convention, defined once.
138
+ * `<prefix>/<epic>/<bare-task-id>` — the branch convention, defined once.
139
+ * `prefix` defaults to BRANCH_PREFIX (`bs`) for the same reason as
140
+ * integrationBranchName above.
102
141
  *
103
142
  * D-23/P9-12: exported because three places need to agree on it — this module
104
143
  * (which creates the branch), taskEvents.ts's `task-added` (which declares it
@@ -108,14 +147,14 @@ export function integrationBranchName(epic) {
108
147
  *
109
148
  * A task_id already embeds its epic ("epic-7/task-142", findings.ts's
110
149
  * convention) so strip that prefix before rejoining — concatenating epic +
111
- * full task_id double-embeds it (smith/epic-7/epic-7/task-142). The CLI hands
150
+ * full task_id double-embeds it (bs/epic-7/epic-7/task-142). The CLI hands
112
151
  * this whatever the operator typed, so both shapes really do arrive.
113
152
  */
114
- export function taskBranchName(epic, taskId) {
115
- return `smith/${epic}/${bareTaskId(epic, taskId)}`;
153
+ export function taskBranchName(epic, taskId, prefix = BRANCH_PREFIX) {
154
+ return `${prefix}/${epic}/${bareTaskId(epic, taskId)}`;
116
155
  }
117
156
  function ensureIntegrationBranch(projectDir, epic) {
118
- const branch = integrationBranchName(epic);
157
+ const branch = integrationBranchName(epic, epicBranchPrefix(projectDir, epic));
119
158
  if (!localBranchExists(projectDir, branch)) {
120
159
  const defaultBranch = detectDefaultBranch(projectDir);
121
160
  git(projectDir, ['branch', branch, defaultBranch]);
@@ -127,24 +166,39 @@ function ensureIntegrationBranch(projectDir, epic) {
127
166
  * sibling of whatever directory the project was handed as, never a path of
128
167
  * this module's own choosing, so a project outside this clone keeps its
129
168
  * worktrees outside it too (see taskWorktreeDir)
130
- * on branch smith/<epic>/<task-id>, cut from the CURRENT head of
131
- * smith/<epic>/integration (creating the integration branch from the default
132
- * branch first if it doesn't exist yet).
169
+ * on branch <prefix>/<epic>/<task-id>, where <prefix> is epicBranchPrefix's
170
+ * answer for this epic (`bs`, or `smith` for an epic already cut under it).
171
+ *
172
+ * Without `opts.from`, cut from the CURRENT head of <prefix>/<epic>/integration
173
+ * (creating the integration branch from the default branch first if it
174
+ * doesn't exist yet). With `opts.from`, cut from <prefix>/<epic>/<from> instead
175
+ * — the predecessor's branch, commits and all — and integration is never
176
+ * consulted.
133
177
  */
134
- export function createTaskWorktree(projectDir, epic, taskId) {
178
+ export function createTaskWorktree(projectDir, epic, taskId, opts = {}) {
135
179
  if (taskId === RESERVED_TASK_ID) {
136
180
  throw new WorktreeError('worktree.reserved-task-id', `Task id "${RESERVED_TASK_ID}" is reserved for the integration branch itself.`, { taskId });
137
181
  }
138
- const integrationBranch = ensureIntegrationBranch(projectDir, epic);
139
- const branch = taskBranchName(epic, taskId);
182
+ const prefix = epicBranchPrefix(projectDir, epic);
183
+ const branch = taskBranchName(epic, taskId, prefix);
140
184
  const worktreeDir = taskWorktreeDir(projectDir, epic, taskId);
141
- git(projectDir, ['worktree', 'add', '-b', branch, worktreeDir, integrationBranch]);
185
+ let sourceBranch;
186
+ if (opts.from !== undefined) {
187
+ sourceBranch = taskBranchName(epic, opts.from, prefix);
188
+ if (!localBranchExists(projectDir, sourceBranch)) {
189
+ throw new WorktreeError('worktree.from-missing', `Predecessor branch ${sourceBranch} does not exist; cannot cut ${taskId} from it.`, { epic, taskId, from: opts.from, branch: sourceBranch });
190
+ }
191
+ }
192
+ else {
193
+ sourceBranch = ensureIntegrationBranch(projectDir, epic);
194
+ }
195
+ git(projectDir, ['worktree', 'add', '-b', branch, worktreeDir, sourceBranch]);
142
196
  return { worktreeDir, branch, epic, taskId };
143
197
  }
144
198
  /** Remove a task's worktree and its local branch after it has merged. */
145
199
  export function removeTaskWorktree(projectDir, epic, taskId) {
146
200
  const worktreeDir = taskWorktreeDir(projectDir, epic, taskId);
147
- const branch = taskBranchName(epic, taskId);
201
+ const branch = taskBranchName(epic, taskId, epicBranchPrefix(projectDir, epic));
148
202
  try {
149
203
  git(projectDir, ['worktree', 'remove', worktreeDir]);
150
204
  }
@@ -180,12 +234,13 @@ function parseWorktreePorcelain(output) {
180
234
  return entries;
181
235
  }
182
236
  /**
183
- * List task worktrees whose branch is fully merged into smith/<epic>/integration.
237
+ * List task worktrees whose branch is fully merged into <prefix>/<epic>/integration.
184
238
  * Reports only — never deletes.
185
239
  */
186
240
  export function listStale(projectDir, epic) {
187
- const integrationBranch = integrationBranchName(epic);
188
- const prefix = `smith/${epic}/`;
241
+ const branchPrefix = epicBranchPrefix(projectDir, epic);
242
+ const integrationBranch = integrationBranchName(epic, branchPrefix);
243
+ const prefix = `${branchPrefix}/${epic}/`;
189
244
  const porcelain = git(projectDir, ['worktree', 'list', '--porcelain']);
190
245
  const entries = parseWorktreePorcelain(porcelain);
191
246
  const stale = [];
@@ -0,0 +1,19 @@
1
+ CREATE TABLE `operator_feedback` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `task_id` text NOT NULL,
4
+ `session_id` text NOT NULL,
5
+ `body` text NOT NULL,
6
+ `kind` text NOT NULL,
7
+ `source` text NOT NULL,
8
+ `external_id` text,
9
+ `author` text,
10
+ `recorded_at` text NOT NULL,
11
+ `recorded_event_id` text NOT NULL,
12
+ `resolved_at` text,
13
+ `resolution` text,
14
+ `follow_up_task_id` text
15
+ );
16
+ --> statement-breakpoint
17
+ CREATE INDEX `operator_feedback_task_idx` ON `operator_feedback` (`task_id`);--> statement-breakpoint
18
+ CREATE INDEX `operator_feedback_session_idx` ON `operator_feedback` (`session_id`);--> statement-breakpoint
19
+ CREATE INDEX `operator_feedback_external_idx` ON `operator_feedback` (`external_id`);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `tasks` ADD `title` text;--> statement-breakpoint
2
+ ALTER TABLE `tasks` ADD `summary` text;
@@ -0,0 +1 @@
1
+ ALTER TABLE `tasks` ADD `terminal_at` text;
@@ -0,0 +1,30 @@
1
+ PRAGMA foreign_keys=OFF;--> statement-breakpoint
2
+ CREATE TABLE `__new_milestones` (
3
+ `milestone_id` text PRIMARY KEY NOT NULL,
4
+ `name` text NOT NULL,
5
+ `status` text NOT NULL,
6
+ `sequence` integer NOT NULL,
7
+ `goal` text,
8
+ `epic_ids` text NOT NULL,
9
+ `project` text DEFAULT 'blacksmith' NOT NULL,
10
+ `kind` text DEFAULT 'factory' NOT NULL,
11
+ `error_issues` integer DEFAULT true NOT NULL
12
+ );
13
+ --> statement-breakpoint
14
+ INSERT INTO `__new_milestones`("milestone_id", "name", "status", "sequence", "goal", "epic_ids", "project", "kind", "error_issues") SELECT "milestone_id", "name", "status", "sequence", "goal", "epic_ids", "project", "kind", "error_issues" FROM `milestones`;--> statement-breakpoint
15
+ DROP TABLE `milestones`;--> statement-breakpoint
16
+ ALTER TABLE `__new_milestones` RENAME TO `milestones`;--> statement-breakpoint
17
+ PRAGMA foreign_keys=ON;--> statement-breakpoint
18
+ CREATE INDEX `milestones_project_idx` ON `milestones` (`project`);--> statement-breakpoint
19
+ -- The factory's own project was renamed from 'black-smith' to 'blacksmith'.
20
+ -- History (events, committed plan files, old roadmap.md bullets) keeps the
21
+ -- old spelling, but every table's read model should not: rewrite every row
22
+ -- that already carries the old name in every table with a project column.
23
+ UPDATE `events_raw` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
24
+ UPDATE `dispatches` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
25
+ UPDATE `tasks` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
26
+ UPDATE `errors` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
27
+ UPDATE `findings` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
28
+ UPDATE `epics` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
29
+ UPDATE `milestones` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
30
+ UPDATE `issue_reports` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `events_raw` ADD `cli_session_id` text;--> statement-breakpoint
2
+ CREATE INDEX `events_raw_cli_session_idx` ON `events_raw` (`cli_session_id`);
@@ -92,6 +92,41 @@
92
92
  "when": 1789464480602,
93
93
  "tag": "0012_issue_reports_error_issues",
94
94
  "breakpoints": true
95
+ },
96
+ {
97
+ "idx": 13,
98
+ "version": "6",
99
+ "when": 1790738459253,
100
+ "tag": "0013_operator_feedback",
101
+ "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "6",
106
+ "when": 1790818969124,
107
+ "tag": "0014_misty_madripoor",
108
+ "breakpoints": true
109
+ },
110
+ {
111
+ "idx": 15,
112
+ "version": "6",
113
+ "when": 1790924037810,
114
+ "tag": "0015_gifted_omega_flight",
115
+ "breakpoints": true
116
+ },
117
+ {
118
+ "idx": 16,
119
+ "version": "6",
120
+ "when": 1791191766572,
121
+ "tag": "0016_milestones_project_default",
122
+ "breakpoints": true
123
+ },
124
+ {
125
+ "idx": 17,
126
+ "version": "6",
127
+ "when": 1791262161043,
128
+ "tag": "0017_events_raw_cli_session_id",
129
+ "breakpoints": true
95
130
  }
96
131
  ]
97
132
  }
@@ -6,23 +6,24 @@
6
6
  # What is mechanical, and what is a request (P9-18, 2026-08-08; `failed_rounds`
7
7
  # added P9-32 and `alarm_ratio` P9-33, both 2026-08-10; `max_in_flight_tasks`
8
8
  # added 2026-08-26, the same day `epic.cap_tokens` grew a reader that actually
9
- # blocks). Exactly six numbers here reach a runtime host:
9
+ # blocks). Exactly six kinds of number here reach a runtime host — each one
10
+ # sized per effort tier, see "Effort tiers" below:
10
11
  #
11
12
  # * `epic.cap_tokens` — three readers now, and only the newest of them
12
13
  # blocks. planQuorum's budget trigger sums *declared* task budgets against
13
14
  # it before any dispatch and only ever surfaces the finding for a human to
14
- # act on; `smith budget alarm` compares it against spend counted from the
15
- # event log, after wave merge, and also stops nothing itself. `smith wave
15
+ # act on; `bs budget alarm` compares it against spend counted from the
16
+ # event log, after wave merge, and also stops nothing itself. `bs wave
16
17
  # check` (src/waveBudget.ts) is the reader that blocks: at wave admission —
17
18
  # before any task in the wave has been dispatched — it sums the wave's
18
19
  # declared task budgets against headroom under the cap (measured spend plus
19
20
  # a declared price for every dispatch nothing has measured yet, the same
20
- # projection `smith budget alarm` computes) and refuses to admit a wave
21
+ # projection `bs budget alarm` computes) and refuses to admit a wave
21
22
  # that would cross it. It is not a stricter version of the other two
22
23
  # readers' advice; it is the only one of the three standing upstream of the
23
24
  # spend it is judging, so it is the only one that can refuse the spend
24
25
  # instead of merely describing it afterward.
25
- # * `epic.alarm_ratio` — `smith budget alarm` (src/budgetAlarm.ts), which
26
+ # * `epic.alarm_ratio` — `bs budget alarm` (src/budgetAlarm.ts), which
26
27
  # reports each epic's measured and projected spend against
27
28
  # `alarm_ratio * cap_tokens` and exits 1 on a crossing. Measured spend is a
28
29
  # floor, not a total: a judge returns findings rather than a Result, so its
@@ -32,14 +33,17 @@
32
33
  # `task:` block's coverage of the roles the factory actually dispatches is
33
34
  # load-bearing and not merely documentary.
34
35
  # * a task's `tokens` — compared at the gate against the result's
35
- # `token_usage.total_tokens`.
36
+ # `token_usage.total_tokens`. The per-role caps under `task:` are what
37
+ # `bs budget alarm` and `bs wave check` charge a dispatch nothing has
38
+ # measured yet, and what the dispatch harness hands a CLI turn as its
39
+ # `cap_tokens`.
36
40
  # * a task's `diff_lines` — compared at the gate against a real
37
41
  # `git diff --numstat` (src/diffstat.ts), lockfiles and generated output
38
42
  # excluded.
39
43
  # * `escalation_ladder.rungs[].failed_rounds` — counted from `gate-outcome`
40
- # events by `smith escalation check` (src/escalation.ts), which asserts the
44
+ # events by `bs escalation check` (src/escalation.ts), which asserts the
41
45
  # rung's `enforce` obligation against what the log shows happened next.
42
- # * `epic.max_in_flight_tasks` — `smith wave check`, which refuses to admit a
46
+ # * `epic.max_in_flight_tasks` — `bs wave check`, which refuses to admit a
43
47
  # wave that would put more of the epic's tasks in flight at once (admitted,
44
48
  # not yet merged) than this. Null by default; see the field's own comment
45
49
  # below for why it is not the mechanism that bounds cost.
@@ -51,7 +55,7 @@
51
55
  # measuring (D-29: a coder spent its tail shaving lines to land exactly on
52
56
  # `diff_lines`, stopping mid-procedure to do it).
53
57
  #
54
- # The epic cap no longer works that way: `smith wave check` blocks on one.
58
+ # The epic cap no longer works that way: `bs wave check` blocks on one.
55
59
  # The difference is not that an epic overrun matters more than a task overrun
56
60
  # — it is *when* each is checked. The task gate polices work already
57
61
  # underway, so refusing there distorts the work it is judging. Wave admission
@@ -68,13 +72,48 @@
68
72
  # agent-interviews.md M-4 had called it inert), and a plan cannot raise or
69
73
  # lower a role's ceiling. So plan validation rejects it as a task budget
70
74
  # field, and the dispatch prompt restates the template's number verbatim.
75
+ # It is one number per template, not per tier: a template cannot know the
76
+ # epic it will be dispatched into, so each `maxTurns` is sized as the ceiling
77
+ # for the huge tier (raised 2026-09-29, measured on eight medium-tier dogfood
78
+ # epics, 2026-09-14..29). The token caps below are what scale down for a
79
+ # smaller epic; the turn ceiling does not.
80
+ #
81
+ # Effort tiers (2026-09-29). Every token and diff number in this file is sized
82
+ # per effort tier — effort.yml's small / medium / huge: the tier the epic
83
+ # runs at, i.e. its plan's `effort` raised by effort.yml's `security_floor`
84
+ # when a live task trips a security trigger, and effort.yml's `default_tier`
85
+ # (medium) when there is no plan in hand or it names none. One source of
86
+ # truth, one scale:
87
+ #
88
+ # * `epic.cap_tokens` is an explicit triplet, because an epic's size is not
89
+ # a straight multiple of its tasks'.
90
+ # * every `task:` number is declared once, at medium, and multiplied by
91
+ # `task_tier_scale` for the tier: small 0.5x, huge 2x.
92
+ # * `epic.alarm_ratio` is a ratio and is the same at every tier.
93
+ #
94
+ # Per-box env overrides (`.env`, or an exported value, which beats it). Each
95
+ # number that a box may want to move has an env name — BS_EPIC_CAP_TOKENS,
96
+ # BS_TASK_<ROLE>_CAP_TOKENS, … listed in .env.example (the deprecated
97
+ # SMITH_* spellings still work as a fallback) — and each name has a
98
+ # `_SMALL`, `_MEDIUM` and `_HUGE` variant. For an epic of a given tier the
99
+ # precedence is, highest first:
100
+ #
101
+ # 1. the variant for that tier, e.g. BS_EPIC_CAP_TOKENS_HUGE;
102
+ # 2. the bare name, e.g. BS_EPIC_CAP_TOKENS, which applies to every tier
103
+ # that has no variant of its own set;
104
+ # 3. the number in this file for that tier.
105
+ #
106
+ # An env value beats the tier: a bare name pins one number for every tier. A
107
+ # malformed value in any variant — even one for a tier not in play — is
108
+ # refused with `budgets.invalid-env`. `bs wave check` and `bs budget
109
+ # alarm` name the variables that moved a number, never their values.
71
110
  #
72
111
  # No concurrency cap on cost grounds, by design (removed 2026-08-05). Workers
73
112
  # run in isolated worktrees on disjoint path claims, so parallelism is bounded
74
113
  # by the claim graph, not by a worker count — and fan-out buys wall-clock
75
114
  # without buying extra tokens (architecture §15.3). Hundreds of concurrent
76
115
  # subagents is a supported shape. That reasoning holds only as long as the
77
- # token cap it leans on actually binds, which — before `smith wave check` —
116
+ # token cap it leans on actually binds, which — before `bs wave check` —
78
117
  # `epic.cap_tokens` did not. `epic.max_in_flight_tasks` below is not that
79
118
  # removed cap come back: it defaults to null, and cost stays bounded by
80
119
  # `cap_tokens`, not by a worker count. It exists for an operator who wants a
@@ -83,7 +122,22 @@
83
122
  # nothing about what bounds spend.
84
123
 
85
124
  epic:
86
- cap_tokens: 4000000 # planner + all workers + judges, one epic.
125
+ cap_tokens: # planner + all workers + judges, one epic.
126
+ small: 4000000
127
+ medium: 16000000
128
+ huge: 32000000
129
+ # 2026-09-29: per tier. Medium is 16M, the cap
130
+ # the operator's own box had already been
131
+ # running by env override; the largest measured
132
+ # medium epic (eight medium-tier dogfood epics,
133
+ # 2026-09-14..29) spent about 7.3M, so 16M
134
+ # carries it with the alarm at 11.2M. Small
135
+ # keeps the old single number, 4M; huge is 32M.
136
+ # This is also the per-epic override follow-up
137
+ # (1) below asked for, in the only form a
138
+ # policy file can give it: an epic's `effort`
139
+ # picks its cap.
140
+ # Earlier history:
87
141
  # Raised from 2,000,000 by operator decision on
88
142
  # 2026-08-11 (dogfood-mcp-1), which is the
89
143
  # operator answer the `alarm_ratio` note below
@@ -99,31 +153,32 @@ epic:
99
153
  # so a number raised for one epic is raised for
100
154
  # every epic black-smith will ever run. What
101
155
  # exists is a per-BOX override, a different
102
- # thing: SMITH_EPIC_CAP_TOKENS in the env (or
156
+ # thing: BS_EPIC_CAP_TOKENS in the env (or
103
157
  # `.env`) replaces this number for every epic
104
158
  # run on that box while it is set — the
105
- # SMITH_EPIC_* / SMITH_TASK_* names in
159
+ # BS_EPIC_* / BS_TASK_* names in
106
160
  # .env.example cover each number in this file
107
161
  # that a box may want to move; (2) a
108
162
  # large share of that 1.5M was rework — stopped
109
- # dispatches and re-dispatches that `smith
163
+ # dispatches and re-dispatches that `bs
110
164
  # escalation check` recorded as ladder
111
165
  # violations — so this cap now accommodates
112
166
  # waste as well as work. A cap sized from a
113
167
  # badly-run epic is a ceiling measured at the
114
168
  # wrong place.
115
- alarm_ratio: 0.7 # at 2.8M tokens: planner must re-plan remaining
169
+ alarm_ratio: 0.7 # same at every tier (small: 2.8M, medium:
170
+ # 11.2M, huge: 22.4M). At that point: planner must re-plan remaining
116
171
  # work to fit, or ask the operator. Epics that
117
172
  # cannot fit are split at spec time, never
118
173
  # silently extended (extension is always an
119
- # operator question). Checked by `smith budget
174
+ # operator question). Checked by `bs budget
120
175
  # alarm <session>`, which the /bs run loop runs at
121
176
  # each wave merge — before P9-33 this number's
122
177
  # only reader was a human remembering to look.
123
178
  max_in_flight_tasks: null # Off by default. "Fan-out buys wall-clock
124
179
  # without buying extra tokens" above is the
125
180
  # standing decision, and it only holds while the
126
- # token cap actually binds — which `smith wave
181
+ # token cap actually binds — which `bs wave
127
182
  # check` now makes true. So this is NOT the
128
183
  # mechanism that bounds cost; `cap_tokens` is,
129
184
  # and it is checked regardless of how this field
@@ -131,17 +186,32 @@ epic:
131
186
  # wants a wall-clock or rate-limit bound for a
132
187
  # reason of their own — a provider's
133
188
  # concurrent-request limit, a laptop's CPU count
134
- # — and setting it makes `smith wave check`
189
+ # — and setting it makes `bs wave check`
135
190
  # refuse to admit a wave that would put more of
136
191
  # the epic's tasks in flight (admitted, not yet
137
192
  # merged) than this number at once.
138
193
 
194
+ task_tier_scale:
195
+ # Multiplies every `task:` number below for an epic of that tier. Not the
196
+ # epic cap, which is declared per tier above.
197
+ small: 0.5
198
+ medium: 1
199
+ huge: 2
200
+
139
201
  task:
202
+ # Medium-tier numbers; `task_tier_scale` gives the small and huge ones.
140
203
  # Defaults a planner sizes a task's own `budget` block against. The task's
141
204
  # declared numbers are what the gate checks; these are the shape of a
142
205
  # reasonable declaration, not a second cap applied on top.
206
+ #
207
+ # Sized 2026-09-29 from the p90 tokens per dispatch measured on eight
208
+ # medium-tier dogfood epics, 2026-09-14..29, rounded up. Before that the
209
+ # coder cap was 150k (10 of 36 measured coder dispatches over it), the four
210
+ # judges shared one 40k bucket (31 of 32 spec-reviewer dispatches over it),
211
+ # researcher was 60k, and planner, tester, security-reviewer and auditor had
212
+ # no cap at all, which left every epic that used one unverifiable.
143
213
  coder:
144
- cap_tokens: 150000 # Second reader, added 2026-08-26: `smith wave
214
+ cap_tokens: 220000 # Second reader, added 2026-08-26: `bs wave
145
215
  # check` prices a task that exists only in the
146
216
  # event log at this number. A follow-up minted by
147
217
  # `findings raise` has claims but no declared
@@ -158,18 +228,38 @@ task:
158
228
  # `budget.tokens` is a different silence and is
159
229
  # still refused — the schema requires that field,
160
230
  # so its absence is a plan that never passed
161
- # `smith plan validate`.
162
- cap_diff_lines: 400 # excludes lockfiles/generated files
231
+ # `bs plan validate`.
232
+ cap_diff_lines: 700 # excludes lockfiles/generated files. Measured
233
+ # p50 316 / p90 727 lines (was 400); small 350,
234
+ # huge 1400.
235
+ tester:
236
+ cap_tokens: 60000 # p90 55k
237
+ planner:
238
+ cap_tokens: 110000 # p90 103k
163
239
  researcher:
164
- cap_tokens: 60000
165
- judges: # spec-reviewer, reviewer, verifier, grader
166
- cap_tokens: 40000
167
- # Not priced here, and dispatched anyway: security-reviewer, merger, tester,
168
- # uiux, planner, scribe. `smith budget alarm` cannot put a number on those
169
- # dispatches, so an epic that used one is reported unverifiable rather than
170
- # under — the omission is visible instead of free. Left unpriced on purpose
171
- # until there is real spend data to size them from; inventing a cap would
172
- # make the projection look complete without making it true.
240
+ cap_tokens: 80000 # p90 74k (was 60k)
241
+ spec-reviewer:
242
+ cap_tokens: 170000 # p90 168k (was the shared 40k judges bucket)
243
+ grader:
244
+ cap_tokens: 110000 # p90 105k (was 40k)
245
+ reviewer:
246
+ cap_tokens: 60000 # p90 56k (was 40k)
247
+ verifier:
248
+ cap_tokens: 40000 # p90 25k (unchanged)
249
+ security-reviewer:
250
+ cap_tokens: 80000 # p90 77k
251
+ auditor:
252
+ cap_tokens: 90000 # p90 89k
253
+ # The old shared `judges:` bucket is gone. A policy file that still carries
254
+ # `judges: {cap_tokens: N}` has N read for spec-reviewer, reviewer, verifier
255
+ # and grader wherever it does not name that role itself.
256
+ #
257
+ # Not priced here, and dispatched anyway: merger, uiux, scribe, wave-runner.
258
+ # `bs budget alarm` cannot put a number on those dispatches, so an epic
259
+ # that used one is reported unverifiable rather than under — the omission is
260
+ # visible instead of free. Left unpriced on purpose until there is real spend
261
+ # data to size them from; inventing a cap would make the projection look
262
+ # complete without making it true.
173
263
 
174
264
  pre_code_budget:
175
265
  # researcher + uiux together, before any code is written for the epic
@@ -193,7 +283,7 @@ context_window:
193
283
  # quietly loses the contract it was given.
194
284
  counts_against_budget: true # tokens spent before a compaction still count
195
285
  # against the caps above — compacting is not a
196
- # way around the 150k coder cap.
286
+ # way around the coder cap.
197
287
  compacting_roles: [planner, coder, tester, researcher, uiux, scribe]
198
288
  narrowing_roles: [reviewer, verifier, grader, spec-reviewer, security-reviewer, merger, auditor]
199
289
  # narrowing_roles hold evidence rather than build on it: a judge that compacts
@@ -219,7 +309,7 @@ escalation_ladder:
219
309
  # architecture §12, agent-constraints.md "budgets (factory-wide)"
220
310
  #
221
311
  # `failed_rounds` and `enforce` are the machine-readable half, added P9-32.
222
- # src/escalation.ts reads them and `smith escalation check` asserts them
312
+ # src/escalation.ts reads them and `bs escalation check` asserts them
223
313
  # against the log; `trigger`/`action` stay as the human contract and must say
224
314
  # the same thing. A failed round is one `gate-outcome` with outcome
225
315
  # `blocked` for the task.
@@ -260,7 +350,7 @@ effort_scaling:
260
350
  # and it lives in `factory/policies/effort.yml`: a per-epic tier
261
351
  # (small/medium/huge) that scales the deliberation steps — pre-code briefs,
262
352
  # spec-review rounds, the plan quorum, grader rounds, verifier severities,
263
- # the closing spec review — and nothing else. Ask `smith effort show`; the
353
+ # the closing spec review — and nothing else. Ask `bs effort show`; the
264
354
  # two files do not overlap, and neither may touch a mechanical gate.
265
355
  - rule: small fixes get a single coder, not a fan-out.
266
356
  - rule: >