@juzser/blacksmith 0.3.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 (46) 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 +12 -5
  8. package/.claude/skills/bs/dispatch.md +40 -4
  9. package/.claude/skills/bs/plan.md +33 -1
  10. package/.claude/skills/bs/run.md +11 -5
  11. package/.claude/skills/bs/wave.md +40 -3
  12. package/README.md +1 -1
  13. package/factory/orchestrator/dist/agentsSync.js +23 -0
  14. package/factory/orchestrator/dist/audit.js +134 -17
  15. package/factory/orchestrator/dist/autonomy.js +6 -2
  16. package/factory/orchestrator/dist/cli.js +309 -28
  17. package/factory/orchestrator/dist/crossFinding.js +20 -0
  18. package/factory/orchestrator/dist/daemon.js +198 -31
  19. package/factory/orchestrator/dist/db/projector.js +51 -27
  20. package/factory/orchestrator/dist/dispatchLint.js +132 -0
  21. package/factory/orchestrator/dist/epic.js +179 -12
  22. package/factory/orchestrator/dist/errorIssues.js +175 -10
  23. package/factory/orchestrator/dist/events.js +59 -11
  24. package/factory/orchestrator/dist/findings.js +25 -2
  25. package/factory/orchestrator/dist/gh.js +19 -2
  26. package/factory/orchestrator/dist/git.js +20 -2
  27. package/factory/orchestrator/dist/hookDecision.js +320 -37
  28. package/factory/orchestrator/dist/integration.js +11 -1
  29. package/factory/orchestrator/dist/issueReporter.js +81 -16
  30. package/factory/orchestrator/dist/judgeStopHook.js +181 -0
  31. package/factory/orchestrator/dist/logCache.js +291 -0
  32. package/factory/orchestrator/dist/paths.js +10 -0
  33. package/factory/orchestrator/dist/plan.js +130 -0
  34. package/factory/orchestrator/dist/projects.js +12 -1
  35. package/factory/orchestrator/dist/queue.js +469 -28
  36. package/factory/orchestrator/dist/scheduler.js +3 -3
  37. package/factory/orchestrator/dist/spec.js +70 -0
  38. package/factory/orchestrator/dist/taskEvents.js +13 -4
  39. package/factory/orchestrator/dist/taxonomy.js +11 -0
  40. package/factory/orchestrator/dist/testgate.js +4 -3
  41. package/factory/orchestrator/dist/usage.js +18 -10
  42. package/factory/orchestrator/dist/waivers.js +60 -7
  43. package/factory/orchestrator/dist/waveSchedule.js +84 -0
  44. package/factory/orchestrator/dist/worktree.js +19 -6
  45. package/factory/specs/roadmap.md +24 -0
  46. package/package.json +1 -1
@@ -11,6 +11,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
11
11
  * assets ships in the tarball. `packaging.test.ts` holds that claim.
12
12
  */
13
13
  export const REPO_ROOT = path.resolve(here, '..', '..', '..');
