bearings 0.4.0 → 0.5.1

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.
@@ -14,11 +14,21 @@ var STARTER_SKILL_NAMES = [
14
14
  ];
15
15
  var SKILLS = STARTER_SKILL_NAMES;
16
16
  var MANUAL_TESTPLAN_FILES = [
17
+ "SKILL.md"
18
+ ];
19
+ var FORGE_A_SKILL_FILES = [
20
+ "SKILL.md",
21
+ "templates/guardrail.md",
22
+ "templates/probe-set.json"
23
+ ];
24
+ var CHECKLIST_FILES = [
17
25
  "SKILL.md",
18
26
  "schema.reference.json",
19
27
  "scripts/validate.mjs",
20
- "scripts/view.mjs",
21
- "viewer.html"
28
+ "scripts/build.mjs",
29
+ "previewer/index.html",
30
+ "previewer/viewer.css",
31
+ "previewer/viewer.js"
22
32
  ];
23
33
  function starterSkillNameFromTarget(targetPath) {
24
34
  return STARTER_SKILL_NAMES.find((skillName) => targetPath === `.agents/skills/${skillName}/SKILL.md`);
@@ -52,6 +62,16 @@ var SCAFFOLD = [
52
62
  template: `agents/skills/manual-testplan/${file}`,
53
63
  target: `.agents/skills/manual-testplan/${file}`,
54
64
  owner: "bearings"
65
+ })),
66
+ ...FORGE_A_SKILL_FILES.map((file) => ({
67
+ template: `agents/skills/forge-a-skill/${file}`,
68
+ target: `.agents/skills/forge-a-skill/${file}`,
69
+ owner: "bearings"
70
+ })),
71
+ ...CHECKLIST_FILES.map((file) => ({
72
+ template: `agents/skills/checklist/${file}`,
73
+ target: `.agents/skills/checklist/${file}`,
74
+ owner: "bearings"
55
75
  }))
56
76
  ];
57
77
 
@@ -210,6 +230,24 @@ function validateSetupPending(value) {
210
230
  toVersion
211
231
  };
212
232
  }
