@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.
- package/.claude/agents/auditor.md +3 -0
- package/.claude/agents/grader.md +12 -5
- package/.claude/agents/reviewer.md +3 -0
- package/.claude/agents/security-reviewer.md +3 -0
- package/.claude/agents/spec-reviewer.md +3 -0
- package/.claude/agents/verifier.md +3 -0
- package/.claude/skills/bs/audit.md +12 -5
- package/.claude/skills/bs/dispatch.md +40 -4
- package/.claude/skills/bs/plan.md +33 -1
- package/.claude/skills/bs/run.md +11 -5
- package/.claude/skills/bs/wave.md +40 -3
- package/README.md +1 -1
- package/factory/orchestrator/dist/agentsSync.js +23 -0
- package/factory/orchestrator/dist/audit.js +134 -17
- package/factory/orchestrator/dist/autonomy.js +6 -2
- package/factory/orchestrator/dist/cli.js +309 -28
- package/factory/orchestrator/dist/crossFinding.js +20 -0
- package/factory/orchestrator/dist/daemon.js +198 -31
- package/factory/orchestrator/dist/db/projector.js +51 -27
- package/factory/orchestrator/dist/dispatchLint.js +132 -0
- package/factory/orchestrator/dist/epic.js +179 -12
- package/factory/orchestrator/dist/errorIssues.js +175 -10
- package/factory/orchestrator/dist/events.js +59 -11
- package/factory/orchestrator/dist/findings.js +25 -2
- package/factory/orchestrator/dist/gh.js +19 -2
- package/factory/orchestrator/dist/git.js +20 -2
- package/factory/orchestrator/dist/hookDecision.js +320 -37
- package/factory/orchestrator/dist/integration.js +11 -1
- package/factory/orchestrator/dist/issueReporter.js +81 -16
- package/factory/orchestrator/dist/judgeStopHook.js +181 -0
- package/factory/orchestrator/dist/logCache.js +291 -0
- package/factory/orchestrator/dist/paths.js +10 -0
- package/factory/orchestrator/dist/plan.js +130 -0
- package/factory/orchestrator/dist/projects.js +12 -1
- package/factory/orchestrator/dist/queue.js +469 -28
- package/factory/orchestrator/dist/scheduler.js +3 -3
- package/factory/orchestrator/dist/spec.js +70 -0
- package/factory/orchestrator/dist/taskEvents.js +13 -4
- package/factory/orchestrator/dist/taxonomy.js +11 -0
- package/factory/orchestrator/dist/testgate.js +4 -3
- package/factory/orchestrator/dist/usage.js +18 -10
- package/factory/orchestrator/dist/waivers.js +60 -7
- package/factory/orchestrator/dist/waveSchedule.js +84 -0
- package/factory/orchestrator/dist/worktree.js +19 -6
- package/factory/specs/roadmap.md +24 -0
- 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 };
|