14
+ /**
15
+ * Is `dir` this factory's own checkout, resolved rather than compared raw so
16
+ * a trailing slash or a relative path names the same answer. `audit.ts`
17
+ * (`auditedProjectName`) and `projects.ts` (`unwatchedProjects`) both need to
18
+ * tell "this clone, filed under its own directory name" from "a project that
19
+ * is genuinely named that" and used to each spell the comparison themselves.
20
+ */
21
+ export function isFactoryCheckout(dir) {
22
+ return path.resolve(dir) === path.resolve(REPO_ROOT);
23
+ }
14
24
  /**
15
25
  * Is this module running out of the blacksmith clone, or out of an installed
16
26
  * copy of the package?
@@ -6,6 +6,7 @@ import { SmithError } from './errors.js';
6
6
  import { topoSort } from './graph.js';
7
7
  import { SPECS_ACTIVE_DIR } from './paths.js';
8
8
  import { compileSchemas, validateRecord, } from './schemas.js';
9
+ import { epicOfTaskId } from './taskId.js';
9
10
  import { loadTaxonomy, TaxonomyError, validateRequiredDimensions, } from './taxonomy.js';
10
11
  export class PlanError extends SmithError {
11
12
  }
@@ -133,6 +134,122 @@ export function loadPlan(epicId, version, opts = {}) {
133
134
  }
134
135
  return JSON.parse(readFileSync(filePath, 'utf8'));
135
136
  }
137
+ /**
138
+ * A task ref's project, read off its own epic's latest plan version -- the
139
+ * D-246 precedent (`db/projector.ts`'s `planProjectResolver`, for the board)
140
+ * restated here so `errorIssues.ts` can answer the same question without
141
+ * importing anything database-shaped (`test/cliBoot.test.ts` pins `cli.ts`'s
142
+ * built module graph off drizzle/sqlite entirely, and `cli.ts` statically
143
+ * imports the error-issue reporter).
144
+ *
145
+ * Never throws and never guesses: an id with no epic segment, an epic with
146
+ * no plan on disk, and an unreadable or unparseable plan file all answer
147
+ * `null` alike, on purpose -- "cannot tell" and "the plan has no project" are
148
+ * the same non-answer to a caller that must never turn either one into this
149
+ * factory's own name. Answers cache by epic id for the resolver's lifetime,
150
+ * since one `issues report` run reads the same epic's plan for every row
151
+ * that epic contributed.
152
+ */
153
+ export function planProjectResolverForTaskRef(opts = {}) {
154
+ const cache = new Map();
155
+ return (taskRef) => {
156
+ const epicId = epicOfTaskId(taskRef);
157
+ if (epicId === null)
158
+ return null;
159
+ const cached = cache.get(epicId);
160
+ if (cached !== undefined)
161
+ return cached;
162
+ let project = null;
163
+ try {
164
+ const version = latestPlanVersion(epicId, opts);
165
+ if (version !== null)
166
+ project = loadPlan(epicId, version, opts).project ?? null;
167
+ }
168
+ catch {
169
+ // Unreadable or unparseable plan file: no answer, not a crash.
170
+ }
171
+ cache.set(epicId, project);
172
+ return project;
173
+ };
174
+ }
175
+ /**
176
+ * Whether `opts.specsDir` names THIS checkout's own specs/active tree --
177
+ * absent (so `planFilePath`/`latestPlanVersion` fall back to
178
+ * `SPECS_ACTIVE_DIR` themselves) or an explicit path that resolves to the
179
+ * same directory. Only then is a plan found there, naming no project,
180
+ * evidence of "self": `--specs-dir` is also how a caller points this
181
+ * resolver at a DIFFERENT checkout's specs tree entirely -- `issueInputs`'s
182
+ * own doc comment names "the daemon's own event log and a target checkout's
183
+ * specs tree" as an intended use -- and a plan found THERE naming no project
184
+ * is that other project's own silence, not this factory's (S2-b).
185
+ */
186
+ function isSelfSpecsDir(opts) {
187
+ return opts.specsDir === undefined || path.resolve(opts.specsDir) === SPECS_ACTIVE_DIR;
188
+ }
189
+ /**
190
+ * The self-fallback half of `planProjectResolverForTaskRefOrSelf`, on its
191
+ * own: a plan found on disk for the epic that simply declares no `project`
192
+ * field IS this factory's own project, by construction -- only this
193
+ * checkout's own epics have a plan under `specs/active` at all, so finding
194
+ * one there is itself the "self" signal a foreign epic's plan (which lives
195
+ * in the foreign project's own specs tree, never this one) can never
196
+ * produce. An epic with no plan on disk anywhere, or a bare ref with no
197
+ * epic segment, answers `null` -- "cannot tell whose project this is" --
198
+ * because plan.ts must not be the module that hardcodes this factory's
199
+ * name; the caller supplies it.
200
+ *
201
+ * That "only this checkout's own epics" premise holds only while `opts`
202
+ * actually points at this checkout's own specs/active tree
203
+ * (`isSelfSpecsDir`, S2-b): a caller that redirected `--specs-dir` at some
204
+ * OTHER checkout's specs tree gets no self-fallback at all, `null` only,
205
+ * because a plan found there naming no project is that other checkout's own
206
+ * silence, not this factory's.
207
+ *
208
+ * Exported as its own resolver, separate from `planProjectResolverForTaskRef`
209
+ * (S2-c): production callers (`cli.ts`'s `issueInputs`) hand the two to
210
+ * `withSessionFallback`/`foldErrorEvents` as DISTINCT tiers -- the strict
211
+ * resolver first, then a session's own unanimous stamp, and only then this
212
+ * guess -- so a session-mate's real stamp naming a different project
213
+ * outranks this weakest signal rather than losing to it, which is what
214
+ * happened when the two were folded into one resolver and tried together as
215
+ * a single first tier.
216
+ */
217
+ export function planSelfFallbackForTaskRef(selfProject, opts = {}) {
218
+ const selfEligible = isSelfSpecsDir(opts);
219
+ const hasPlanCache = new Map();
220
+ return (taskRef) => {
221
+ if (!selfEligible)
222
+ return null;
223
+ const epicId = epicOfTaskId(taskRef);
224
+ if (epicId === null)
225
+ return null;
226
+ let hasPlan = hasPlanCache.get(epicId);
227
+ if (hasPlan === undefined) {
228
+ hasPlan = latestPlanVersion(epicId, opts) !== null;
229
+ hasPlanCache.set(epicId, hasPlan);
230
+ }
231
+ return hasPlan ? selfProject : null;
232
+ };
233
+ }
234
+ /**
235
+ * `planProjectResolverForTaskRef` composed with `planSelfFallbackForTaskRef`
236
+ * as a single resolver, direct answer first and self-fallback second, for
237
+ * callers that want the OLD combined behavior (both tiers tried back to
238
+ * back, with nothing -- such as a session stamp -- able to sit between
239
+ * them). Kept for callers that do not need the S2-c tier split; `cli.ts`'s
240
+ * `issueInputs` no longer uses this, it hands the two halves to
241
+ * `withSessionFallback` separately instead.
242
+ */
243
+ export function planProjectResolverForTaskRefOrSelf(selfProject, opts = {}) {
244
+ const resolve = planProjectResolverForTaskRef(opts);
245
+ const selfFallback = planSelfFallbackForTaskRef(selfProject, opts);
246
+ return (taskRef) => {
247
+ const direct = resolve(taskRef);
248
+ if (direct !== null)
249
+ return direct;
250
+ return selfFallback(taskRef);
251
+ };
252
+ }
136
253
  /**
137
254
  * Reduce an `output_schema_ref` to the name the compiled schema set knows it
138
255
  * by. `compileSchemas` keys on filename-minus-`.schema.json`, so a path, a
@@ -509,6 +626,19 @@ export function livePlanTasks(plan) {
509
626
  }
510
627
  return live;
511
628
  }
629
+ /**
630
+ * The plan's tasks in the shape `resolveFindingOwner` reads: only the live
631
+ * (D-126) ones, and only the `claims` field ownership resolution needs. This
632
+ * is `livePlanTasks` restated as claims rather than full specs, so a caller
633
+ * asking "does some task in this plan still claim that file?" (`audit
634
+ * resolve`) gets the same live/superseded rule `epic verdict` does, not the
635
+ * raw `plan.tasks` array that also holds every amendment left behind.
636
+ */
637
+ export function planClaimedTasks(plan) {
638
+ return livePlanTasks(plan)
639
+ .filter((task) => Array.isArray(task.claims))
640
+ .map((task) => ({ task_id: task.task_id, claims: task.claims }));
641
+ }
512
642
  /**
513
643
  * What makes two records the same *spec*: everything except the version stamp
514
644
  * and the workflow status. `nextVersion` rewrites `plan_version` on every
@@ -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 };