@juzser/blacksmith 0.4.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.
- package/.claude/agents/{auditor.md → bs-auditor.md} +5 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +4 -4
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +13 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +6 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +15 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +4 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +34 -27
- package/.claude/skills/bs/dispatch.md +100 -50
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +27 -18
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +181 -49
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +109 -53
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +48 -14
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +125 -17
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +790 -100
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +154 -36
- package/factory/orchestrator/dist/db/projector.js +278 -29
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +13 -37
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +323 -38
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +10 -4
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +105 -3
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +83 -12
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +92 -3
- package/factory/orchestrator/dist/integration.js +9 -7
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +90 -32
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +10 -7
- package/factory/orchestrator/dist/plan.js +269 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +4 -4
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +177 -13
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +179 -27
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +16 -13
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +97 -40
- package/factory/orchestrator/dist/waivers.js +39 -5
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +61 -19
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +7 -7
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- package/.claude/agents/uiux.md +0 -133
|
@@ -30,11 +30,12 @@ import { readFileSync } from 'node:fs';
|
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import picomatch from 'picomatch';
|
|
32
32
|
import { isOperatorActor } from './actors.js';
|
|
33
|
+
import { templateFileFor } from './agentNames.js';
|
|
33
34
|
import { foldLessons } from './db/projector.js';
|
|
34
35
|
import { SmithError } from './errors.js';
|
|
35
36
|
import { appendEvent, readLineageEvents } from './events.js';
|
|
36
37
|
import { AGENTS_DIR, lessonsReadPath } from './paths.js';
|
|
37
|
-
import { FILE_SCOPED_SCOPES, isFileScopedScope, LESSON_SCOPES, parseLessons, } from './severity.js';
|
|
38
|
+
import { BULLET, CONTINUATION, ENTRY_HEADING, FILE_SCOPED_SCOPES, isFileScopedScope, LESSON_SCOPES, parseLessons, SECTION_HEADING, } from './severity.js';
|
|
38
39
|
import { loadTaxonomy, validateTag } from './taxonomy.js';
|
|
39
40
|
export class LessonsError extends SmithError {
|
|
40
41
|
}
|
|
@@ -301,8 +302,8 @@ select on. Each names its own bullet, validated against \`taxonomy.yml\`:
|
|
|
301
302
|
At dispatch, an \`agent-role\` entry reaches only the role its \`agent_role\`
|
|
302
303
|
names, and a \`case-type\` entry only a task whose \`case\` its \`case_type\`
|
|
303
304
|
names. An entry in one of those scopes with **no** selector reaches NOBODY —
|
|
304
|
-
it is inert, not universal, and \`
|
|
305
|
-
such. \`
|
|
305
|
+
it is inert, not universal, and \`bs lessons for-dispatch\` reports it as
|
|
306
|
+
such. \`bs lessons raise\` refuses to mint one; the entries that predate
|
|
306
307
|
the selector (D-129) are the only ones that can still be in that state.
|
|
307
308
|
|
|
308
309
|
Two kinds of entry are therefore injected at dispatch and still never
|
|
@@ -388,22 +389,186 @@ function renderEntry(lesson) {
|
|
|
388
389
|
bullets.push(renderBulletLines('statement', lesson.statement));
|
|
389
390
|
return `### ${headingFor(lesson)}\n\n${bullets.join('\n')}\n`;
|
|
390
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Walks a PRE-EXISTING lessons.md, salvaging every entry compileLessons()
|
|
394
|
+
* would otherwise have to overwrite from scratch. Deliberately stricter than
|
|
395
|
+
* severity.ts's parseLessons(): that parser is read-only, so silently
|
|
396
|
+
* skipping an entry it can't make sense of just leaves the same-mistake gate
|
|
397
|
+
* with one fewer rule. Here, skipping one means DELETING hand-authored
|
|
398
|
+
* content from the compiled file with no store row left to regenerate it
|
|
399
|
+
* from — so an entry under a real `## <scope>` section that doesn't parse is
|
|
400
|
+
* a hard error instead (never a silent drop), while content above the first
|
|
401
|
+
* `## <scope>` heading (this file's own header, including its illustrative
|
|
402
|
+
* fenced `### ` example) is ignored exactly as parseLessons ignores it: real
|
|
403
|
+
* entries only start once `scope` is one of severity.ts's `LESSON_SCOPES`.
|
|
404
|
+
*
|
|
405
|
+
* Line endings are normalized to `\n` before any of the above line-based
|
|
406
|
+
* regexes run: `SECTION_HEADING`/`BULLET`'s trailing `$` and `.` never match a
|
|
407
|
+
* `\r`, so on an unmodified `\r\n` file every line silently fails to match,
|
|
408
|
+
* `scope` never leaves `''`, and every entry in the file is dropped with no
|
|
409
|
+
* error at all — the exact silent-loss failure this function exists to
|
|
410
|
+
* prevent, just triggered by a different byte.
|
|
411
|
+
*
|
|
412
|
+
* A `### ` entry that carries a `lesson_id` bullet but sits under a `##`
|
|
413
|
+
* heading that is NOT one of `LESSON_SCOPES` — an unknown or misspelled
|
|
414
|
+
* section — is refused the same way an unparseable entry is, but only once a
|
|
415
|
+
* real scope section has already been seen once in the file: before that
|
|
416
|
+
* point the file is still in its own header/doc zone (the `## Schema`
|
|
417
|
+
* walkthrough, with its illustrative fenced example), which parseLessons has
|
|
418
|
+
* always ignored and must keep being ignored here too. After the first real
|
|
419
|
+
* scope section, though, a `## ` heading is no longer prose — it is a typo
|
|
420
|
+
* or a moved section, and an entry stranded under it would otherwise vanish
|
|
421
|
+
* exactly like the CRLF case above: no bucket matches, so it is never
|
|
422
|
+
* pushed, and flush()'s `heading === null` guard means it is never even
|
|
423
|
+
* counted as unparseable.
|
|
424
|
+
*/
|
|
425
|
+
function extractExistingLessonEntries(markdown) {
|
|
426
|
+
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
427
|
+
const entries = [];
|
|
428
|
+
let scope = '';
|
|
429
|
+
let seenValidScope = false;
|
|
430
|
+
let heading = null;
|
|
431
|
+
let entryLines = [];
|
|
432
|
+
let bullets = {};
|
|
433
|
+
let lastKey = null;
|
|
434
|
+
/** Whether the entry currently being tracked sits under an unrecognized scope (see header comment). */
|
|
435
|
+
let orphan = false;
|
|
436
|
+
function flush() {
|
|
437
|
+
if (heading === null)
|
|
438
|
+
return;
|
|
439
|
+
while (entryLines.length > 0 && entryLines[entryLines.length - 1]?.trim() === '') {
|
|
440
|
+
entryLines.pop();
|
|
441
|
+
}
|
|
442
|
+
const lessonId = bullets.lesson_id;
|
|
443
|
+
const statement = bullets.statement;
|
|
444
|
+
if (orphan) {
|
|
445
|
+
if (lessonId) {
|
|
446
|
+
throw new LessonsError('lessons.entry-under-unknown-scope', `Existing lessons.md entry ${JSON.stringify(heading)} carries lesson_id ` +
|
|
447
|
+
`${JSON.stringify(lessonId)} under "## ${scope}", which is not a recognized lesson ` +
|
|
448
|
+
`scope (${VALID_SCOPES.join(', ')}) — refusing to compile, which would silently drop ` +
|
|
449
|
+
'it. Fix the scope heading or remove the entry by hand, then recompile.', { scope, heading, lessonId });
|
|
450
|
+
}
|
|
451
|
+
// No lesson_id: treated like the header's own doc examples — ignored,
|
|
452
|
+
// not an error, so free-standing prose under a stray heading doesn't
|
|
453
|
+
// become a hard failure.
|
|
454
|
+
heading = null;
|
|
455
|
+
entryLines = [];
|
|
456
|
+
bullets = {};
|
|
457
|
+
lastKey = null;
|
|
458
|
+
orphan = false;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!lessonId || !statement) {
|
|
462
|
+
throw new LessonsError('lessons.unparseable-existing-entry', `Existing lessons.md entry ${JSON.stringify(heading)} under "## ${scope}" is missing its ` +
|
|
463
|
+
`${!lessonId ? 'lesson_id' : 'statement'} bullet — refusing to compile, which would ` +
|
|
464
|
+
'silently drop it. Fix or remove the entry by hand, then recompile.', { scope, heading });
|
|
465
|
+
}
|
|
466
|
+
entries.push({ lessonId, scope, raw: `${entryLines.join('\n')}\n` });
|
|
467
|
+
heading = null;
|
|
468
|
+
entryLines = [];
|
|
469
|
+
bullets = {};
|
|
470
|
+
lastKey = null;
|
|
471
|
+
orphan = false;
|
|
472
|
+
}
|
|
473
|
+
for (const line of lines) {
|
|
474
|
+
const sectionMatch = line.match(SECTION_HEADING);
|
|
475
|
+
if (sectionMatch) {
|
|
476
|
+
flush();
|
|
477
|
+
scope = sectionMatch[1].trim();
|
|
478
|
+
if (VALID_SCOPES.includes(scope))
|
|
479
|
+
seenValidScope = true;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (ENTRY_HEADING.test(line)) {
|
|
483
|
+
flush();
|
|
484
|
+
if (VALID_SCOPES.includes(scope)) {
|
|
485
|
+
heading = line;
|
|
486
|
+
entryLines = [line];
|
|
487
|
+
bullets = {};
|
|
488
|
+
lastKey = null;
|
|
489
|
+
orphan = false;
|
|
490
|
+
}
|
|
491
|
+
else if (seenValidScope) {
|
|
492
|
+
heading = line;
|
|
493
|
+
entryLines = [line];
|
|
494
|
+
bullets = {};
|
|
495
|
+
lastKey = null;
|
|
496
|
+
orphan = true;
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (heading === null)
|
|
501
|
+
continue;
|
|
502
|
+
entryLines.push(line);
|
|
503
|
+
const bulletMatch = line.match(BULLET);
|
|
504
|
+
if (bulletMatch) {
|
|
505
|
+
const key = bulletMatch[1];
|
|
506
|
+
bullets[key] = bulletMatch[2].trim();
|
|
507
|
+
lastKey = key;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (line.trim() === '') {
|
|
511
|
+
lastKey = null;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const continuationMatch = line.match(CONTINUATION);
|
|
515
|
+
if (continuationMatch && lastKey) {
|
|
516
|
+
const existingValue = bullets[lastKey] ?? '';
|
|
517
|
+
bullets[lastKey] = `${existingValue} ${continuationMatch[1].trim()}`.trim();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
flush();
|
|
521
|
+
return entries;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Which pre-existing entries a compile of `lessons` against `existingMarkdown`
|
|
525
|
+
* would preserve — the same set compileLessons() itself splices in, exposed
|
|
526
|
+
* so a caller (the CLI's `lessons compile` handler) can report how many
|
|
527
|
+
* survived without re-deriving the "now store-tracked, so the fresh render
|
|
528
|
+
* wins" filter a second time and risking the two counts drifting apart.
|
|
529
|
+
*/
|
|
530
|
+
export function lessonsToPreserve(lessons, existingMarkdown) {
|
|
531
|
+
if (existingMarkdown === undefined)
|
|
532
|
+
return [];
|
|
533
|
+
const knownIds = new Set(lessons.map((l) => l.lessonId));
|
|
534
|
+
return extractExistingLessonEntries(existingMarkdown).filter((entry) => !knownIds.has(entry.lessonId));
|
|
535
|
+
}
|
|
391
536
|
/**
|
|
392
537
|
* Regenerates the full factory/policies/lessons.md content from a list of
|
|
393
538
|
* APPROVED lessons. Round-trips through severity.ts's parseLessons(): every
|
|
394
539
|
* entry here that carries a `finding_category` + `statement` (claim-path or
|
|
395
540
|
* stack-wide scoped) survives the parse back into a `LessonRule`.
|
|
541
|
+
*
|
|
542
|
+
* `existingMarkdown`, when given, is the compiled file's own PRIOR content
|
|
543
|
+
* (e.g. read from `--out` before overwriting it). Compiling `lessons` alone
|
|
544
|
+
* loses every entry lessons.md carries that has no row in the store — a hand
|
|
545
|
+
* lesson raised before the store existed, or one `smith lessons raise` never
|
|
546
|
+
* touched — because this function only ever knew how to build the file from
|
|
547
|
+
* scratch. With `existingMarkdown`, any such entry (its `lesson_id` absent
|
|
548
|
+
* from `lessons`) is preserved verbatim instead of dropped; an entry whose id
|
|
549
|
+
* DOES appear in `lessons` is regenerated fresh from the store, same as
|
|
550
|
+
* before — the store stays the one source of truth for anything it tracks.
|
|
551
|
+
* Compiling the real repo file against the current store this way is
|
|
552
|
+
* insertions-only: nothing already there disappears.
|
|
396
553
|
*/
|
|
397
|
-
export function compileLessons(lessons) {
|
|
554
|
+
export function compileLessons(lessons, existingMarkdown) {
|
|
398
555
|
const bySection = new Map(VALID_SCOPES.map((s) => [s, []]));
|
|
399
556
|
for (const lesson of lessons) {
|
|
400
557
|
const bucket = bySection.get(lesson.lessonScope);
|
|
401
558
|
if (bucket)
|
|
402
559
|
bucket.push(lesson);
|
|
403
560
|
}
|
|
561
|
+
const preservedBySection = new Map(VALID_SCOPES.map((s) => [s, []]));
|
|
562
|
+
for (const entry of lessonsToPreserve(lessons, existingMarkdown)) {
|
|
563
|
+
const bucket = preservedBySection.get(entry.scope);
|
|
564
|
+
if (bucket)
|
|
565
|
+
bucket.push(entry.raw);
|
|
566
|
+
}
|
|
404
567
|
const sections = VALID_SCOPES.map((scope) => {
|
|
405
|
-
const
|
|
406
|
-
const
|
|
568
|
+
const freshBlocks = (bySection.get(scope) ?? []).map(renderEntry);
|
|
569
|
+
const keptBlocks = preservedBySection.get(scope) ?? [];
|
|
570
|
+
const blocks = [...keptBlocks, ...freshBlocks];
|
|
571
|
+
const body = blocks.length === 0 ? '_(none yet)_\n' : blocks.join('\n');
|
|
407
572
|
return `## ${scope}\n\n${body}`;
|
|
408
573
|
});
|
|
409
574
|
return `${LESSONS_MD_HEADER}\n${sections.join('\n')}`;
|
|
@@ -494,7 +659,7 @@ export function scopesForRole(role, opts = {}) {
|
|
|
494
659
|
if (!ROLE_NAME.test(role)) {
|
|
495
660
|
throw new LessonsError('lessons.invalid-role-name', `Not a role name: ${JSON.stringify(role)} (expected e.g. "coder", "security-reviewer").`, { role });
|
|
496
661
|
}
|
|
497
|
-
const templatePath = path.join(opts.agentsDir ?? AGENTS_DIR,
|
|
662
|
+
const templatePath = path.join(opts.agentsDir ?? AGENTS_DIR, templateFileFor(role));
|
|
498
663
|
let template;
|
|
499
664
|
try {
|
|
500
665
|
template = readFileSync(templatePath, 'utf8');
|
|
@@ -506,13 +671,13 @@ export function scopesForRole(role, opts = {}) {
|
|
|
506
671
|
for (const match of template.matchAll(SCOPE_MARKER)) {
|
|
507
672
|
const scope = match[1];
|
|
508
673
|
if (!VALID_SCOPES.includes(scope)) {
|
|
509
|
-
throw new LessonsError('lessons.unknown-lesson-scope', `${role}
|
|
674
|
+
throw new LessonsError('lessons.unknown-lesson-scope', `${templateFileFor(role)} declares lesson scope "${scope}", which is not one of: ${VALID_SCOPES.join(', ')}.`, { role, scope, templatePath });
|
|
510
675
|
}
|
|
511
676
|
if (!scopes.includes(scope))
|
|
512
677
|
scopes.push(scope);
|
|
513
678
|
}
|
|
514
679
|
if (scopes.length === 0) {
|
|
515
|
-
throw new LessonsError('lessons.role-template-has-no-scope-marker', `${role}
|
|
680
|
+
throw new LessonsError('lessons.role-template-has-no-scope-marker', `${templateFileFor(role)} carries no <!-- LESSONS:<scope> --> marker, so no lesson can reach it.`, { role, templatePath });
|
|
516
681
|
}
|
|
517
682
|
return scopes;
|
|
518
683
|
}
|
|
@@ -594,7 +759,7 @@ export function lessonsForDispatch(role, claimPaths, opts = {}) {
|
|
|
594
759
|
markdown = readFileSync(lessonsPath, 'utf8');
|
|
595
760
|
}
|
|
596
761
|
catch {
|
|
597
|
-
throw new LessonsError('lessons.compiled-file-not-found', `No compiled lessons at ${lessonsPath} — run \`
|
|
762
|
+
throw new LessonsError('lessons.compiled-file-not-found', `No compiled lessons at ${lessonsPath} — run \`bs lessons compile\`.`, { role, lessonsPath });
|
|
598
763
|
}
|
|
599
764
|
// A mistyped case would otherwise match no entry and emit no warning — a
|
|
600
765
|
// silent empty injection, the exact failure D-129 is about. The role needs
|
|
@@ -877,6 +1042,17 @@ export async function dream(events, ctx, opts = {}, options = {}) {
|
|
|
877
1042
|
noveltyRejected.push(lessonId);
|
|
878
1043
|
}
|
|
879
1044
|
}
|
|
1045
|
+
// DS8 plan §2.3 -- a durable mark that a pass happened, even one that
|
|
1046
|
+
// raised nothing, so the Lessons page can show when lessons were last
|
|
1047
|
+
// checked rather than only when one was last raised.
|
|
1048
|
+
await appendEvent({
|
|
1049
|
+
session_id: ctx.sessionId,
|
|
1050
|
+
actor: ctx.actor ?? 'system',
|
|
1051
|
+
event_type: 'lessons-pass-completed',
|
|
1052
|
+
plan_version: ctx.planVersion,
|
|
1053
|
+
causal_parent: parent,
|
|
1054
|
+
payload: { raised: raised.length },
|
|
1055
|
+
}, opts);
|
|
880
1056
|
return {
|
|
881
1057
|
checkpointsExtracted: checkpoints.length,
|
|
882
1058
|
raised,
|
|
@@ -1111,8 +1287,9 @@ export async function raiseLessonCandidate(input, ctx, opts = {}, options = {})
|
|
|
1111
1287
|
...(contradictionOf ? { possible_contradiction_of: contradictionOf } : {}),
|
|
1112
1288
|
},
|
|
1113
1289
|
}, opts);
|
|
1290
|
+
let lastEventId = raisedEvent.event_id;
|
|
1114
1291
|
if (!novelty.novel) {
|
|
1115
|
-
await appendEvent({
|
|
1292
|
+
const rejectedEvent = await appendEvent({
|
|
1116
1293
|
session_id: ctx.sessionId,
|
|
1117
1294
|
actor: 'system',
|
|
1118
1295
|
event_type: 'lesson-status-changed',
|
|
@@ -1120,6 +1297,7 @@ export async function raiseLessonCandidate(input, ctx, opts = {}, options = {})
|
|
|
1120
1297
|
causal_parent: raisedEvent.event_id,
|
|
1121
1298
|
payload: { lesson_id: lessonId, from_status: 'candidate', to_status: 'novelty-rejected' },
|
|
1122
1299
|
}, opts);
|
|
1300
|
+
lastEventId = rejectedEvent.event_id;
|
|
1123
1301
|
}
|
|
1124
1302
|
return {
|
|
1125
1303
|
lessonId,
|
|
@@ -1128,6 +1306,7 @@ export async function raiseLessonCandidate(input, ctx, opts = {}, options = {})
|
|
|
1128
1306
|
mostSimilar: novelty.mostSimilar,
|
|
1129
1307
|
possibleContradictionOf: contradictionOf,
|
|
1130
1308
|
warnings,
|
|
1309
|
+
event_id: lastEventId,
|
|
1131
1310
|
};
|
|
1132
1311
|
}
|
|
1133
1312
|
// ---------------------------------------------------------------------------
|
|
@@ -260,7 +260,7 @@ export function addMcpSurface(opts) {
|
|
|
260
260
|
const targetDir = opts.targetDir ?? path.join(opts.projectsDir ?? PROJECTS_DIR, opts.projectName);
|
|
261
261
|
const manifestPath = path.join(targetDir, 'mcp.manifest.json');
|
|
262
262
|
if (!existsSync(path.join(targetDir, 'package.json'))) {
|
|
263
|
-
throw new McpError('mcp.unknown-project', `${targetDir} is not a scaffolded project (no package.json). Run \`
|
|
263
|
+
throw new McpError('mcp.unknown-project', `${targetDir} is not a scaffolded project (no package.json). Run \`bs new ${opts.projectName}\` first.`, { targetDir, projectName: opts.projectName });
|
|
264
264
|
}
|
|
265
265
|
if (existsSync(manifestPath)) {
|
|
266
266
|
throw new McpError('mcp.surface-exists', `${manifestPath} already exists. Edit the manifest in place; re-scaffolding would discard declared tools and their operator sign-off.`, { manifestPath, projectName: opts.projectName });
|
|
@@ -314,7 +314,7 @@ export function registerMcpMilestone(projectName, roadmapPath = ensureWritableRo
|
|
|
314
314
|
// in the roadmap itself which epic closes the milestone, the way
|
|
315
315
|
// envkit-config-loader already does, instead of leaving it to convention.
|
|
316
316
|
`- epics: [${id}]\n` +
|
|
317
|
-
'- goal: Declare and harden the MCP surface — `
|
|
317
|
+
'- goal: Declare and harden the MCP surface — `bs mcp check` green against ' +
|
|
318
318
|
'docs/standards/mcp.md. Required before the final milestone can close.\n';
|
|
319
319
|
writeFileSync(roadmapPath, `${text.trimEnd()}\n${block}`, 'utf8');
|
|
320
320
|
return id;
|
|
@@ -339,7 +339,7 @@ export function checkRoadmapMcpMilestone(project, milestones) {
|
|
|
339
339
|
{
|
|
340
340
|
rule: 'MCP-M1',
|
|
341
341
|
path: `/milestones/${project}`,
|
|
342
|
-
message: `Project "${project}" has no \`${project}-${MCP_MILESTONE_SUFFIX}\` milestone. Run \`
|
|
342
|
+
message: `Project "${project}" has no \`${project}-${MCP_MILESTONE_SUFFIX}\` milestone. Run \`bs mcp init ${project}\` to register it.`,
|
|
343
343
|
},
|
|
344
344
|
];
|
|
345
345
|
}
|
|
@@ -453,7 +453,7 @@ export function runMcpCheck(opts) {
|
|
|
453
453
|
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
454
454
|
const roadmapPath = opts.roadmapPath ?? roadmapReadPath();
|
|
455
455
|
if (!existsSync(manifestPath)) {
|
|
456
|
-
throw new McpError('mcp.no-surface', `${manifestPath} does not exist. Run \`
|
|
456
|
+
throw new McpError('mcp.no-surface', `${manifestPath} does not exist. Run \`bs mcp init ${opts.projectName}\` before checking the surface.`, { manifestPath, projectName: opts.projectName });
|
|
457
457
|
}
|
|
458
458
|
let manifest;
|
|
459
459
|
try {
|
|
@@ -548,8 +548,8 @@ export function mcpBlockers(epicId, status) {
|
|
|
548
548
|
if (!status.check) {
|
|
549
549
|
return [
|
|
550
550
|
status.problem === 'unreadable'
|
|
551
|
-
? `epic ${epicId} is under milestone ${milestone} but its MCP surface at ${manifest} exists and could not be read, so no verdict could be rendered — fix the manifest in place and run \`
|
|
552
|
-
: `epic ${epicId} is under milestone ${milestone} but has no MCP surface at ${manifest} — run \`
|
|
551
|
+
? `epic ${epicId} is under milestone ${milestone} but its MCP surface at ${manifest} exists and could not be read, so no verdict could be rendered — fix the manifest in place and run \`bs mcp check\` for the parser's own message. Do not re-run \`bs mcp init\`: it refuses over an existing manifest, because re-scaffolding would discard the declared tools and their operator sign-off.`
|
|
552
|
+
: `epic ${epicId} is under milestone ${milestone} but has no MCP surface at ${manifest} — run \`bs mcp init\` and declare it.`,
|
|
553
553
|
];
|
|
554
554
|
}
|
|
555
555
|
return status.check.violations.map((v) => `mcp surface ${milestone}: ${v.rule} at ${v.path} — ${v.message}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { readEnv } from './env.js';
|
|
4
5
|
// factory/orchestrator/src/paths.ts -> repo root is three levels up.
|
|
5
6
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
/**
|
|
@@ -29,7 +30,7 @@ export function isFactoryCheckout(dir) {
|
|
|
29
30
|
* `package.json#files` could not ship it if it tried. A linked worktree spells
|
|
30
31
|
* it as a file rather than a directory, which `existsSync` reads either way.
|
|
31
32
|
*/
|
|
32
|
-
const IS_CLONE = existsSync(path.join(REPO_ROOT, '.git'));
|
|
33
|
+
export const IS_CLONE = existsSync(path.join(REPO_ROOT, '.git'));
|
|
33
34
|
/**
|
|
34
35
|
* Where the work root goes when the CLI is not standing in a clone.
|
|
35
36
|
*
|
|
@@ -46,9 +47,10 @@ const WORK_DIR_NAME = '.blacksmith';
|
|
|
46
47
|
*
|
|
47
48
|
* Pure, and exported for the test: the constants below call it once at module
|
|
48
49
|
* load, but the install case can only be stated by a test that passes its own
|
|
49
|
-
* roots in. `
|
|
50
|
-
* into itself, and an install writes beside
|
|
51
|
-
* `node_modules/@juzser/blacksmith`, which the
|
|
50
|
+
* roots in. `BS_HOME` (or its legacy `SMITH_HOME` fallback) wins outright;
|
|
51
|
+
* otherwise a clone keeps writing into itself, and an install writes beside
|
|
52
|
+
* the operator instead of into `node_modules/@juzser/blacksmith`, which the
|
|
53
|
+
* next `npm i` replaces wholesale.
|
|
52
54
|
*
|
|
53
55
|
* The layout *under* the root is identical in both cases on purpose. A path
|
|
54
56
|
* like `state/events/<session>.jsonl` is spelled in runbooks, in agent
|
|
@@ -56,9 +58,10 @@ const WORK_DIR_NAME = '.blacksmith';
|
|
|
56
58
|
* if only the anchor moves.
|
|
57
59
|
*/
|
|
58
60
|
export function resolveWorkRoot(repoRoot, cwd, env, isClone) {
|
|
59
|
-
const declared = env
|
|
60
|
-
// An empty `SMITH_HOME=` is how an unset variable gets spelled by
|
|
61
|
-
// a shell profile or a CI matrix; resolved literally it would
|
|
61
|
+
const declared = readEnv(env, 'BS_HOME')?.trim();
|
|
62
|
+
// An empty `BS_HOME=`/`SMITH_HOME=` is how an unset variable gets spelled by
|
|
63
|
+
// accident in a shell profile or a CI matrix; resolved literally it would
|
|
64
|
+
// mean cwd.
|
|
62
65
|
if (declared)
|
|
63
66
|
return path.resolve(cwd, declared);
|
|
64
67
|
return isClone ? repoRoot : path.join(cwd, WORK_DIR_NAME);
|