233
+ function validateMigrationReconciliation(value, index) {
234
+ const context = `migrationReconciliations[${index}]`;
235
+ if (!isPlainObject(value)) fail(`${context} must be an object`);
236
+ const { kind, source, target, backup, sourceHash, incomingHash } = value;
237
+ if (kind !== "testplan-collision" && kind !== "invalid-testplan") {
238
+ fail(`${context}.kind must be "testplan-collision" or "invalid-testplan"`);
239
+ }
240
+ if (!isSafeManifestPath(source)) fail(`${context}.source is unsafe: ${String(source)}`);
241
+ if (!isSafeManifestPath(backup)) fail(`${context}.backup is unsafe: ${String(backup)}`);
242
+ if (!isHash(sourceHash)) fail(`${context}.sourceHash must match sha256 format`);
243
+ if (kind === "testplan-collision") {
244
+ if (!isSafeManifestPath(target)) fail(`${context}.target is unsafe: ${String(target)}`);
245
+ if (!isHash(incomingHash)) fail(`${context}.incomingHash must match sha256 format`);
246
+ return { kind, source, target, backup, sourceHash, incomingHash };
247
+ }
248
+ if (target !== void 0 && !isSafeManifestPath(target)) fail(`${context}.target is unsafe: ${String(target)}`);
249
+ return { kind, source, ...target !== void 0 ? { target } : {}, backup, sourceHash };
250
+ }
213
251
  function parseManifest(raw) {
214
252
  if (!isPlainObject(raw)) fail("manifest must be an object");
215
253
  const { version, bearingsVersion, harnesses, exposure, files } = raw;
@@ -225,13 +263,19 @@ function parseManifest(raw) {
225
263
  if (version === 2) {
226
264
  const validatedFiles = files.map((file, index) => validateFileV2(file, index));
227
265
  assertUniquePaths(validatedFiles.map((file) => file.path));
228
- const { setupPending } = raw;
266
+ const { setupPending, migrationReconciliations } = raw;
267
+ if (migrationReconciliations !== void 0 && !Array.isArray(migrationReconciliations)) {
268
+ fail("migrationReconciliations must be an array");
269
+ }
229
270
  return {
230
271
  version: 2,
231
272
  bearingsVersion,
232
273
  harnesses,
233
274
  exposure,
234
275
  ...setupPending !== void 0 ? { setupPending: validateSetupPending(setupPending) } : {},
276
+ ...migrationReconciliations !== void 0 ? {
277
+ migrationReconciliations: migrationReconciliations.map(validateMigrationReconciliation)
278
+ } : {},
235
279
  files: validatedFiles
236
280
  };
237
281
  }
@@ -348,6 +392,7 @@ function renderUpdatePlan(input) {
348
392
  pushSection(lines, "Skipped", categories.skipped);
349
393
  pushSection(lines, "Removed", categories.removed);
350
394
  pushSection(lines, "Kept and untracked", categories.keptUntracked);
395
+ pushMigrationSections(lines, input.migrationActions ?? []);
351
396
  if (input.adapterActions.length) {
352
397
  lines.push("", `Adapter changes (${input.adapterActions.length}):`);
353
398
  for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
@@ -372,6 +417,7 @@ function renderUpdateReport(input) {
372
417
  pushSection(lines, "Skipped", categories.skipped);
373
418
  pushSection(lines, "Removed", categories.removed);
374
419
  pushSection(lines, "Kept and untracked", categories.keptUntracked);
420
+ pushMigrationSections(lines, input.migrationActions ?? []);
375
421
  if (input.adapterActions.length) {
376
422
  lines.push("", `Adapter changes (${input.adapterActions.length}):`);
377
423
  for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
@@ -394,6 +440,12 @@ function renderUpdateReport(input) {
394
440
  }
395
441
  return lines.join("\n");
396
442
  }
443
+ function pushMigrationSections(lines, actions) {
444
+ pushSection(lines, "Migrated legacy test plans", actions.filter((action) => action.kind === "migrate-testplan").map((action) => `${action.source} -> ${action.target}`));
445
+ pushSection(lines, "Migration collisions backed up -> /setup-repo", actions.flatMap((action) => action.kind === "migrate-testplan" && action.backup !== void 0 ? [`${action.target} -> ${action.backup}`] : []));
446
+ pushSection(lines, "Invalid legacy test plans preserved -> /setup-repo", actions.filter((action) => action.kind === "preserve-invalid-testplan").map((action) => `${action.source} -> ${action.backup}`));
447
+ pushSection(lines, "Removed empty legacy plan directories", actions.filter((action) => action.kind === "remove-empty-testplan-dir").map((action) => action.path));
448
+ }
397
449
  function renderVerifyReport(v) {
398
450
  const lines = [];
399
451
  for (const f of v.failures) lines.push(`FAIL ${f.code} ${f.path} \u2014 ${f.message}`);
@@ -405,7 +457,7 @@ ${v.failures.length} failure(s).` : "\nbearings verify: OK");
405
457
 
406
458
  // src/commands/init.ts
407
459
  import { lstat as lstat2, readdir as readdir2 } from "fs/promises";
408
- import { join as join6 } from "path";
460
+ import { join as join7 } from "path";
409
461
 
410
462
  // src/scanner.ts
411
463
  import { access as access2, symlink, rm } from "fs/promises";
@@ -436,40 +488,78 @@ async function scan(repoDir) {
436
488
  }
437
489
 
438
490
  // src/generator.ts
439
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rename, access as access3 } from "fs/promises";
491
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2, access as access4 } from "fs/promises";
492
+ import { dirname as dirname3, join as join5 } from "path";
493
+
494
+ // src/backup-path.ts
495
+ import { access as access3, mkdir as mkdir2, open, rm as rm2 } from "fs/promises";
440
496
  import { dirname as dirname2, join as join4 } from "path";
497
+ async function freeBackupPath(repoDir, target) {
498
+ let candidate = `${target}.bkp`;
499
+ for (let index = 1; await access3(join4(repoDir, candidate)).then(() => true, () => false); index++) {
500
+ candidate = `${target}.bkp.${index}`;
501
+ }
502
+ return candidate;
503
+ }
504
+ async function writeBackup(repoDir, target, content, options = {}) {
505
+ let candidate = options.path ?? `${target}.bkp`;
506
+ for (let index = 1; ; index++) {
507
+ await options.beforeCreate?.(candidate);
508
+ const absolute = join4(repoDir, candidate);
509
+ await mkdir2(dirname2(absolute), { recursive: true });
510
+ let handle;
511
+ try {
512
+ handle = await open(absolute, "wx");
513
+ } catch (error) {
514
+ if (!options.path && error.code === "EEXIST") {
515
+ candidate = `${target}.bkp.${index}`;
516
+ continue;
517
+ }
518
+ throw error;
519
+ }
520
+ try {
521
+ await handle.writeFile(content);
522
+ await handle.close();
523
+ return candidate;
524
+ } catch (error) {
525
+ await handle.close().catch(() => {
526
+ });
527
+ await rm2(absolute, { force: true }).catch(() => {
528
+ });
529
+ throw error;
530
+ }
531
+ }
532
+ }
533
+
534
+ // src/generator.ts
441
535
  async function exists2(p) {
442
536
  try {
443
- await access3(p);
537
+ await access4(p);
444
538
  return true;
445
539
  } catch {
446
540
  return false;
447
541
  }
448
542
  }
449
- async function freeBackupPath(repoDir, target) {
450
- let candidate = `${target}.bkp`;
451
- for (let i = 1; await exists2(join4(repoDir, candidate)); i++) candidate = `${target}.bkp.${i}`;
452
- return candidate;
453
- }
454
- async function generate(repoDir, bearingsVersion) {
543
+ async function generate(repoDir, bearingsVersion, hooks = {}) {
455
544
  const priorState = await inspectManifest(repoDir);
456
545
  if (priorState.kind === "invalid") throw new Error(`Invalid manifest: ${priorState.message}`);
457
546
  const prior = priorState.kind === "valid" ? priorState.manifest.version === 1 ? await migrateV1(repoDir, priorState.manifest) : priorState.manifest : null;
458
547
  const result = { written: [], backedUp: [], skippedUnchanged: [], files: [] };
459
548
  for (const entry of SCAFFOLD) {
460
- const abs = join4(repoDir, entry.target);
461
- const templateContent = await readFile2(join4(templatesDir(), entry.template), "utf8");
549
+ const abs = join5(repoDir, entry.target);
550
+ const templateContent = await readFile2(join5(templatesDir(), entry.template), "utf8");
462
551
  const priorEntry = prior?.files.find((f) => f.path === entry.target);
463
552
  let backup;
464
553
  let currentContent;
465
554
  if (await exists2(abs)) {
466
- currentContent = await readFile2(abs, "utf8");
555
+ const currentBytes = await readFile2(abs);
556
+ currentContent = currentBytes.toString("utf8");
467
557
  if (priorEntry && sha256(currentContent) === priorEntry.hash) {
468
558
  const skillName2 = starterSkillNameFromTarget(entry.target);
469
559
  if (skillName2) {
470
- const baseline = join4(repoDir, skillBaselinePath(skillName2));
560
+ const baseline = join5(repoDir, skillBaselinePath(skillName2));
471
561
  if (!await exists2(baseline)) {
472
- await mkdir2(dirname2(baseline), { recursive: true });
562
+ await mkdir3(dirname3(baseline), { recursive: true });
473
563
  await writeFile2(baseline, currentContent);
474
564
  }
475
565
  }
@@ -477,18 +567,19 @@ async function generate(repoDir, bearingsVersion) {
477
567
  result.files.push(priorEntry);
478
568
  continue;
479
569
  }
480
- backup = await freeBackupPath(repoDir, entry.target);
481
- await rename(abs, join4(repoDir, backup));
570
+ backup = await writeBackup(repoDir, entry.target, currentBytes, {
571
+ beforeCreate: hooks.beforeBackupCreate
572
+ });
482
573
  result.backedUp.push({ path: entry.target, backup });
483
574
  }
484
- await mkdir2(dirname2(abs), { recursive: true });
575
+ await mkdir3(dirname3(abs), { recursive: true });
485
576
  await writeFile2(abs, templateContent);
486
577
  result.written.push(entry.target);
487
578
  const incomingHash = sha256(templateContent);
488
579
  const skillName = starterSkillNameFromTarget(entry.target);
489
580
  if (skillName) {
490
- const baseline = join4(repoDir, skillBaselinePath(skillName));
491
- await mkdir2(dirname2(baseline), { recursive: true });
581
+ const baseline = join5(repoDir, skillBaselinePath(skillName));
582
+ await mkdir3(dirname3(baseline), { recursive: true });
492
583
  await writeFile2(baseline, templateContent);
493
584
  }
494
585
  const reconciliation = backup && currentContent !== void 0 ? {
@@ -512,15 +603,15 @@ async function generate(repoDir, bearingsVersion) {
512
603
  }
513
604
 
514
605
  // src/adapters.ts
515
- import { mkdir as mkdir3, readdir, symlink as symlink2, lstat, readlink, rm as rm2, cp, readFile as readFile3 } from "fs/promises";
516
- import { join as join5 } from "path";
606
+ import { mkdir as mkdir4, readdir, symlink as symlink2, lstat, readlink, rm as rm3, cp, readFile as readFile3 } from "fs/promises";
607
+ import { join as join6 } from "path";
517
608
  var ADAPTER_KINDS = ["skills", "commands"];
518
609
  async function canonicalAdapterEntries(repoDir) {
519
610
  const entries = /* @__PURE__ */ new Map();
520
611
  for (const kind of ADAPTER_KINDS) {
521
- const directory = join5(repoDir, ".agents", kind);
612
+ const directory = join6(repoDir, ".agents", kind);
522
613
  for (const name of await readdir(directory).catch(() => [])) {
523
- entries.set(`${kind}/${name}`, join5(directory, name));
614
+ entries.set(`${kind}/${name}`, join6(directory, name));
524
615
  }
525
616
  }
526
617
  return entries;
@@ -540,7 +631,7 @@ async function entriesMatch(src, dst) {
540
631
  dstEntries.sort();
541
632
  for (let i = 0; i < srcEntries.length; i++) {
542
633
  if (srcEntries[i] !== dstEntries[i]) return false;
543
- if (!await entriesMatch(join5(src, srcEntries[i]), join5(dst, dstEntries[i]))) return false;
634
+ if (!await entriesMatch(join6(src, srcEntries[i]), join6(dst, dstEntries[i]))) return false;
544
635
  }
545
636
  return true;
546
637
  }
@@ -552,30 +643,30 @@ async function entriesMatch(src, dst) {
552
643
  async function expose(repoDir, harness, mode) {
553
644
  const created = [];
554
645
  for (const kind of ADAPTER_KINDS) {
555
- const srcDir = join5(repoDir, ".agents", kind);
646
+ const srcDir = join6(repoDir, ".agents", kind);
556
647
  let entries;
557
648
  try {
558
649
  entries = await readdir(srcDir);
559
650
  } catch {
560
651
  continue;
561
652
  }
562
- const dstDir = join5(repoDir, `.${harness}`, kind);
563
- await mkdir3(dstDir, { recursive: true });
653
+ const dstDir = join6(repoDir, `.${harness}`, kind);
654
+ await mkdir4(dstDir, { recursive: true });
564
655
  for (const name of entries) {
565
- const dst = join5(dstDir, name);
566
- const adapterPath = join5(`.${harness}`, kind, name);
567
- const relTarget = join5("..", "..", ".agents", kind, name);
656
+ const dst = join6(dstDir, name);
657
+ const adapterPath = join6(`.${harness}`, kind, name);
658
+ const relTarget = join6("..", "..", ".agents", kind, name);
568
659
  const stat = await lstat(dst).catch(() => null);
569
660
  if (mode === "symlink") {
570
661
  if (stat?.isSymbolicLink() && await readlink(dst) === relTarget) continue;
571
662
  if (stat && !stat.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
572
- if (stat) await rm2(dst, { recursive: true });
663
+ if (stat) await rm3(dst, { recursive: true });
573
664
  await symlink2(relTarget, dst);
574
665
  } else {
575
- const src = join5(srcDir, name);
666
+ const src = join6(srcDir, name);
576
667
  if (stat && await entriesMatch(src, dst)) continue;
577
668
  if (stat && !stat.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
578
- if (stat) await rm2(dst, { recursive: true });
669
+ if (stat) await rm3(dst, { recursive: true });
579
670
  await cp(src, dst, { recursive: true });
580
671
  }
581
672
  created.push(adapterPath);
@@ -607,14 +698,14 @@ async function plannedAdapterSources(repoDir) {
607
698
  const prefix = `.agents/${kind}/`;
608
699
  if (entry.target.startsWith(prefix)) {
609
700
  const name = entry.target.slice(prefix.length).split("/")[0];
610
- sources[kind].set(name, join6(templatesDir(), "agents", kind, name));
701
+ sources[kind].set(name, join7(templatesDir(), "agents", kind, name));
611
702
  }
612
703
  }
613
704
  }
614
705
  for (const kind of KINDS) {
615
- const srcDir = join6(repoDir, ".agents", kind);
706
+ const srcDir = join7(repoDir, ".agents", kind);
616
707
  for (const name of await readdir2(srcDir).catch(() => [])) {
617
- if (!sources[kind].has(name)) sources[kind].set(name, join6(srcDir, name));
708
+ if (!sources[kind].has(name)) sources[kind].set(name, join7(srcDir, name));
618
709
  }
619
710
  }
620
711
  return sources;
@@ -624,8 +715,8 @@ async function preflightAdapterCollisions(repoDir, harnesses, exposure) {
624
715
  for (const h of harnesses) {
625
716
  for (const kind of KINDS) {
626
717
  for (const [name, source] of sources[kind]) {
627
- const adapterPath = join6(`.${h}`, kind, name);
628
- const target = join6(repoDir, adapterPath);
718
+ const adapterPath = join7(`.${h}`, kind, name);
719
+ const target = join7(repoDir, adapterPath);
629
720
  const stat = await lstat2(target).catch(() => null);
630
721
  if (!stat || stat.isSymbolicLink()) continue;
631
722
  if (exposure === "copy" && await entriesMatch(source, target)) continue;
@@ -692,7 +783,7 @@ async function runFreshInit(repoDir, flags, version) {
692
783
  async function runInit(repoDir, flags, version) {
693
784
  const state = await inspectManifest(repoDir);
694
785
  if (state.kind === "absent") return runFreshInit(repoDir, flags, version);
695
- const { runUpdate } = await import("./update-TBMTUJUT.js");
786
+ const { runUpdate } = await import("./update-QTWNGAGX.js");
696
787
  return runUpdate(repoDir, flags, version, state);
697
788
  }
698
789
 
@@ -706,6 +797,8 @@ export {
706
797
  sha256,
707
798
  inspectManifest,
708
799
  migrateV1,
800
+ freeBackupPath,
801
+ writeBackup,
709
802
  ADAPTER_KINDS,
710
803
  canonicalAdapterEntries,
711
804
  entriesMatch,
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  skillBaselinePath,
9
9
  starterSkillNameFromTarget,
10
10
  validateHarnesses
11
- } from "./chunk-KQPOIH7N.js";
11
+ } from "./chunk-5D5YO3E3.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { Command } from "commander";
@@ -81,6 +81,17 @@ async function verify(repoDir) {
81
81
  }
82
82
  }
83
83
  }
84
+ if ("migrationReconciliations" in m && m.migrationReconciliations) {
85
+ for (const reconciliation of m.migrationReconciliations) {
86
+ if (await exists(join(repoDir, reconciliation.backup))) {
87
+ warnings.push({
88
+ code: "pending-reconciliation",
89
+ path: reconciliation.backup,
90
+ message: reconciliation.kind === "invalid-testplan" ? "repair with the checklist skill during /setup-repo, then delete" : "review during /setup-repo, then delete"
91
+ });
92
+ }
93
+ }
94
+ }
84
95
  for (const kind of KINDS) {
85
96
  const srcDir = join(repoDir, ".agents", kind);
86
97
  let entries = [];
@@ -150,23 +161,48 @@ async function runVerify(repoDir) {
150
161
  return v.failures.length ? 1 : 0;
151
162
  }
152
163
 
153
- // src/commands/testplan.ts
164
+ // src/commands/checklist.ts
165
+ import { spawn } from "child_process";
154
166
  import { access as access2 } from "fs/promises";
155
167
  import { join as join2 } from "path";
156
- import { spawn } from "child_process";
157
- async function runTestplan(repoDir) {
158
- const launcher = join2(repoDir, ".agents/skills/manual-testplan/scripts/view.mjs");
159
- try {
160
- await access2(launcher);
161
- } catch {
162
- throw new Error("Manual test-plan viewer is not installed. Run `bearings init` to update this repository.");
163
- }
168
+ function openChecklist(path, platform = process.platform, spawnProcess = spawn) {
169
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
170
+ const args = platform === "win32" ? ["/c", "start", "", path] : [path];
164
171
  return new Promise((resolve2, reject) => {
165
- const child = spawn(process.execPath, [launcher], { cwd: repoDir, stdio: "inherit" });
172
+ const child = spawnProcess(command, args, { detached: true, stdio: "ignore" });
173
+ child.once("error", reject);
174
+ child.once("spawn", () => resolve2());
175
+ child.unref();
176
+ });
177
+ }
178
+ async function runBuilder(builder, repoDir) {
179
+ return new Promise((resolve2, reject) => {
180
+ const child = spawn(process.execPath, [builder, repoDir], { cwd: repoDir, stdio: "inherit" });
166
181
  child.once("error", reject);
167
182
  child.once("exit", (code) => resolve2(code ?? 1));
168
183
  });
169
184
  }
185
+ async function runChecklist(repoDir, open = openChecklist, log = console.log) {
186
+ const bundle = join2(repoDir, ".agents/skills/checklist");
187
+ const builder = join2(bundle, "scripts/build.mjs");
188
+ const previewer = join2(bundle, "previewer/index.html");
189
+ log(previewer);
190
+ try {
191
+ await Promise.all([
192
+ builder,
193
+ join2(bundle, "scripts/validate.mjs"),
194
+ previewer,
195
+ join2(bundle, "previewer/viewer.js"),
196
+ join2(bundle, "previewer/viewer.css")
197
+ ].map((path) => access2(path)));
198
+ } catch {
199
+ throw new Error("Checklist previewer is not installed. Run `bearings init` to update this repository.");
200
+ }
201
+ const buildCode = await runBuilder(builder, repoDir);
202
+ if (buildCode !== 0) return buildCode;
203
+ await open(previewer);
204
+ return 0;
205
+ }
170
206
 
171
207
  // src/cli.ts
172
208
  var pkg = JSON.parse(
@@ -184,8 +220,8 @@ program.command("init").description("Scaffold the agent-friendly setup in the cu
184
220
  program.command("verify").description("Check the agent-friendly setup for drift and breakage").action(async () => {
185
221
  process.exitCode = await runVerify(process.cwd());
186
222
  });
187
- program.command("testplan").description("Open the manual test-plan visualizer").action(async () => {
188
- process.exitCode = await runTestplan(process.cwd());
223
+ program.command("checklist").description("Build and open the checklist previewer").action(async () => {
224
+ process.exitCode = await runChecklist(process.cwd());
189
225
  });
190
226
  program.parseAsync().catch((error) => {
191
227
  console.error(error instanceof Error ? error.message : String(error));