@juzser/blacksmith 0.2.0 → 0.4.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 (71) hide show
  1. package/.claude/agents/auditor.md +3 -0
  2. package/.claude/agents/grader.md +12 -5
  3. package/.claude/agents/reviewer.md +3 -0
  4. package/.claude/agents/security-reviewer.md +3 -0
  5. package/.claude/agents/spec-reviewer.md +3 -0
  6. package/.claude/agents/verifier.md +3 -0
  7. package/.claude/skills/bs/audit.md +25 -9
  8. package/.claude/skills/bs/dispatch.md +60 -7
  9. package/.claude/skills/bs/plan.md +33 -1
  10. package/.claude/skills/bs/run.md +18 -13
  11. package/.claude/skills/bs/wave.md +68 -10
  12. package/README.md +4 -4
  13. package/factory/orchestrator/dist/agentsSync.js +178 -0
  14. package/factory/orchestrator/dist/audit.js +134 -17
  15. package/factory/orchestrator/dist/autonomy.js +25 -0
  16. package/factory/orchestrator/dist/budgets.js +91 -2
  17. package/factory/orchestrator/dist/claims.js +8 -4
  18. package/factory/orchestrator/dist/cli.js +394 -26
  19. package/factory/orchestrator/dist/crossFinding.js +20 -0
  20. package/factory/orchestrator/dist/daemon.js +230 -41
  21. package/factory/orchestrator/dist/db/projector.js +93 -36
  22. package/factory/orchestrator/dist/db/queries.js +58 -35
  23. package/factory/orchestrator/dist/db/schema.js +24 -0
  24. package/factory/orchestrator/dist/dispatchAudit.js +10 -2
  25. package/factory/orchestrator/dist/dispatchLint.js +132 -0
  26. package/factory/orchestrator/dist/epic.js +192 -39
  27. package/factory/orchestrator/dist/epicWidth.js +25 -20
  28. package/factory/orchestrator/dist/errorIssues.js +209 -24
  29. package/factory/orchestrator/dist/eventOrder.js +102 -0
  30. package/factory/orchestrator/dist/events.js +73 -91
  31. package/factory/orchestrator/dist/findingContext.js +1 -13
  32. package/factory/orchestrator/dist/findings.js +132 -11
  33. package/factory/orchestrator/dist/gh.js +29 -6
  34. package/factory/orchestrator/dist/git.js +20 -2
  35. package/factory/orchestrator/dist/harness.js +92 -10
  36. package/factory/orchestrator/dist/hookDecision.js +320 -37
  37. package/factory/orchestrator/dist/impact.js +24 -2
  38. package/factory/orchestrator/dist/integration.js +11 -1
  39. package/factory/orchestrator/dist/issueReporter.js +415 -0
  40. package/factory/orchestrator/dist/judgeStopHook.js +181 -0
  41. package/factory/orchestrator/dist/judges.js +1 -1
  42. package/factory/orchestrator/dist/lessons.js +46 -18
  43. package/factory/orchestrator/dist/logCache.js +291 -0
  44. package/factory/orchestrator/dist/paths.js +10 -0
  45. package/factory/orchestrator/dist/plan.js +138 -4
  46. package/factory/orchestrator/dist/planQuorum.js +1 -1
  47. package/factory/orchestrator/dist/policy.js +57 -11
  48. package/factory/orchestrator/dist/projects.js +12 -1
  49. package/factory/orchestrator/dist/queue.js +477 -29
  50. package/factory/orchestrator/dist/scheduler.js +51 -0
  51. package/factory/orchestrator/dist/severity.js +40 -8
  52. package/factory/orchestrator/dist/spec.js +70 -0
  53. package/factory/orchestrator/dist/taskEvents.js +13 -4
  54. package/factory/orchestrator/dist/taskStatus.js +74 -0
  55. package/factory/orchestrator/dist/taxonomy.js +11 -0
  56. package/factory/orchestrator/dist/testgate.js +32 -3
  57. package/factory/orchestrator/dist/usage.js +42 -9
  58. package/factory/orchestrator/dist/waivers.js +60 -9
  59. package/factory/orchestrator/dist/waveConcurrency.js +29 -0
  60. package/factory/orchestrator/dist/waveNext.js +12 -10
  61. package/factory/orchestrator/dist/waveSchedule.js +84 -0
  62. package/factory/orchestrator/dist/worktree.js +19 -6
  63. package/factory/orchestrator/drizzle/0012_issue_reports_error_issues.sql +20 -0
  64. package/factory/orchestrator/drizzle/meta/_journal.json +7 -0
  65. package/factory/policies/budgets.yml +9 -2
  66. package/factory/policies/crosscheck.yml +2 -2
  67. package/factory/policies/lessons.md +298 -0
  68. package/factory/policies/scheduler.yml +27 -2
  69. package/factory/policies/taxonomy.yml +10 -3
  70. package/factory/specs/roadmap.md +70 -23
  71. package/package.json +8 -8
