bearings 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.
@@ -4,6 +4,7 @@ import {
4
4
  SCAFFOLD,
5
5
  canonicalAdapterEntries,
6
6
  entriesMatch,
7
+ freeBackupPath,
7
8
  isStarterSkillTarget,
8
9
  migrateV1,
9
10
  renderUpdatePlan,
@@ -13,8 +14,9 @@ import {
13
14
  skillIncomingPath,
14
15
  starterSkillNameFromTarget,
15
16
  templatesDir,
16
- validateHarnesses
17
- } from "./chunk-KQPOIH7N.js";
17
+ validateHarnesses,
18
+ writeBackup
19
+ } from "./chunk-5D5YO3E3.js";
18
20
 
19
21
  // src/semver.ts
20
22
  function parseSemver(value) {
@@ -75,11 +77,6 @@ async function exists(path) {
75
77
  return false;
76
78
  }
77
79
  }
78
- async function freeBackupPath(repoDir, target) {
79
- let candidate = `${target}.bkp`;
80
- for (let i = 1; await exists(join(repoDir, candidate)); i++) candidate = `${target}.bkp.${i}`;
81
- return candidate;
82
- }
83
80
  function lineDiffStats(current, incoming) {
84
81
  const left = current.split("\n");
85
82
  const right = incoming.split("\n");
@@ -562,12 +559,12 @@ async function planAdapterActions(input) {
562
559
  for (const action of fileActions) {
563
560
  const root = parseAdapterRoot(actionPath(action));
564
561
  if (!root) continue;
565
- if (action.kind === "delete") {
562
+ if (action.kind === "delete" || action.kind === "untrack-missing") {
566
563
  if (!desiredRootState.has(root)) desiredRootState.set(root, false);
567
564
  } else {
568
565
  desiredRootState.set(root, true);
569
566
  }
570
- if (action.kind === "write" || action.kind === "merge" || action.kind === "delete") {
567
+ if (action.kind === "write" || action.kind === "merge" || action.kind === "delete" || action.kind === "untrack-missing") {
571
568
  affectedRootKeys.add(root);
572
569
  }
573
570
  }
@@ -638,12 +635,12 @@ async function capture(repoDir, txDir, relativePath, snapshots) {
638
635
  await rename(target, stored);
639
636
  snapshots.push({ target, stored });
640
637
  }
641
- async function captureToBackup(repoDir, relativePath, backupRelativePath, snapshots) {
642
- const target = join3(repoDir, relativePath);
643
- const stored = join3(repoDir, backupRelativePath);
644
- await mkdir(dirname(stored), { recursive: true });
645
- await rename(target, stored);
646
- snapshots.push({ target, stored });
638
+ async function captureToBackup(repoDir, txDir, relativePath, backupRelativePath, snapshots) {
639
+ await capture(repoDir, txDir, relativePath, snapshots);
640
+ const sourceSnapshot = snapshots.at(-1);
641
+ if (!sourceSnapshot?.stored) throw new Error(`Cannot back up missing path: ${relativePath}`);
642
+ await writeBackup(repoDir, relativePath, await readFile3(sourceSnapshot.stored), { path: backupRelativePath });
643
+ snapshots.push({ target: join3(repoDir, backupRelativePath) });
647
644
  }
648
645
  async function ensureDir(repoDir, absDir, createdDirs) {
649
646
  const rel = relative(repoDir, absDir);
@@ -664,10 +661,6 @@ async function writeContent(repoDir, txDir, relativePath, content, snapshots, cr
664
661
  await ensureDir(repoDir, dirname(target), createdDirs);
665
662
  await writeFile(target, content);
666
663
  }
667
- async function writeCopy(repoDir, txDir, fromRelativePath, toRelativePath, snapshots, createdDirs) {
668
- const content = await readFile3(join3(repoDir, fromRelativePath));
669
- await writeContent(repoDir, txDir, toRelativePath, content, snapshots, createdDirs);
670
- }
671
664
  async function restore(snapshots, hooks, txDir) {
672
665
  for (const snapshot of [...snapshots].reverse()) {
673
666
  try {
@@ -693,6 +686,25 @@ async function removeEmptyDirs(createdDirs) {
693
686
  }
694
687
  }
695
688
  }
689
+ async function removeEmptyCanonicalParents(repoDir, paths) {
690
+ const skillsRoot = join3(repoDir, ".agents", "skills");
691
+ const candidates = /* @__PURE__ */ new Set();
692
+ for (const path of paths) {
693
+ let current = dirname(join3(repoDir, path));
694
+ while (current.startsWith(`${skillsRoot}${sep}`)) {
695
+ candidates.add(current);
696
+ current = dirname(current);
697
+ }
698
+ }
699
+ for (const dir of [...candidates].sort((a, b) => b.length - a.length)) {
700
+ try {
701
+ await rmdir(dir);
702
+ } catch (error) {
703
+ const code = error.code;
704
+ if (code !== "ENOENT" && code !== "ENOTEMPTY") throw error;
705
+ }
706
+ }
707
+ }
696
708
  async function applyUpdate(repoDir, plan, hooks = {}) {
697
709
  const txDir = join3(repoDir, ".agents", `.bearings-txn-${randomUUID()}`);
698
710
  const snapshots = [];
@@ -720,11 +732,12 @@ async function applyUpdate(repoDir, plan, hooks = {}) {
720
732
  }
721
733
  await afterOp();
722
734
  } else if (action.kind === "merge") {
723
- await captureToBackup(repoDir, action.path, action.backup, snapshots);
735
+ await captureToBackup(repoDir, txDir, action.path, action.backup, snapshots);
724
736
  await writeFile(abs(action.path), action.content);
725
737
  await afterOp();
726
738
  } else if (action.kind === "skill-handoff") {
727
- await writeCopy(repoDir, txDir, action.path, action.backup, snapshots, createdDirs);
739
+ await writeBackup(repoDir, action.path, await readFile3(abs(action.path)), { path: action.backup });
740
+ snapshots.push({ target: abs(action.backup) });
728
741
  await writeContent(repoDir, txDir, action.incomingPath, action.content, snapshots, createdDirs);
729
742
  await afterOp();
730
743
  } else if (action.kind === "delete") {
@@ -732,6 +745,27 @@ async function applyUpdate(repoDir, plan, hooks = {}) {
732
745
  await afterOp();
733
746
  }
734
747
  }
748
+ await removeEmptyCanonicalParents(
749
+ repoDir,
750
+ plan.files.filter((action) => action.kind === "delete").map((action) => action.path)
751
+ );
752
+ for (const action of plan.migrations ?? []) {
753
+ if (action.kind === "migrate-testplan") {
754
+ if (action.backup) await captureToBackup(repoDir, txDir, action.target, action.backup, snapshots);
755
+ else await capture(repoDir, txDir, action.target, snapshots);
756
+ await capture(repoDir, txDir, action.source, snapshots);
757
+ await ensureDir(repoDir, dirname(abs(action.target)), createdDirs);
758
+ await writeFile(abs(action.target), action.content);
759
+ await afterOp();
760
+ } else if (action.kind === "preserve-invalid-testplan") {
761
+ await captureToBackup(repoDir, txDir, action.source, action.backup, snapshots);
762
+ await afterOp();
763
+ } else {
764
+ const entries = await readdir2(abs(action.path));
765
+ if (entries.length === 0) await capture(repoDir, txDir, action.path, snapshots);
766
+ await afterOp();
767
+ }
768
+ }
735
769
  for (const action of plan.adapters) {
736
770
  if (action.kind === "write-symlink") {
737
771
  await capture(repoDir, txDir, action.path, snapshots);
@@ -766,6 +800,151 @@ async function applyUpdate(repoDir, plan, hooks = {}) {
766
800
  }
767
801
  }
768
802
 
803
+ // src/update/testplan-migration.ts
804
+ import { access as access2, readFile as readFile4, readdir as readdir3 } from "fs/promises";
805
+ import { join as join4 } from "path";
806
+ var LEGACY_NAME = /^(.+)\.testplan\.json$/;
807
+ var SCOPE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
808
+ async function exists2(path) {
809
+ return access2(path).then(() => true, () => false);
810
+ }
811
+ function isObject(value) {
812
+ return value !== null && typeof value === "object" && !Array.isArray(value);
813
+ }
814
+ function nonEmptyString(value) {
815
+ return typeof value === "string" && value.trim().length > 0;
816
+ }
817
+ function optionalString(value) {
818
+ return value === void 0 || typeof value === "string";
819
+ }
820
+ function validPairs(value) {
821
+ return value === void 0 || Array.isArray(value) && value.every((pair) => Array.isArray(pair) && pair.length === 2 && pair.every((part) => typeof part === "string"));
822
+ }
823
+ function hasOnlyKeys(value, keys) {
824
+ const allowed = new Set(keys);
825
+ return Object.keys(value).every((key) => allowed.has(key));
826
+ }
827
+ function isValidLegacyPlan(value) {
828
+ if (!isObject(value) || !nonEmptyString(value.scope) || !SCOPE.test(value.scope)) return false;
829
+ if (!hasOnlyKeys(value, ["scope", "title", "subtitle", "intro", "callout", "conventions", "sections"])) return false;
830
+ if (!nonEmptyString(value.title)) return false;
831
+ if (!["subtitle", "intro", "callout", "conventions"].every((field) => optionalString(value[field]))) return false;
832
+ if (!Array.isArray(value.sections) || value.sections.length === 0) return false;
833
+ const ids = /* @__PURE__ */ new Set();
834
+ for (const section of value.sections) {
835
+ if (!isObject(section) || !nonEmptyString(section.n) || !nonEmptyString(section.title)) return false;
836
+ if (!hasOnlyKeys(section, ["n", "title", "kind", "intro", "tests"])) return false;
837
+ if (!["checklist", "info"].includes(section.kind) || !optionalString(section.intro)) return false;
838
+ if (!Array.isArray(section.tests) || section.tests.length === 0) return false;
839
+ for (const test of section.tests) {
840
+ if (!isObject(test) || !nonEmptyString(test.id) || !nonEmptyString(test.title) || ids.has(test.id)) return false;
841
+ if (!hasOnlyKeys(test, ["id", "title", "note", "expect", "optional", "code", "subs"])) return false;
842
+ ids.add(test.id);
843
+ if (!optionalString(test.note) || !optionalString(test.expect)) return false;
844
+ if (test.optional !== void 0 && typeof test.optional !== "boolean") return false;
845
+ if (!validPairs(test.code) || !validPairs(test.subs)) return false;
846
+ for (const pair of test.subs ?? []) {
847
+ if (!nonEmptyString(pair[0]) || ids.has(pair[0])) return false;
848
+ ids.add(pair[0]);
849
+ }
850
+ }
851
+ }
852
+ return true;
853
+ }
854
+ function convertLegacyPlan(plan) {
855
+ const checklist = {
856
+ title: plan.title,
857
+ summary: nonEmptyString(plan.subtitle) ? plan.subtitle : plan.title,
858
+ ...plan.intro !== void 0 ? { intro: plan.intro } : {},
859
+ ...plan.callout !== void 0 ? { callout: plan.callout } : {},
860
+ ...plan.conventions !== void 0 ? { conventions: plan.conventions } : {},
861
+ sections: plan.sections.map((section) => ({
862
+ number: section.n,
863
+ title: section.title,
864
+ kind: section.kind,
865
+ ...section.intro !== void 0 ? { intro: section.intro } : {},
866
+ items: section.tests.map((test) => ({
867
+ id: test.id,
868
+ title: test.title,
869
+ ...test.note !== void 0 ? { instruction: test.note } : {},
870
+ ...test.expect !== void 0 ? { expectedResult: test.expect } : {},
871
+ ...test.code !== void 0 ? {
872
+ codeBlocks: test.code.map(([language, content]) => ({
873
+ language: nonEmptyString(language) ? language : "text",
874
+ content
875
+ }))
876
+ } : {},
877
+ ...test.optional !== void 0 ? { optional: test.optional } : {},
878
+ ...test.subs !== void 0 ? {
879
+ subItems: test.subs.map(([id, title]) => ({
880
+ id,
881
+ title: nonEmptyString(title) ? title : id
882
+ }))
883
+ } : {}
884
+ }))
885
+ }))
886
+ };
887
+ return JSON.stringify(checklist, null, 2) + "\n";
888
+ }
889
+ async function planTestplanMigrations(repoDir) {
890
+ const dir = join4(repoDir, "testplans");
891
+ const entries = await readdir3(dir, { withFileTypes: true }).catch((error) => {
892
+ if (error.code === "ENOENT") return null;
893
+ throw error;
894
+ });
895
+ if (entries === null) return { actions: [], reconciliations: [] };
896
+ const actions = [];
897
+ const reconciliations = [];
898
+ let canRemoveDirectory = entries.length === 0;
899
+ for (const entry of entries) {
900
+ const match = entry.isFile() ? LEGACY_NAME.exec(entry.name) : null;
901
+ if (!match) continue;
902
+ const source = `testplans/${entry.name}`;
903
+ const slug = match[1];
904
+ const target = SCOPE.test(slug) ? `checklists/tests/${slug}.json` : void 0;
905
+ const raw = await readFile4(join4(repoDir, source), "utf8");
906
+ let parsed;
907
+ try {
908
+ parsed = JSON.parse(raw);
909
+ } catch {
910
+ parsed = void 0;
911
+ }
912
+ if (!target || !isValidLegacyPlan(parsed)) {
913
+ const backup = await freeBackupPath(repoDir, source);
914
+ actions.push({ kind: "preserve-invalid-testplan", source, backup });
915
+ reconciliations.push({
916
+ kind: "invalid-testplan",
917
+ source,
918
+ ...target ? { target } : {},
919
+ backup,
920
+ sourceHash: sha256(raw)
921
+ });
922
+ continue;
923
+ }
924
+ const content = convertLegacyPlan(parsed);
925
+ if (await exists2(join4(repoDir, target))) {
926
+ const backup = await freeBackupPath(repoDir, target);
927
+ const collided = await readFile4(join4(repoDir, target), "utf8");
928
+ actions.push({ kind: "migrate-testplan", source, target, content, backup });
929
+ reconciliations.push({
930
+ kind: "testplan-collision",
931
+ source,
932
+ target,
933
+ backup,
934
+ sourceHash: sha256(collided),
935
+ incomingHash: sha256(content)
936
+ });
937
+ } else {
938
+ actions.push({ kind: "migrate-testplan", source, target, content });
939
+ }
940
+ }
941
+ if (entries.length > 0) {
942
+ canRemoveDirectory = entries.every((entry) => entry.isFile() && LEGACY_NAME.test(entry.name) && actions.some((action) => action.kind === "migrate-testplan" && action.source === `testplans/${entry.name}`));
943
+ }
944
+ if (canRemoveDirectory) actions.push({ kind: "remove-empty-testplan-dir", path: "testplans" });
945
+ return { actions, reconciliations };
946
+ }
947
+
769
948
  // src/update/prompts.ts
770
949
  function cancelUpdate(p) {
771
950
  p.cancel("Init cancelled.");
@@ -836,7 +1015,7 @@ async function runUpdate(repoDir, flags, version, state) {
836
1015
  }
837
1016
  const migratedFromV1 = state.kind === "valid" && state.manifest.version === 1;
838
1017
  const current = state.kind === "valid" ? migratedFromV1 ? await migrateV1(repoDir, state.manifest) : state.manifest : null;
839
- if (!migratedFromV1 && (current?.setupPending || current?.files.some((file) => file.reconciliations?.length))) {
1018
+ if (!migratedFromV1 && (current?.setupPending || current?.migrationReconciliations?.length || current?.files.some((file) => file.reconciliations?.length))) {
840
1019
  throw new Error("Finish the pending /setup-repo handoff before running bearings init again.");
841
1020
  }
842
1021
  if (current && compareSemver(version, current.bearingsVersion) < 0) {
@@ -876,6 +1055,7 @@ async function runUpdate(repoDir, flags, version, state) {
876
1055
  else if (action.kind === "removed-conflict") decisions.push(await chooseRemovedConflict(p, action));
877
1056
  }
878
1057
  const resolved = await resolveFilePlan(repoDir, draft, decisions);
1058
+ const migrationPlan = await planTestplanMigrations(repoDir);
879
1059
  const adapterActions = await planAdapterActions({
880
1060
  repoDir,
881
1061
  currentHarnesses,
@@ -886,13 +1066,13 @@ async function runUpdate(repoDir, flags, version, state) {
886
1066
  });
887
1067
  const harnessesUnchanged = current ? current.harnesses.length === harnesses.length && current.harnesses.every((h) => harnesses.includes(h)) : false;
888
1068
  const exposureUnchanged = current ? current.exposure === exposure : false;
889
- const noArtifactChanges = resolved.actions.every((a) => a.kind === "keep") && adapterActions.length === 0;
1069
+ const noArtifactChanges = resolved.actions.every((a) => a.kind === "keep") && migrationPlan.actions.length === 0 && adapterActions.length === 0;
890
1070
  const versionUnchanged = !!current && version === current.bearingsVersion;
891
1071
  if (!reconstruction && !migratedFromV1 && noArtifactChanges && versionUnchanged && harnessesUnchanged && exposureUnchanged) {
892
1072
  return { manifest: current, report: "bearings init: already up to date." };
893
1073
  }
894
1074
  const migratedAbsorbedBackup = migratedFromV1 && (current?.files.some((file) => file.reconciliations?.length) ?? false);
895
- const setupRequired = resolved.setupRequired || adapterActions.length > 0 || migratedAbsorbedBackup;
1075
+ const setupRequired = resolved.setupRequired || migrationPlan.actions.length > 0 || adapterActions.length > 0 || migratedAbsorbedBackup;
896
1076
  const finalManifest = {
897
1077
  version: 2,
898
1078
  bearingsVersion: version,
@@ -905,10 +1085,12 @@ async function runUpdate(repoDir, flags, version, state) {
905
1085
  toVersion: version
906
1086
  }
907
1087
  } : {},
1088
+ ...migrationPlan.reconciliations.length > 0 ? { migrationReconciliations: [...migrationPlan.reconciliations] } : {},
908
1089
  files: [...resolved.files]
909
1090
  };
910
1091
  const summaryInput = {
911
1092
  actions: resolved.actions,
1093
+ migrationActions: migrationPlan.actions,
912
1094
  adapterActions,
913
1095
  fromVersion: current?.bearingsVersion,
914
1096
  toVersion: version,
@@ -917,7 +1099,12 @@ async function runUpdate(repoDir, flags, version, state) {
917
1099
  reconstruction
918
1100
  };
919
1101
  await confirmUpdatePlan(p, renderUpdatePlan(summaryInput));
920
- await applyUpdate(repoDir, { files: resolved.actions, adapters: adapterActions, manifest: finalManifest });
1102
+ await applyUpdate(repoDir, {
1103
+ files: resolved.actions,
1104
+ migrations: migrationPlan.actions,
1105
+ adapters: adapterActions,
1106
+ manifest: finalManifest
1107
+ });
921
1108
  return { manifest: finalManifest, report: renderUpdateReport({ ...summaryInput, setupRequired }) };
922
1109
  }
923
1110
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bearings",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Give your AI agents their bearings — scaffold an agent-friendly setup in any repository.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,26 +11,29 @@ answer per question — and explore the code before asking anything the code
11
11
  can answer.
12
12
 
13
13
  This is the only operation the temporary `## Setup Required` gate in
14
- `AGENTS.md` permits. Do not remove the gate until step 11 below.
14
+ `AGENTS.md` permits. Do not remove the gate until step 12 below.
15
15
 
16
16
  ## Post-update reconciliation
17
17
 
18
18
  When `.agents/bearings.json` is manifest v2 and has `setupPending`:
19
19
 
20
- 1. Read every file record's ordered `reconciliations`.
21
- 2. For each non-`skill-update` reconciliation, read its Backup File and current target, summarize
20
+ 1. Read every file record's ordered `reconciliations` and every top-level
21
+ `migrationReconciliations` record.
22
+ 2. Resolve every `migrationReconciliations` record through **Migrated checklist
23
+ repairs** below.
24
+ 3. For each non-`skill-update` reconciliation, read its Backup File and current target, summarize
22
25
  the differences, and ask which backed-up changes to apply to the target.
23
- 3. Apply the developer's choice directly to the target regardless of owner,
26
+ 4. Apply the developer's choice directly to the target regardless of owner,
24
27
  delete the resolved Backup File, and remove its reconciliation record.
25
- 4. A `skippedTemplate` records a declined template revision.
28
+ 5. A `skippedTemplate` records a declined template revision.
26
29
  Do not apply the declined template — normal project maintenance may
27
30
  still update that file.
28
- 5. Run only setup steps supported by current drift; do not reset completed
31
+ 6. Run only setup steps supported by current drift; do not reset completed
29
32
  project tailoring.
30
- 6. Run `bearings verify`. Resolve every failure and warning except the
33
+ 7. Run `bearings verify`. Resolve every failure and warning except the
31
34
  expected `setup-pending` warning, then remove `setupPending` from the
32
35
  manifest.
33
- 7. Run `bearings verify` again and finish only at zero failures and warnings.
36
+ 8. Run `bearings verify` again and finish only at zero failures and warnings.
34
37
 
35
38
  ### Starter skill updates (`skill-update`)
36
39
 
@@ -61,6 +64,52 @@ For each file record with a `skill-update` reconciliation:
61
64
  new content hash.
62
65
  5. Delete backup, incoming file, and the reconciliation entry.
63
66
 
67
+ ### Migrated checklist repairs (`migrationReconciliations`)
68
+
69
+ Resolve each record through the generic checklist workflow. Invoke the
70
+ `checklist` skill directly, or delegate one record to a subagent that loads and
71
+ follows `.agents/skills/checklist/SKILL.md`.
72
+
73
+ 1. For `testplan-collision`, read the current `target` and the pre-existing
74
+ checklist in `backup`. Summarize their authored content and ask the developer
75
+ whether to keep either version or combine them. Write the choice to `target`.
76
+ 2. For `invalid-testplan`, read `backup`, explain each contract problem, and
77
+ repair its useful authored content into `target`. When the record has no
78
+ `target`, ask the developer to choose a safe
79
+ `checklists/<category...>/<slug>.json` path first. Do not discard content
80
+ that cannot be repaired without the developer's decision.
81
+ 3. Follow the checklist skill's schema, validation, and full-library build
82
+ steps for the resolved target. Continue only after validation and build both
83
+ pass.
84
+ 4. Delete the resolved `backup` and remove its
85
+ `migrationReconciliations` record. Remove the top-level property when no
86
+ records remain.
87
+
88
+ ## Checklist opener
89
+
90
+ Add or reuse one project-native command that runs `npx bearings checklist` (or
91
+ the repository's established local bearings invocation). This command builds
92
+ the checklist payload and opens the static previewer.
93
+
94
+ 1. Search existing automation and documentation for an existing equivalent
95
+ runner command that already builds and opens the checklist previewer. Reuse
96
+ it and report its exact invocation without editing its runner.
97
+ 2. Otherwise, select the first existing safe automation home in this exact
98
+ priority: `justfile`, `Makefile`, `Taskfile.yml`, `mise.toml`, then
99
+ `package.json`.
100
+ 3. Before editing an existing automation file, show the proposed command and
101
+ ask the developer for approval. Edit it only after approval. If approval is
102
+ declined, leave it unchanged and continue through the priority order.
103
+ 4. If no existing home is safe and approved, ask the developer to choose
104
+ between creating one missing automation file from the list above and using
105
+ the direct fallback. Create a missing file only after the developer selects
106
+ that strategy and file.
107
+ 5. When no automation file is selected, make no automation edit and report the
108
+ stack-agnostic direct build-and-open fallback: `npx bearings checklist`.
109
+ 6. Run the selected command, or the direct fallback, and confirm that it builds
110
+ successfully and opens the static previewer. Report its exact invocation for
111
+ future use.
112
+
64
113
  ## Required workflow
65
114
 
66
115
  1. Read `AGENTS.md` and `.agents/bearings.json`.
@@ -96,17 +145,18 @@ For each file record with a `skill-update` reconciliation:
96
145
  `lastTemplateHash` equals `hash`).
97
146
  - Write `.agents/.bearings-baseline/skills/<name>/SKILL.md` equal to the
98
147
  live file bytes.
99
- 7. Run the `/refresh-repo-map` workflow to initialize `docs/DOMAIN.md`,
100
- `docs/ARCHITECTURE.md`, `docs/CODEBASE_MAP.md`, and
101
- `docs/diagrams/c4-component.puml`.
102
- 8. Expose any newly created skills into each configured harness using the
103
- manifest's existing symlink/copy mode.
104
- 9. Run `bearings verify`. Fix every failure and every warning.
105
- 10. Confirm the run reports zero failures and zero warnings before
106
- proceeding.
107
- 11. Remove the `## Setup Required` section from `AGENTS.md` — only after
108
- step 10 confirms zero failures and zero warnings.
109
- 12. Run `bearings verify` again and report the completed setup to the
148
+ 7. Complete the **Checklist opener** workflow above.
149
+ 8. Run the `/refresh-repo-map` workflow to initialize `docs/DOMAIN.md`,
150
+ `docs/ARCHITECTURE.md`, `docs/CODEBASE_MAP.md`, and
151
+ `docs/diagrams/c4-component.puml`.
152
+ 9. Expose any newly created skills into each configured harness using the
153
+ manifest's existing symlink/copy mode.
154
+ 10. Run `bearings verify`. Fix every failure and every warning.
155
+ 11. Confirm the run reports zero failures and zero warnings before
156
+ proceeding.
157
+ 12. Remove the `## Setup Required` section from `AGENTS.md` — only after
158
+ step 11 confirms zero failures and zero warnings.
159
+ 13. Run `bearings verify` again and report the completed setup to the
110
160
  developer.
111
161
 
112
162
  ## Rules
@@ -118,8 +168,9 @@ For each file record with a `skill-update` reconciliation:
118
168
  - Do edit starter **skills** during setup (adapt + claim). After claim they
119
169
  are agent-owned; maintainers may edit them freely.
120
170
  - Manifest edits are limited to: skill `owner` / `hash` / `lastTemplateHash` /
121
- `skippedTemplate` / `reconciliations`, deleting resolved backups/incoming,
122
- and clearing `setupPending`.
171
+ `skippedTemplate` / `reconciliations`, resolving
172
+ `migrationReconciliations`, deleting resolved backups/incoming, and clearing
173
+ `setupPending`.
123
174
  - Do not add a skill registry row to `AGENTS.md` — native skill discovery
124
175
  replaces it.
125
176
  - Do not invent domain or technical constraints — every stated constraint
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: checklist
3
+ description: Create a generic executable checklist for human-run procedures. Use when work needs an operational checklist for setup, deployment, release, verification, maintenance, or another process a person must perform.
4
+ ---
5
+
6
+ # Checklist
7
+
8
+ Create one evidence-backed checklist for a human operator. The JSON contains
9
+ authored instructions; the static previewer owns execution state.
10
+
11
+ ## Workflow
12
+
13
+ 1. Read `.agents/skills/checklist/schema.reference.json` and inspect the
14
+ repository sources that define the requested procedure. Resolve every
15
+ mechanic, command, URL, prerequisite, expected result, and hazard from
16
+ running code, configuration, authoritative documentation, or developer
17
+ confirmation. Ask the developer about any required detail the evidence does
18
+ not establish.
19
+ 2. Choose a lowercase slug path under `checklists/`. The final segment is the
20
+ checklist slug and every preceding segment is its category. Use nested
21
+ categories when useful, for example `checklists/operations/servers/rotate-keys.json`
22
+ has category `operations/servers` and identity `operations/servers/rotate-keys`.
23
+ 3. Write exactly one JSON file at `checklists/<category...>/<slug>.json`.
24
+ Category identity and slug come only from the path; omit them from the JSON.
25
+ 4. Run
26
+ `node .agents/skills/checklist/scripts/validate.mjs checklists/<category...>/<slug>.json`.
27
+ Fix every reported violation until it exits zero and prints `OK`.
28
+ 5. From the repository root, run
29
+ `node .agents/skills/checklist/scripts/build.mjs`. Fix every reported
30
+ checklist-library error until the build exits zero.
31
+ 6. Open `.agents/skills/checklist/previewer/index.html` with the environment's
32
+ browser-opening tool. If opening fails, report the absolute file path so the
33
+ developer can open it. The command must finish; the static previewer needs
34
+ no server process.
35
+
36
+ ## Content Policy
37
+
38
+ - Organize sections in execution order and keep each checklist item to one
39
+ primary action or verification. Use `info` sections for concise context that
40
+ the operator does not mark as executed.
41
+ - Give every executable item exact, evidence-backed mechanics and an observable
42
+ `expectedResult`. Put commands in `codeBlocks`; distinguish literal values
43
+ from placeholders and state where each command runs.
44
+ - Use `links` for authoritative background material instead of expanding the
45
+ checklist into a manual. Include only verified, safe destination URLs and
46
+ clear labels.
47
+ - Put a `warning` before destructive, irreversible, privileged,
48
+ security-sensitive, externally visible, data-changing, or access-loss
49
+ actions. State the concrete hazard and any prerequisite that preserves
50
+ operator control.
51
+ - Add `failureGuidance` when an item can fail or block progress. Give an
52
+ evidence-backed next action such as stop, retry, diagnose, roll back, or
53
+ escalate, plus the observable recovered state when recovery applies.
54
+ - Refer to credentials through the repository's established secret mechanism
55
+ or documented location. Keep passwords, tokens, private keys, and other
56
+ secret values out of checklist content.
57
+ - Keep authored content only in the JSON. Statuses such as `not-started`,
58
+ `complete`, `failed`, `blocked`, and `not-applicable`, and all operator
59
+ progress, belong to browser-local previewer state.
60
+ - Treat absent evidence as an unresolved input. Ask for it or state a verified
61
+ limitation; do not invent mechanics, URLs, commands, credentials, expected
62
+ behavior, warnings, or recovery steps.
63
+
64
+ ## Completion
65
+
66
+ Finish only when exactly one authored checklist file exists for this request,
67
+ its mechanics and outcomes trace to evidence, its validator prints `OK`, the
68
+ full library build succeeds, and the static previewer opens or its absolute path
69
+ is reported after an opening failure.
@@ -0,0 +1,38 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'">
7
+ <title>Checklist library</title>
8
+ <link rel="stylesheet" href="./viewer.css">
9
+ </head>
10
+ <body>
11
+ <header class="masthead">
12
+ <div class="masthead-inner">
13
+ <p class="eyebrow">Bearings / local checklist library</p>
14
+ <h1 id="title">Choose a checklist</h1>
15
+ <p id="summary" class="summary">Browse the checklists generated from this repository.</p>
16
+ </div>
17
+ </header>
18
+ <div class="toolbar">
19
+ <div class="toolbar-inner">
20
+ <label class="picker-label" for="checklist-picker">Checklist</label>
21
+ <select id="checklist-picker"><option value="">Loading...</option></select>
22
+ <div class="progress" aria-live="polite">
23
+ <span id="progress-copy">0 / 0 resolved</span>
24
+ <span class="progress-track" aria-hidden="true"><span id="progress-fill"></span></span>
25
+ </div>
26
+ <button id="export-progress" type="button">Export</button>
27
+ <button id="import-progress" type="button">Import</button>
28
+ <input id="import-file" type="file" accept="application/json,.json" hidden>
29
+ <button id="reset-progress" type="button">Reset</button>
30
+ </div>
31
+ </div>
32
+ <p id="storage-warning" class="storage-warning" role="status" hidden></p>
33
+ <p id="status" class="status" role="status" aria-live="polite"></p>
34
+ <main id="content"></main>
35
+ <script src="./data.generated.js"></script>
36
+ <script src="./viewer.js"></script>
37
+ </body>
38
+ </html>