bearings 0.3.4 → 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.
@@ -13,6 +13,23 @@ var STARTER_SKILL_NAMES = [
13
13
  "recording-decisions"
14
14
  ];
15
15
  var SKILLS = STARTER_SKILL_NAMES;
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 = [
25
+ "SKILL.md",
26
+ "schema.reference.json",
27
+ "scripts/validate.mjs",
28
+ "scripts/build.mjs",
29
+ "previewer/index.html",
30
+ "previewer/viewer.css",
31
+ "previewer/viewer.js"
32
+ ];
16
33
  function starterSkillNameFromTarget(targetPath) {
17
34
  return STARTER_SKILL_NAMES.find((skillName) => targetPath === `.agents/skills/${skillName}/SKILL.md`);
18
35
  }
@@ -40,6 +57,21 @@ var SCAFFOLD = [
40
57
  template: `agents/skills/${skill}/SKILL.md`,
41
58
  target: `.agents/skills/${skill}/SKILL.md`,
42
59
  owner: "bearings"
60
+ })),
61
+ ...MANUAL_TESTPLAN_FILES.map((file) => ({
62
+ template: `agents/skills/manual-testplan/${file}`,
63
+ target: `.agents/skills/manual-testplan/${file}`,
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"
43
75
  }))
44
76
  ];
45
77
 
@@ -198,6 +230,24 @@ function validateSetupPending(value) {
198
230
  toVersion
199
231
  };