@@ -39,8 +39,20 @@ export class PolicyError extends SmithError {
39
39
  * that reason: it is the mechanism that keeps a tester out of the code it is
40
40
  * grading, and a policy that cannot name it either blocks the tester
41
41
  * entirely or lets it mark its own homework.
42
+ *
43
+ * The list is closed in both directions. Every id here must appear in
44
+ * guardrails.yml, for the reasons above; and every id guardrails.yml declares
45
+ * must appear here, because a rule is a row in that file *plus* a branch in
46
+ * this one that looks the row up. A row with no branch parses, counts and
47
+ * prints like the others and enforces nothing — the file says the door is
48
+ * shut and no one is standing at it. So the name is what the evaluator
49
+ * consults, not what the file happens to carry, and `GuardrailRuleId` below
50
+ * makes the compiler hold every `requireRule`/`sandboxViolation` call site to
51
+ * it. What no type can check is a stale entry — an id left here after its
52
+ * branch was deleted — so policy.test.ts scrapes the call sites back out of
53
+ * this file and holds them against the list.
42
54
  */
43
- const REQUIRED_RULE_IDS = [
55
+ export const EVALUATED_RULE_IDS = [
44
56
  'push-to-protected',
45
57
  'force-push',
46
58
  'merge-into-protected',
@@ -52,6 +64,7 @@ const REQUIRED_RULE_IDS = [
52
64
  'judge-sandbox-escape',
53
65
  'role-write-scope',
54
66
  ];
67
+ const EVALUATED_RULE_ID_SET = new Set(EVALUATED_RULE_IDS);
55
68
  function isStringArray(value) {
56
69
  return Array.isArray(value) && value.every((v) => typeof v === 'string');
57
70
  }
@@ -80,8 +93,15 @@ function parseCommandSpecs(raw, key) {
80
93
  return { command: e.command, subcommands: e.subcommands };
81
94
  });
82
95
  }
83
- /** Fold one `rules:` list into the shared id-keyed map. Used for the top-level list and the judge_sandbox one. */
84
- function collectRules(raw, into, key) {
96
+ /**
97
+ * Fold one `rules:` list into the shared id-keyed map, used for the top-level
98
+ * list and the two block-level ones. `declaredIn` remembers which block each
99
+ * id came from, so a second definition can be refused by name rather than
100
+ * overwriting the first: the folds run in source order, and a plain `set`
101
+ * would leave the last block read holding the rule while the file still reads
102
+ * as if the first one did.
103
+ */
104
+ function collectRules(raw, into, declaredIn, key) {
85
105
  if (!Array.isArray(raw)) {
86
106
  throw new PolicyError('policy.invalid-document', `guardrails.yml is missing ${key} (a list).`, {
87
107
  key,
@@ -94,6 +114,11 @@ function collectRules(raw, into, key) {
94
114
  typeof r.reason !== 'string') {
95
115
  throw new PolicyError('policy.invalid-document', `guardrails.yml has a rule in ${key} missing a string id/severity/reason.`, { key, rule: entry });
96
116
  }
117
+ const already = declaredIn.get(r.id);
118
+ if (already !== undefined) {
119
+ throw new PolicyError('policy.invalid-document', `guardrails.yml declares rule "${r.id}" in both ${already} and ${key}; a rule gets one definition, not two.`, { rule: r.id, declaredIn: already, redeclaredIn: key });
120
+ }
121
+ declaredIn.set(r.id, key);
97
122
  into.set(r.id, {
98
123
  id: r.id,
99
124
  severity: r.severity,
@@ -194,10 +219,16 @@ export function parseGuardrailPolicy(yamlText) {
194
219
  }
195
220
  const roleWriteScopes = parseRoleWriteScopes(rawScopes.scopes, judgeSandbox.roles);
196
221
  const rules = new Map();
197
- collectRules(doc.rules, rules, 'rules');
198
- collectRules(rawSandbox.rules, rules, 'judge_sandbox.rules');
199
- collectRules(rawScopes.rules, rules, 'role_write_scopes.rules');
200
- for (const requiredId of REQUIRED_RULE_IDS) {
222
+ const declaredIn = new Map();
223
+ collectRules(doc.rules, rules, declaredIn, 'rules');
224
+ collectRules(rawSandbox.rules, rules, declaredIn, 'judge_sandbox.rules');
225
+ collectRules(rawScopes.rules, rules, declaredIn, 'role_write_scopes.rules');
226
+ for (const [declaredId, key] of declaredIn) {
227
+ if (!EVALUATED_RULE_ID_SET.has(declaredId)) {
228
+ throw new PolicyError('policy.invalid-document', `guardrails.yml declares rule "${declaredId}" in ${key}, which nothing in the evaluator consults; a rule with no branch to fire it enforces nothing.`, { rule: declaredId, key });
229
+ }
230
+ }
231
+ for (const requiredId of EVALUATED_RULE_IDS) {
201
232
  if (!rules.has(requiredId)) {
202
233
  throw new PolicyError('policy.invalid-document', `guardrails.yml is missing required rule "${requiredId}".`, { rule: requiredId });
203
234
  }
@@ -365,20 +396,35 @@ function renderBranch(template, branch) {
365
396
  * inspect, not a hardcoded branch in `evaluateCommand` — a second Bash-like
366
397
  * tool added later is a one-line change here, not new control flow.
367
398
  */
368
- const INSPECTED_TOOLS = ['Bash'];
399
+ export const INSPECTED_TOOLS = ['Bash'];
369
400
  /**
370
401
  * Tools whose `file_path` this policy inspects — the write path that does
371
402
  * not go through a shell. Nothing here has a command to match, so only the
372
403
  * lease rules can fire on them; the base six are all about shell commands
373
404
  * and no-op on an empty one.
405
+ *
406
+ * Exported because this evaluator is not the only place that has to know
407
+ * which tools write a file. `harness.ts` asks the same question from the
408
+ * other side: a `cli` judge is refused a worktree unless its `judge_args`
409
+ * deny every one of these, and a new write tool added here is a tool that
410
+ * harness has to start denying on the same commit. Held to that shipped
411
+ * policy, and to the guard hook's own `matcher`, by policy.test.ts.
374
412
  */
375
- const INSPECTED_FILE_TOOLS = ['Write', 'Edit', 'MultiEdit', 'NotebookEdit'];
413
+ export const INSPECTED_FILE_TOOLS = [
414
+ 'Write',
415
+ 'Edit',
416
+ 'MultiEdit',
417
+ 'NotebookEdit',
418
+ ];
376
419
  function requireRule(policy, id) {
377
420
  const rule = policy.rules.get(id);
378
421
  if (!rule) {
379
422
  // Unreachable once a policy has passed parseGuardrailPolicy (which
380
- // requires every id in REQUIRED_RULE_IDS) — this is a defensive
381
- // safety-net, not a real code path.
423
+ // requires every id in EVALUATED_RULE_IDS) — this is a defensive
424
+ // safety-net, not a real code path. Taking a `GuardrailRuleId` rather
425
+ // than a string is the other half: a call site that invents an id the
426
+ // roster does not carry does not compile, so the only way to reach a
427
+ // rule from here is to have declared it as one.
382
428
  throw new PolicyError('policy.missing-rule', `guardrails.yml has no rule "${id}".`, {
383
429
  rule: id,
384
430
  });
@@ -32,7 +32,7 @@
32
32
  // silent exactly when somebody is editing (D-21).
33
33
  import { existsSync } from 'node:fs';
34
34
  import path from 'node:path';
35
- import { PROJECTS_DIR, REPO_ROOT, WORKSPACES_DIR } from './paths.js';
35
+ import { isFactoryCheckout, PROJECTS_DIR, REPO_ROOT, WORKSPACES_DIR } from './paths.js';
36
36
  import { FACTORY_PROJECT, loadRoadmap } from './roadmap.js';
37
37
  function collectProjects(opts) {
38
38
  const roots = opts.roots ?? [PROJECTS_DIR, WORKSPACES_DIR];
@@ -58,6 +58,17 @@ function collectProjects(opts) {
58
58
  missing.push({ name, roots });
59
59
  continue;
60
60
  }
61
+ // Dedupe by name alone is not enough: a self-audit's cut can stamp this
62
+ // clone's OWN directory name onto a milestone rather than FACTORY_PROJECT
63
+ // (audit.ts's `auditedProjectName`), and that name is never in `seen`.
64
+ // Left unchecked, `firstCheckout` resolves it to REPO_ROOT -- PROJECTS_DIR
65
+ // is this clone's own parent -- and this clone gets registered a second
66
+ // time as a foreign, non-self ref that `--no-self` cannot filter
67
+ // (`resolveProjectDirs` only ever drops the entry at index 0). Self
68
+ // already leads `refs`; a checkout that resolves to REPO_ROOT is that
69
+ // entry under another name, so it is skipped rather than pushed again.
70
+ if (isFactoryCheckout(dir))
71
+ continue;
61
72
  refs.push({ name, dir, self: false });
62
73
  }
63
74
  return { refs, missing };