200
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
+ }
201
251
  function parseManifest(raw) {
202
252
  if (!isPlainObject(raw)) fail("manifest must be an object");
203
253
  const { version, bearingsVersion, harnesses, exposure, files } = raw;
@@ -213,13 +263,19 @@ function parseManifest(raw) {
213
263
  if (version === 2) {
214
264
  const validatedFiles = files.map((file, index) => validateFileV2(file, index));
215
265
  assertUniquePaths(validatedFiles.map((file) => file.path));
216
- const { setupPending } = raw;
266
+ const { setupPending, migrationReconciliations } = raw;
267
+ if (migrationReconciliations !== void 0 && !Array.isArray(migrationReconciliations)) {
268
+ fail("migrationReconciliations must be an array");
269
+ }
217
270
  return {
218
271
  version: 2,
219
272
  bearingsVersion,
220
273
  harnesses,
221
274
  exposure,
222
275
  ...setupPending !== void 0 ? { setupPending: validateSetupPending(setupPending) } : {},
276
+ ...migrationReconciliations !== void 0 ? {
277
+ migrationReconciliations: migrationReconciliations.map(validateMigrationReconciliation)
278
+ } : {},
223
279
  files: validatedFiles
224
280
  };
225
281
  }
@@ -336,6 +392,7 @@ function renderUpdatePlan(input) {
336
392
  pushSection(lines, "Skipped", categories.skipped);
337
393
  pushSection(lines, "Removed", categories.removed);
338
394
  pushSection(lines, "Kept and untracked", categories.keptUntracked);
395
+ pushMigrationSections(lines, input.migrationActions ?? []);
339
396
  if (input.adapterActions.length) {
340
397
  lines.push("", `Adapter changes (${input.adapterActions.length}):`);
341
398
  for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
@@ -360,6 +417,7 @@ function renderUpdateReport(input) {
360
417
  pushSection(lines, "Skipped", categories.skipped);
361
418
  pushSection(lines, "Removed", categories.removed);
362
419
  pushSection(lines, "Kept and untracked", categories.keptUntracked);
420
+ pushMigrationSections(lines, input.migrationActions ?? []);
363
421
  if (input.adapterActions.length) {
364
422
  lines.push("", `Adapter changes (${input.adapterActions.length}):`);
365
423
  for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
@@ -382,6 +440,12 @@ function renderUpdateReport(input) {
382
440
  }
383
441
  return lines.join("\n");
384
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
+ }
385
449
  function renderVerifyReport(v) {
386
450
  const lines = [];
387
451
  for (const f of v.failures) lines.push(`FAIL ${f.code} ${f.path} \u2014 ${f.message}`);
@@ -393,7 +457,7 @@ ${v.failures.length} failure(s).` : "\nbearings verify: OK");
393
457
 
394
458
  // src/commands/init.ts
395
459
  import { lstat as lstat2, readdir as readdir2 } from "fs/promises";
396
- import { join as join6 } from "path";
460
+ import { join as join7 } from "path";
397
461
 
398
462
  // src/scanner.ts
399
463
  import { access as access2, symlink, rm } from "fs/promises";
@@ -424,40 +488,78 @@ async function scan(repoDir) {
424
488
  }
425
489
 
426
490
  // src/generator.ts
427
- 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";
428
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
429
535
  async function exists2(p) {
430
536
  try {
431
- await access3(p);
537
+ await access4(p);
432
538
  return true;
433
539
  } catch {
434
540
  return false;
435
541
  }
436
542
  }
437
- async function freeBackupPath(repoDir, target) {
438
- let candidate = `${target}.bkp`;
439
- for (let i = 1; await exists2(join4(repoDir, candidate)); i++) candidate = `${target}.bkp.${i}`;
440
- return candidate;
441
- }
442
- async function generate(repoDir, bearingsVersion) {
543
+ async function generate(repoDir, bearingsVersion, hooks = {}) {
443
544
  const priorState = await inspectManifest(repoDir);
444
545
  if (priorState.kind === "invalid") throw new Error(`Invalid manifest: ${priorState.message}`);
445
546
  const prior = priorState.kind === "valid" ? priorState.manifest.version === 1 ? await migrateV1(repoDir, priorState.manifest) : priorState.manifest : null;
446
547
  const result = { written: [], backedUp: [], skippedUnchanged: [], files: [] };
447
548
  for (const entry of SCAFFOLD) {
448
- const abs = join4(repoDir, entry.target);
449
- 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");
450
551
  const priorEntry = prior?.files.find((f) => f.path === entry.target);
451
552
  let backup;
452
553
  let currentContent;
453
554
  if (await exists2(abs)) {
454
- currentContent = await readFile2(abs, "utf8");
555
+ const currentBytes = await readFile2(abs);
556
+ currentContent = currentBytes.toString("utf8");
455
557
  if (priorEntry && sha256(currentContent) === priorEntry.hash) {
456
558
  const skillName2 = starterSkillNameFromTarget(entry.target);
457
559
  if (skillName2) {
458
- const baseline = join4(repoDir, skillBaselinePath(skillName2));
560
+ const baseline = join5(repoDir, skillBaselinePath(skillName2));
459
561
  if (!await exists2(baseline)) {
460
- await mkdir2(dirname2(baseline), { recursive: true });
562
+ await mkdir3(dirname3(baseline), { recursive: true });
461
563
  await writeFile2(baseline, currentContent);
462
564
  }
463
565
  }
@@ -465,18 +567,19 @@ async function generate(repoDir, bearingsVersion) {
465
567
  result.files.push(priorEntry);
466
568
  continue;
467
569
  }
468
- backup = await freeBackupPath(repoDir, entry.target);
469
- await rename(abs, join4(repoDir, backup));
570
+ backup = await writeBackup(repoDir, entry.target, currentBytes, {
571
+ beforeCreate: hooks.beforeBackupCreate
572
+ });
470
573
  result.backedUp.push({ path: entry.target, backup });
471
574
  }
472
- await mkdir2(dirname2(abs), { recursive: true });
575
+ await mkdir3(dirname3(abs), { recursive: true });
473
576
  await writeFile2(abs, templateContent);
474
577
  result.written.push(entry.target);
475
578
  const incomingHash = sha256(templateContent);
476
579
  const skillName = starterSkillNameFromTarget(entry.target);
477
580
  if (skillName) {
478
- const baseline = join4(repoDir, skillBaselinePath(skillName));
479
- await mkdir2(dirname2(baseline), { recursive: true });
581
+ const baseline = join5(repoDir, skillBaselinePath(skillName));
582
+ await mkdir3(dirname3(baseline), { recursive: true });
480
583
  await writeFile2(baseline, templateContent);
481
584
  }
482
585
  const reconciliation = backup && currentContent !== void 0 ? {
@@ -500,15 +603,15 @@ async function generate(repoDir, bearingsVersion) {
500
603
  }
501
604
 
502
605
  // src/adapters.ts
503
- import { mkdir as mkdir3, readdir, symlink as symlink2, lstat, readlink, rm as rm2, cp, readFile as readFile3 } from "fs/promises";
504
- 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";
505
608
  var ADAPTER_KINDS = ["skills", "commands"];
506
609
  async function canonicalAdapterEntries(repoDir) {
507
610
  const entries = /* @__PURE__ */ new Map();
508
611
  for (const kind of ADAPTER_KINDS) {
509
- const directory = join5(repoDir, ".agents", kind);
612
+ const directory = join6(repoDir, ".agents", kind);
510
613
  for (const name of await readdir(directory).catch(() => [])) {
511
- entries.set(`${kind}/${name}`, join5(directory, name));
614
+ entries.set(`${kind}/${name}`, join6(directory, name));
512
615
  }
513
616
  }
514
617
  return entries;
@@ -528,7 +631,7 @@ async function entriesMatch(src, dst) {
528
631
  dstEntries.sort();
529
632
  for (let i = 0; i < srcEntries.length; i++) {
530
633
  if (srcEntries[i] !== dstEntries[i]) return false;
531
- 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;
532
635
  }
533
636
  return true;
534
637
  }
@@ -540,30 +643,30 @@ async function entriesMatch(src, dst) {
540
643
  async function expose(repoDir, harness, mode) {
541
644
  const created = [];
542
645
  for (const kind of ADAPTER_KINDS) {
543
- const srcDir = join5(repoDir, ".agents", kind);
646
+ const srcDir = join6(repoDir, ".agents", kind);
544
647
  let entries;
545
648
  try {
546
649
  entries = await readdir(srcDir);
547
650
  } catch {
548
651
  continue;
549
652
  }
550
- const dstDir = join5(repoDir, `.${harness}`, kind);
551
- await mkdir3(dstDir, { recursive: true });
653
+ const dstDir = join6(repoDir, `.${harness}`, kind);
654
+ await mkdir4(dstDir, { recursive: true });
552
655
  for (const name of entries) {
553
- const dst = join5(dstDir, name);
554
- const adapterPath = join5(`.${harness}`, kind, name);
555
- 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);
556
659
  const stat = await lstat(dst).catch(() => null);
557
660
  if (mode === "symlink") {
558
661
  if (stat?.isSymbolicLink() && await readlink(dst) === relTarget) continue;
559
662
  if (stat && !stat.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
560
- if (stat) await rm2(dst, { recursive: true });
663
+ if (stat) await rm3(dst, { recursive: true });
561
664
  await symlink2(relTarget, dst);
562
665
  } else {
563
- const src = join5(srcDir, name);
666
+ const src = join6(srcDir, name);
564
667
  if (stat && await entriesMatch(src, dst)) continue;
565
668
  if (stat && !stat.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
566
- if (stat) await rm2(dst, { recursive: true });
669
+ if (stat) await rm3(dst, { recursive: true });
567
670
  await cp(src, dst, { recursive: true });
568
671
  }
569
672
  created.push(adapterPath);
@@ -595,14 +698,14 @@ async function plannedAdapterSources(repoDir) {
595
698
  const prefix = `.agents/${kind}/`;
596
699
  if (entry.target.startsWith(prefix)) {
597
700
  const name = entry.target.slice(prefix.length).split("/")[0];
598
- sources[kind].set(name, join6(templatesDir(), "agents", kind, name));
701
+ sources[kind].set(name, join7(templatesDir(), "agents", kind, name));
599
702
  }
600
703
  }
601
704
  }
602
705
  for (const kind of KINDS) {
603
- const srcDir = join6(repoDir, ".agents", kind);
706
+ const srcDir = join7(repoDir, ".agents", kind);
604
707
  for (const name of await readdir2(srcDir).catch(() => [])) {
605
- 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));
606
709
  }
607
710
  }
608
711
  return sources;
@@ -612,8 +715,8 @@ async function preflightAdapterCollisions(repoDir, harnesses, exposure) {
612
715
  for (const h of harnesses) {
613
716
  for (const kind of KINDS) {
614
717
  for (const [name, source] of sources[kind]) {
615
- const adapterPath = join6(`.${h}`, kind, name);
616
- const target = join6(repoDir, adapterPath);
718
+ const adapterPath = join7(`.${h}`, kind, name);
719
+ const target = join7(repoDir, adapterPath);
617
720
  const stat = await lstat2(target).catch(() => null);
618
721
  if (!stat || stat.isSymbolicLink()) continue;
619
722
  if (exposure === "copy" && await entriesMatch(source, target)) continue;
@@ -680,7 +783,7 @@ async function runFreshInit(repoDir, flags, version) {
680
783
  async function runInit(repoDir, flags, version) {
681
784
  const state = await inspectManifest(repoDir);
682
785
  if (state.kind === "absent") return runFreshInit(repoDir, flags, version);
683
- const { runUpdate } = await import("./update-XOUXGD2Z.js");
786
+ const { runUpdate } = await import("./update-QTWNGAGX.js");
684
787
  return runUpdate(repoDir, flags, version, state);
685
788
  }
686
789
 
@@ -694,6 +797,8 @@ export {
694
797
  sha256,
695
798
  inspectManifest,
696
799
  migrateV1,
800
+ freeBackupPath,
801
+ writeBackup,
697
802
  ADAPTER_KINDS,
698
803
  canonicalAdapterEntries,
699
804
  entriesMatch,
package/dist/cli.js CHANGED
@@ -8,13 +8,13 @@ import {
8
8
  skillBaselinePath,
9
9
  starterSkillNameFromTarget,
10
10
  validateHarnesses
11
- } from "./chunk-GEIRG7UT.js";
11
+ } from "./chunk-5D5YO3E3.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { Command } from "commander";
15
15
  import { readFileSync } from "fs";
16
16
  import { fileURLToPath } from "url";
17
- import { dirname as dirname2, join as join2 } from "path";
17
+ import { dirname as dirname2, join as join3 } from "path";
18
18
 
19
19
  // src/verifier.ts
20
20
  import { access, lstat, readFile, readdir, readlink, stat } from "fs/promises";
@@ -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,9 +161,52 @@ async function runVerify(repoDir) {
150
161
  return v.failures.length ? 1 : 0;
151
162
  }
152
163
 
164
+ // src/commands/checklist.ts
165
+ import { spawn } from "child_process";
166
+ import { access as access2 } from "fs/promises";
167
+ import { join as join2 } from "path";
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];
171
+ return new Promise((resolve2, reject) => {
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" });
181
+ child.once("error", reject);
182
+ child.once("exit", (code) => resolve2(code ?? 1));
183
+ });
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
+ }
206
+
153
207
  // src/cli.ts
154
208
  var pkg = JSON.parse(
155
- readFileSync(join2(dirname2(fileURLToPath(import.meta.url)), "../package.json"), "utf8")
209
+ readFileSync(join3(dirname2(fileURLToPath(import.meta.url)), "../package.json"), "utf8")
156
210
  );
157
211
  var program = new Command("bearings").version(pkg.version);
158
212
  program.command("init").description("Scaffold the agent-friendly setup in the current repository").option("--harness <name...>", "claude and/or opencode").option("--copy", "copy instead of symlink").option("-y, --yes", "accept defaults, no prompts").action(async (o) => {
@@ -166,6 +220,9 @@ program.command("init").description("Scaffold the agent-friendly setup in the cu
166
220
  program.command("verify").description("Check the agent-friendly setup for drift and breakage").action(async () => {
167
221
  process.exitCode = await runVerify(process.cwd());
168
222
  });
223
+ program.command("checklist").description("Build and open the checklist previewer").action(async () => {
224
+ process.exitCode = await runChecklist(process.cwd());
225
+ });
169
226
  program.parseAsync().catch((error) => {
170
227
  console.error(error instanceof Error ? error.message : String(error));
171
228
  process.exit(1);