bearings 0.3.2 → 0.3.4

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/README.md CHANGED
@@ -50,7 +50,7 @@ docs/
50
50
  skills/*
51
51
  ```
52
52
 
53
- Every file carries an `owner` in the manifest. `bearings`-owned files (`CLAUDE.md`, the ADR template, commands, skills) are package-maintained — the incoming template is their canonical update source. `agent`-owned files (`AGENTS.md`, the three maps, the two indexes) are yours; bearings never overwrites them without an explicit per-file decision.
53
+ Every file carries an `owner` in the manifest. `bearings`-owned files (`CLAUDE.md`, the ADR template, and commands) are package-maintained — the incoming template is their canonical update source. Starter skills begin bearings-owned, then `/setup-repo` adapts and claims them as agent-owned. `agent`-owned files (`AGENTS.md`, the three maps, the two indexes, and claimed starter skills) are yours; bearings never overwrites them without an explicit per-file decision except for the starter-skill update handling below.
54
54
 
55
55
  `/setup-repo` creates `docs/diagrams/c4-component.puml` (via `/refresh-repo-map`) once the repo is tailored; it is not part of the generated scaffold or the manifest.
56
56
 
@@ -93,6 +93,9 @@ State-aware entry point. With no manifest it scaffolds; with a manifest it runs
93
93
  | File no longer shipped, unchanged locally | Removed |
94
94
  | File no longer shipped, changed locally | You choose **Remove** or **Keep and untrack** |
95
95
  | New scaffold path already occupied | Conflict, resolved the same way |
96
+ | Claimed starter skill unchanged locally, template changed | Replaced silently; its agent owner is retained and its baseline is refreshed |
97
+ | Claimed starter skill and template both changed | No CLI prompt: local content is preserved with backup, base, and incoming artifacts; `/setup-repo` guides the developer through a three-way resolution when the base exists |
98
+ | Legacy bearings-owned starter skill and template both changed, with no baseline | No CLI prompt and no auto-claim: local content is preserved with backup and incoming artifacts; `verify` warns about the missing base and `/setup-repo` offers Take new template, Keep local, or Freeform |
96
99
 
97
100
  Adapter changes (harness added/removed, symlink↔copy switch) are planned alongside. The full plan is shown and confirmed before any mutation, then applied as a single transaction — the manifest is written last, and any failure restores the exact prior state. A run that changes nothing reports `already up to date`.
98
101
 
@@ -111,9 +114,10 @@ Check the manifest and harness exposures for mechanical breakage. Exit code `0`
111
114
  | `broken-symlink` | fail | harness entry is a symlink whose target doesn't resolve |
112
115
  | `copy-drift` | fail | copy mode: harness file content differs from `.agents` source content |
113
116
  | `setup-pending` | warn | an update finished but `/setup-repo` hasn't run yet |
114
- | `unfilled-placeholder` | warn | a bearings-generated file still contains `<agent:` |
117
+ | `unfilled-placeholder` | warn | an `owner: agent` file still contains a complete single-line agent placeholder marker (bearings-owned files are not checked) |
118
+ | `missing-skill-baseline` | warn | an agent-owned starter skill or pending `skill-update` reconciliation has no skill baseline file |
115
119
  | `unreviewed-backup` | warn | manifest `backup` path still exists on disk |
116
- | `pending-reconciliation` | warn | a recorded backup from a collision or Merge is still unresolved |
120
+ | `pending-reconciliation` | warn | a reconciliation artifact, including a skill-update backup or incoming file, is still unresolved |
117
121
 
118
122
  ## Development
119
123
 
@@ -6,12 +6,25 @@ import { dirname, join } from "path";
6
6
  function templatesDir() {
7
7
  return join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
8
8
  }
9
- var SKILLS = [
9
+ var STARTER_SKILL_NAMES = [
10
10
  "commit-convention",
11
11
  "defer-work",
12
12
  "resurface-deferred-work",
13
13
  "recording-decisions"
14
14
  ];
15
+ var SKILLS = STARTER_SKILL_NAMES;
16
+ function starterSkillNameFromTarget(targetPath) {
17
+ return STARTER_SKILL_NAMES.find((skillName) => targetPath === `.agents/skills/${skillName}/SKILL.md`);
18
+ }
19
+ function isStarterSkillTarget(targetPath) {
20
+ return starterSkillNameFromTarget(targetPath) !== void 0;
21
+ }
22
+ function skillBaselinePath(skillName) {
23
+ return `.agents/.bearings-baseline/skills/${skillName}/SKILL.md`;
24
+ }
25
+ function skillIncomingPath(skillName) {
26
+ return `.agents/.bearings-incoming/skills/${skillName}/SKILL.md`;
27
+ }
15
28
  var SCAFFOLD = [
16
29
  { template: "AGENTS.md", target: "AGENTS.md", owner: "agent" },
17
30
  { template: "CLAUDE.md", target: "CLAUDE.md", owner: "bearings" },
@@ -48,7 +61,7 @@ var HASH = /^sha256:[0-9a-f]{64}$/;
48
61
  var HARNESSES = ["claude", "opencode"];
49
62
  var EXPOSURES = ["symlink", "copy"];
50
63
  var OWNERS = ["bearings", "agent"];
51
- var RECONCILIATION_REASONS = ["init-collision", "update-merge"];
64
+ var RECONCILIATION_REASONS = ["init-collision", "update-merge", "skill-update"];
52
65
  var SETUP_PENDING_KINDS = ["update", "reconstruction"];
53
66
  function fail(message) {
54
67
  throw new Error(message);
@@ -81,7 +94,17 @@ function validateReconciliation(value, context) {
81
94
  const { backup, reason, sourceHash, incomingTemplateVersion, incomingHash } = value;
82
95
  if (!isSafeManifestPath(backup)) fail(`${context}.backup is unsafe: ${String(backup)}`);
83
96
  if (typeof reason !== "string" || !RECONCILIATION_REASONS.includes(reason)) {
84
- fail(`${context}.reason must be "init-collision" or "update-merge"`);
97
+ fail(`${context}.reason must be "init-collision", "update-merge", or "skill-update"`);
98
+ }
99
+ if (reason === "skill-update") {
100
+ const { basePath, incomingPath, baseHash } = value;
101
+ if (!isSafeManifestPath(basePath)) fail(`${context}.basePath is unsafe: ${String(basePath)}`);
102
+ if (!isSafeManifestPath(incomingPath)) fail(`${context}.incomingPath is unsafe: ${String(incomingPath)}`);
103
+ if (!isHash(baseHash)) fail(`${context}.baseHash must match sha256 format`);
104
+ if (!isHash(sourceHash)) fail(`${context}.sourceHash must match sha256 format`);
105
+ if (!isNonEmptyString(incomingTemplateVersion)) fail(`${context}.incomingTemplateVersion must be a string`);
106
+ if (!isHash(incomingHash)) fail(`${context}.incomingHash must match sha256 format`);
107
+ return { backup, reason, basePath, incomingPath, baseHash, sourceHash, incomingTemplateVersion, incomingHash };
85
108
  }
86
109
  if (!isHash(sourceHash)) fail(`${context}.sourceHash must match sha256 format`);
87
110
  if (!isNonEmptyString(incomingTemplateVersion)) fail(`${context}.incomingTemplateVersion must be a string`);
@@ -116,12 +139,15 @@ function validateFileV1(value, index) {
116
139
  function validateFileV2(value, index) {
117
140
  const context = `files[${index}]`;
118
141
  if (!isPlainObject(value)) fail(`${context} must be an object`);
119
- const { path, template, templateVersion, hash, owner, retired, skippedTemplate, reconciliations } = value;
142
+ const { path, template, templateVersion, hash, owner, lastTemplateHash, retired, skippedTemplate, reconciliations } = value;
120
143
  if (!isSafeManifestPath(path)) fail(`${context}.path is unsafe: ${String(path)}`);
121
144
  if (!isNonEmptyString(template)) fail(`${context}.template must be a string`);
122
145
  if (!isNonEmptyString(templateVersion)) fail(`${context}.templateVersion must be a string`);
123
146
  if (!isHash(hash)) fail(`${context}.hash must match sha256 format`);
124
147
  if (!isOwner(owner)) fail(`${context}.owner must be "bearings" or "agent"`);
148
+ if (lastTemplateHash !== void 0 && !isHash(lastTemplateHash)) {
149
+ fail(`${context}.lastTemplateHash must match sha256 format`);
150
+ }
125
151
  if (retired !== void 0 && retired !== true) fail(`${context}.retired must be true when present`);
126
152
  let normalizedSkippedTemplate;
127
153
  if (skippedTemplate !== void 0) {
@@ -145,6 +171,7 @@ function validateFileV2(value, index) {
145
171
  templateVersion,
146
172
  hash,
147
173
  owner,
174
+ ...lastTemplateHash !== void 0 ? { lastTemplateHash } : {},
148
175
  ...retired === true ? { retired: true } : {},
149
176
  ...normalizedSkippedTemplate ? { skippedTemplate: normalizedSkippedTemplate } : {},
150
177
  ...normalizedReconciliations ? { reconciliations: normalizedReconciliations } : {}
@@ -270,6 +297,7 @@ function categorizeActions(actions) {
270
297
  restored: [],
271
298
  replaced: [],
272
299
  merged: [],
300
+ skillHandoffs: [],
273
301
  skipped: [],
274
302
  removed: [],
275
303
  keptUntracked: []
@@ -281,6 +309,8 @@ function categorizeActions(actions) {
281
309
  else result.replaced.push(action.path);
282
310
  } else if (action.kind === "merge") {
283
311
  result.merged.push(`${action.path} -> ${action.backup}`);
312
+ } else if (action.kind === "skill-handoff") {
313
+ result.skillHandoffs.push(action.path);
284
314
  } else if (action.kind === "skip") {
285
315
  result.skipped.push(action.path);
286
316
  } else if (action.kind === "delete") {
@@ -302,6 +332,7 @@ function renderUpdatePlan(input) {
302
332
  pushSection(lines, "Restored", categories.restored);
303
333
  pushSection(lines, "Replaced", categories.replaced);
304
334
  pushSection(lines, "Merged", categories.merged);
335
+ pushSection(lines, "Skill handoff \u2192 /setup-repo", categories.skillHandoffs);
305
336
  pushSection(lines, "Skipped", categories.skipped);
306
337
  pushSection(lines, "Removed", categories.removed);
307
338
  pushSection(lines, "Kept and untracked", categories.keptUntracked);
@@ -325,6 +356,7 @@ function renderUpdateReport(input) {
325
356
  pushSection(lines, "Restored", categories.restored);
326
357
  pushSection(lines, "Replaced", categories.replaced);
327
358
  pushSection(lines, "Merged", categories.merged);
359
+ pushSection(lines, "Skill handoff \u2192 /setup-repo", categories.skillHandoffs);
328
360
  pushSection(lines, "Skipped", categories.skipped);
329
361
  pushSection(lines, "Removed", categories.removed);
330
362
  pushSection(lines, "Kept and untracked", categories.keptUntracked);
@@ -421,6 +453,14 @@ async function generate(repoDir, bearingsVersion) {
421
453
  if (await exists2(abs)) {
422
454
  currentContent = await readFile2(abs, "utf8");
423
455
  if (priorEntry && sha256(currentContent) === priorEntry.hash) {
456
+ const skillName2 = starterSkillNameFromTarget(entry.target);
457
+ if (skillName2) {
458
+ const baseline = join4(repoDir, skillBaselinePath(skillName2));
459
+ if (!await exists2(baseline)) {
460
+ await mkdir2(dirname2(baseline), { recursive: true });
461
+ await writeFile2(baseline, currentContent);
462
+ }
463
+ }
424
464
  result.skippedUnchanged.push(entry.target);
425
465
  result.files.push(priorEntry);
426
466
  continue;
@@ -433,6 +473,12 @@ async function generate(repoDir, bearingsVersion) {
433
473
  await writeFile2(abs, templateContent);
434
474
  result.written.push(entry.target);
435
475
  const incomingHash = sha256(templateContent);
476
+ const skillName = starterSkillNameFromTarget(entry.target);
477
+ if (skillName) {
478
+ const baseline = join4(repoDir, skillBaselinePath(skillName));
479
+ await mkdir2(dirname2(baseline), { recursive: true });
480
+ await writeFile2(baseline, templateContent);
481
+ }
436
482
  const reconciliation = backup && currentContent !== void 0 ? {
437
483
  backup,
438
484
  reason: "init-collision",
@@ -446,6 +492,7 @@ async function generate(repoDir, bearingsVersion) {
446
492
  templateVersion: bearingsVersion,
447
493
  hash: incomingHash,
448
494
  owner: entry.owner,
495
+ ...isStarterSkillTarget(entry.target) ? { lastTemplateHash: incomingHash } : {},
449
496
  ...reconciliation ? { reconciliations: [reconciliation] } : {}
450
497
  });
451
498
  }
@@ -597,7 +644,7 @@ async function runFreshInit(repoDir, flags, version) {
597
644
  if (p.isCancel(selectedHarnesses)) {
598
645
  cancelInit(p);
599
646
  }
600
- harnesses = validateHarnesses(selectedHarnesses);
647
+ harnesses = validateHarnesses(selectedHarnesses) ?? [];
601
648
  }
602
649
  if (!exposure && s.symlinksSupported) {
603
650
  const selectedExposure = await p.select({
@@ -633,12 +680,16 @@ async function runFreshInit(repoDir, flags, version) {
633
680
  async function runInit(repoDir, flags, version) {
634
681
  const state = await inspectManifest(repoDir);
635
682
  if (state.kind === "absent") return runFreshInit(repoDir, flags, version);
636
- const { runUpdate } = await import("./update-5F7TZEUH.js");
683
+ const { runUpdate } = await import("./update-XOUXGD2Z.js");
637
684
  return runUpdate(repoDir, flags, version, state);
638
685
  }
639
686
 
640
687
  export {
641
688
  templatesDir,
689
+ starterSkillNameFromTarget,
690
+ isStarterSkillTarget,
691
+ skillBaselinePath,
692
+ skillIncomingPath,
642
693
  SCAFFOLD,
643
694
  sha256,
644
695
  inspectManifest,
package/dist/cli.js CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  inspectManifest,
4
+ isStarterSkillTarget,
4
5
  renderVerifyReport,
5
6
  runInit,
6
7
  sha256,
8
+ skillBaselinePath,
9
+ starterSkillNameFromTarget,
7
10
  validateHarnesses
8
- } from "./chunk-VRWQ3OWC.js";
11
+ } from "./chunk-GEIRG7UT.js";
9
12
 
10
13
  // src/cli.ts
11
14
  import { Command } from "commander";
@@ -17,6 +20,7 @@ import { dirname as dirname2, join as join2 } from "path";
17
20
  import { access, lstat, readFile, readdir, readlink, stat } from "fs/promises";
18
21
  import { dirname, join, resolve } from "path";
19
22
  var KINDS = ["skills", "commands"];
23
+ var PLACEHOLDER = /<agent:[^>\n]*>/;
20
24
  async function exists(p) {
21
25
  try {
22
26
  await access(p);
@@ -28,6 +32,12 @@ async function exists(p) {
28
32
  async function verify(repoDir) {
29
33
  const failures = [];
30
34
  const warnings = [];
35
+ const missingSkillBaselines = /* @__PURE__ */ new Set();
36
+ async function warnMissingSkillBaseline(baseline) {
37
+ if (missingSkillBaselines.has(baseline) || await exists(join(repoDir, baseline))) return;
38
+ missingSkillBaselines.add(baseline);
39
+ warnings.push({ code: "missing-skill-baseline", path: baseline, message: "run /setup-repo to repair skill baseline" });
40
+ }
31
41
  const state = await inspectManifest(repoDir);
32
42
  if (state.kind === "absent") {
33
43
  return { failures: [{ code: "no-manifest", path: ".agents/bearings.json", message: "run bearings init first" }], warnings };
@@ -47,19 +57,27 @@ async function verify(repoDir) {
47
57
  failures.push({ code: "missing-file", path: f.path, message: "managed file deleted" });
48
58
  } else {
49
59
  const content = await readFile(abs, "utf8");
50
- if (f.owner === "agent" && content.includes("<agent:")) {
60
+ if (f.owner === "agent" && PLACEHOLDER.test(content)) {
51
61
  warnings.push({ code: "unfilled-placeholder", path: f.path, message: "run /setup-repo to fill" });
52
62
  }
53
63
  }
54
64
  }
65
+ if (!retired && f.owner === "agent" && isStarterSkillTarget(f.path)) {
66
+ const skillName = starterSkillNameFromTarget(f.path);
67
+ await warnMissingSkillBaseline(skillBaselinePath(skillName));
68
+ }
55
69
  if ("backup" in f && f.backup && await exists(join(repoDir, f.backup))) {
56
70
  warnings.push({ code: "unreviewed-backup", path: f.backup, message: "review during /setup-repo, then delete" });
57
71
  }
58
72
  if ("reconciliations" in f && f.reconciliations) {
59
73
  for (const r of f.reconciliations) {
74
+ if (r.reason === "skill-update") await warnMissingSkillBaseline(r.basePath);
60
75
  if (await exists(join(repoDir, r.backup))) {
61
76
  warnings.push({ code: "pending-reconciliation", path: r.backup, message: "review during /setup-repo, then delete" });
62
77
  }
78
+ if (r.reason === "skill-update" && await exists(join(repoDir, r.incomingPath))) {
79
+ warnings.push({ code: "pending-reconciliation", path: r.incomingPath, message: "review during /setup-repo, then delete" });
80
+ }
63
81
  }
64
82
  }
65
83
  }
@@ -4,13 +4,17 @@ import {
4
4
  SCAFFOLD,
5
5
  canonicalAdapterEntries,
6
6
  entriesMatch,
7
+ isStarterSkillTarget,
7
8
  migrateV1,
8
9
  renderUpdatePlan,
9
10
  renderUpdateReport,
10
11
  sha256,
12
+ skillBaselinePath,
13
+ skillIncomingPath,
14
+ starterSkillNameFromTarget,
11
15
  templatesDir,
12
16
  validateHarnesses
13
- } from "./chunk-VRWQ3OWC.js";
17
+ } from "./chunk-GEIRG7UT.js";
14
18
 
15
19
  // src/semver.ts
16
20
  function parseSemver(value) {
@@ -102,6 +106,80 @@ async function planFiles(input) {
102
106
  const currentHash = currentContent !== void 0 ? sha256(currentContent) : void 0;
103
107
  const templateContent = await readFile(join(source.templatesDir, entry.template), "utf8");
104
108
  const incomingHash = sha256(templateContent);
109
+ const prevRecord = previous?.files.find((file) => file.path === entry.target);
110
+ if (isStarterSkillTarget(entry.target)) {
111
+ const prevOwner = prevRecord?.owner ?? entry.owner;
112
+ const lastTemplateHash = prevRecord?.lastTemplateHash ?? prevRecord?.hash;
113
+ const incomingRecordBase = {
114
+ path: entry.target,
115
+ template: entry.template,
116
+ templateVersion: source.bearingsVersion,
117
+ hash: incomingHash,
118
+ owner: prevOwner === "agent" ? "agent" : entry.owner,
119
+ lastTemplateHash: incomingHash
120
+ };
121
+ if (!currentExists) {
122
+ actions.push({
123
+ kind: "write",
124
+ reason: prevRecord ? "restore" : "add",
125
+ path: entry.target,
126
+ content: templateContent,
127
+ record: incomingRecordBase
128
+ });
129
+ continue;
130
+ }
131
+ if (!prevRecord && reconstruction && currentHash === incomingHash) {
132
+ actions.push({ kind: "keep", record: incomingRecordBase });
133
+ continue;
134
+ }
135
+ if (!prevRecord) {
136
+ actions.push({
137
+ kind: "conflict",
138
+ reason: "new-path",
139
+ path: entry.target,
140
+ content: templateContent,
141
+ currentHash,
142
+ record: incomingRecordBase,
143
+ stats: lineDiffStats(currentContent, templateContent),
144
+ canSkip: entry.target !== SETUP_COMMAND_PATH
145
+ });
146
+ continue;
147
+ }
148
+ if (prevRecord.skippedTemplate?.hash === incomingHash || lastTemplateHash === incomingHash) {
149
+ actions.push({ kind: "keep", record: prevRecord });
150
+ continue;
151
+ }
152
+ if (currentHash === prevRecord.hash) {
153
+ actions.push({
154
+ kind: "write",
155
+ reason: "replace",
156
+ path: entry.target,
157
+ content: templateContent,
158
+ record: {
159
+ ...incomingRecordBase,
160
+ ...prevRecord.reconciliations ? { reconciliations: prevRecord.reconciliations } : {}
161
+ }
162
+ });
163
+ continue;
164
+ }
165
+ actions.push({
166
+ kind: "skill-handoff",
167
+ path: entry.target,
168
+ content: templateContent,
169
+ currentHash,
170
+ previous: prevRecord,
171
+ baseHash: prevRecord.hash,
172
+ record: {
173
+ ...prevRecord,
174
+ template: entry.template,
175
+ templateVersion: source.bearingsVersion,
176
+ owner: prevRecord.owner === "agent" ? "agent" : prevRecord.owner,
177
+ lastTemplateHash
178
+ },
179
+ stats: lineDiffStats(currentContent, templateContent)
180
+ });
181
+ continue;
182
+ }
105
183
  const incomingRecord = {
106
184
  path: entry.target,
107
185
  template: entry.template,
@@ -109,7 +187,6 @@ async function planFiles(input) {
109
187
  hash: incomingHash,
110
188
  owner: entry.owner
111
189
  };
112
- const prevRecord = previous?.files.find((file) => file.path === entry.target);
113
190
  const canSkip = entry.target !== SETUP_COMMAND_PATH;
114
191
  if (!currentExists) {
115
192
  actions.push({
@@ -277,6 +354,33 @@ async function resolveFilePlan(repoDir, draft, decisions) {
277
354
  }
278
355
  continue;
279
356
  }
357
+ if (action.kind === "skill-handoff") {
358
+ const skillName = starterSkillNameFromTarget(action.path);
359
+ const backup = await freeBackupPath(repoDir, action.path);
360
+ const basePath = skillBaselinePath(skillName);
361
+ const incomingPath = skillIncomingPath(skillName);
362
+ const reconciliation = {
363
+ reason: "skill-update",
364
+ backup,
365
+ basePath,
366
+ incomingPath,
367
+ baseHash: action.baseHash,
368
+ sourceHash: action.currentHash,
369
+ incomingTemplateVersion: action.record.templateVersion,
370
+ incomingHash: sha256(action.content)
371
+ };
372
+ const record2 = {
373
+ ...action.record,
374
+ owner: action.previous.owner === "agent" ? "agent" : action.record.owner,
375
+ hash: action.previous.hash,
376
+ lastTemplateHash: action.previous.lastTemplateHash ?? action.previous.hash,
377
+ reconciliations: [...action.previous.reconciliations ?? [], reconciliation]
378
+ };
379
+ actions.push({ kind: "skill-handoff", path: action.path, content: action.content, backup, basePath, incomingPath, record: record2 });
380
+ files.push(record2);
381
+ setupRequired = true;
382
+ continue;
383
+ }
280
384
  if (action.kind === "conflict") {
281
385
  const decision2 = decisionsByPath.get(action.path);
282
386
  if (!decision2) fail(`Missing decision for path: ${action.path}`);
@@ -519,7 +623,7 @@ async function planAdapterActions(input) {
519
623
 
520
624
  // src/update/transaction.ts
521
625
  import { randomUUID } from "crypto";
522
- import { cp, lstat as lstat2, mkdir, readdir as readdir2, rename, rm, rmdir, symlink, writeFile } from "fs/promises";
626
+ import { cp, lstat as lstat2, mkdir, readFile as readFile3, readdir as readdir2, rename, rm, rmdir, symlink, writeFile } from "fs/promises";
523
627
  import { dirname, join as join3, relative, sep } from "path";
524
628
  var MANIFEST_RELATIVE = join3(".agents", "bearings.json");
525
629
  async function capture(repoDir, txDir, relativePath, snapshots) {
@@ -554,6 +658,16 @@ async function ensureDir(repoDir, absDir, createdDirs) {
554
658
  }
555
659
  }
556
660
  }
661
+ async function writeContent(repoDir, txDir, relativePath, content, snapshots, createdDirs) {
662
+ await capture(repoDir, txDir, relativePath, snapshots);
663
+ const target = join3(repoDir, relativePath);
664
+ await ensureDir(repoDir, dirname(target), createdDirs);
665
+ await writeFile(target, content);
666
+ }
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
+ }
557
671
  async function restore(snapshots, hooks, txDir) {
558
672
  for (const snapshot of [...snapshots].reverse()) {
559
673
  try {
@@ -593,14 +707,26 @@ async function applyUpdate(repoDir, plan, hooks = {}) {
593
707
  try {
594
708
  for (const action of plan.files) {
595
709
  if (action.kind === "write") {
596
- await capture(repoDir, txDir, action.path, snapshots);
597
- await ensureDir(repoDir, dirname(abs(action.path)), createdDirs);
598
- await writeFile(abs(action.path), action.content);
710
+ await writeContent(repoDir, txDir, action.path, action.content, snapshots, createdDirs);
711
+ if (isStarterSkillTarget(action.path)) {
712
+ await writeContent(
713
+ repoDir,
714
+ txDir,
715
+ skillBaselinePath(starterSkillNameFromTarget(action.path)),
716
+ action.content,
717
+ snapshots,
718
+ createdDirs
719
+ );
720
+ }
599
721
  await afterOp();
600
722
  } else if (action.kind === "merge") {
601
723
  await captureToBackup(repoDir, action.path, action.backup, snapshots);
602
724
  await writeFile(abs(action.path), action.content);
603
725
  await afterOp();
726
+ } else if (action.kind === "skill-handoff") {
727
+ await writeCopy(repoDir, txDir, action.path, action.backup, snapshots, createdDirs);
728
+ await writeContent(repoDir, txDir, action.incomingPath, action.content, snapshots, createdDirs);
729
+ await afterOp();
604
730
  } else if (action.kind === "delete") {
605
731
  await capture(repoDir, txDir, action.path, snapshots);
606
732
  await afterOp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bearings",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Give your AI agents their bearings — scaffold an agent-friendly setup in any repository.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -20,7 +20,9 @@ Primary stack: <agent: fill during handoff — language/runtime/framework/platfo
20
20
  | A consequential domain or technical decision | `docs/adr/INDEX.md` | Read the index first, then only matching ADRs. |
21
21
  | Planning a new capability | `docs/deferred/INDEX.md` | Read the index first, then only matching deferred details. |
22
22
 
23
- ## Response Style
23
+ ## User-Facing Output
24
+
25
+ Write all user-facing output for a reader with ADHD. Keep it concise, concrete, easy to scan, and in ASD-STE100 Simplified Technical English.
24
26
 
25
27
  - Lead with the answer; omit preamble and restatement.
26
28
  - Use the shortest clear structure: line, bullets, table, tree, or flow.
@@ -11,14 +11,14 @@ 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 10 below.
14
+ `AGENTS.md` permits. Do not remove the gate until step 11 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
20
  1. Read every file record's ordered `reconciliations`.
21
- 2. For each reconciliation, read its Backup File and current target, summarize
21
+ 2. For each non-`skill-update` reconciliation, read its Backup File and current target, summarize
22
22
  the differences, and ask which backed-up changes to apply to the target.
23
23
  3. Apply the developer's choice directly to the target regardless of owner,
24
24
  delete the resolved Backup File, and remove its reconciliation record.
@@ -32,6 +32,35 @@ When `.agents/bearings.json` is manifest v2 and has `setupPending`:
32
32
  manifest.
33
33
  7. Run `bearings verify` again and finish only at zero failures and warnings.
34
34
 
35
+ ### Starter skill updates (`skill-update`)
36
+
37
+ For each file record with a `skill-update` reconciliation:
38
+
39
+ 1. Read `basePath`, the live skill path (local), and `incomingPath`. Check
40
+ whether `basePath` exists before attempting any diff.
41
+ 2. If `basePath` is missing, explain that Three-way merge is not available
42
+ without a historical base. Do not fabricate one. Offer exactly these
43
+ options (one skill at a time):
44
+ - **Take new template**, **Keep local**, and **Freeform**. Apply the same
45
+ manifest and baseline updates described below for the selected outcome.
46
+ 3. If `basePath` exists, diff local vs base and incoming vs base; summarize
47
+ both for the developer. If `basePath` exists, offer exactly these four options
48
+ (one skill at a time), mark a recommendation:
49
+ - **Take new template** — replace live skill with incoming; set manifest
50
+ `hash` and `lastTemplateHash` to incoming hash; `owner: agent`.
51
+ - **Keep local** — leave live skill; set `hash` to current content hash;
52
+ keep `lastTemplateHash`; set `skippedTemplate` to the incoming
53
+ version/hash; `owner: agent`.
54
+ - **Three-way merge** (Recommended when both diffs are non-empty) — merge
55
+ both sides into the live skill; then set `hash` to result hash and
56
+ `lastTemplateHash` to incoming hash; `owner: agent`.
57
+ - **Freeform** — developer/agent writes the resolved file; same baselining
58
+ as merge (hash = result, `lastTemplateHash` = incoming hash) unless they
59
+ explicitly choose to decline the template (then same as Keep local).
60
+ 4. Refresh `.agents/.bearings-baseline/skills/<name>/SKILL.md` to match the
61
+ new content hash.
62
+ 5. Delete backup, incoming file, and the reconciliation entry.
63
+
35
64
  ## Required workflow
36
65
 
37
66
  1. Read `AGENTS.md` and `.agents/bearings.json`.
@@ -47,31 +76,50 @@ When `.agents/bearings.json` is manifest v2 and has `setupPending`:
47
76
  4. Fill only the `AGENTS.md` project-purpose and primary-stack placeholders.
48
77
  Keep `AGENTS.md` a thin router — do not add a skill table, invariants
49
78
  section, or always-on rules block.
50
- 5. Create a project-specific skill only when native skill discovery has no
51
- adequate existing match. Every starter skill is bearings-owned — leave
52
- them untouched.
53
- 6. Run the `/refresh-repo-map` workflow to initialize `docs/DOMAIN.md`,
79
+ 5. Adapt every starter skill:
80
+ - `.agents/skills/commit-convention/SKILL.md`
81
+ - `.agents/skills/defer-work/SKILL.md`
82
+ - `.agents/skills/resurface-deferred-work/SKILL.md`
83
+ - `.agents/skills/recording-decisions/SKILL.md`
84
+ - Explore the repo for format, lint-fix, lint, typecheck, test, build,
85
+ and docs verification commands.
86
+ - Replace every `<agent: fill during handoff — …>` marker with a real
87
+ command or explicit `not configured`.
88
+ - Light project tailoring only — do not remove the skill's safety workflow.
89
+ 6. Claim starter skills in `.agents/bearings.json` (allowed manifest edit):
90
+ - Set each starter skill `owner` to `agent`.
91
+ - Set `hash` to the sha256 of the file contents (use the same algorithm as
92
+ bearings: UTF-8 body, `sha256:` + hex).
93
+ - Set `lastTemplateHash` to the hash of the skill template as shipped in the
94
+ installed bearings package (read from the package templates if needed;
95
+ if the adapted file still matches the package template byte-for-byte,
96
+ `lastTemplateHash` equals `hash`).
97
+ - Write `.agents/.bearings-baseline/skills/<name>/SKILL.md` equal to the
98
+ live file bytes.
99
+ 7. Run the `/refresh-repo-map` workflow to initialize `docs/DOMAIN.md`,
54
100
  `docs/ARCHITECTURE.md`, `docs/CODEBASE_MAP.md`, and
55
101
  `docs/diagrams/c4-component.puml`.
56
- 7. Expose any newly created skills into each configured harness using the
102
+ 8. Expose any newly created skills into each configured harness using the
57
103
  manifest's existing symlink/copy mode.
58
- 8. Run `bearings verify`. Fix every failure and every warning.
59
- 9. Confirm the run reports zero failures and zero warnings before
60
- proceeding.
61
- 10. Remove the `## Setup Required` section from `AGENTS.md` — only after
62
- step 9 confirms zero failures and zero warnings.
63
- 11. Run `bearings verify` again and report the completed setup to the
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
64
110
  developer.
65
111
 
66
112
  ## Rules
67
113
 
68
114
  - Do not overwrite developer decisions silently — every merge/keep/discard
69
115
  of a Backup File is the developer's call.
70
- - Do not edit `.agents/bearings.json` except where this workflow requires
71
- it.
72
- - Do not change bearings-owned starter commands or skills. The one exception:
73
- a reconciliation that records an explicit Merge decision permits applying
74
- that Backup File's changes directly to its bearings-owned target.
116
+ - Do not edit bearings-owned **commands** except when a recorded Merge
117
+ reconciliation explicitly permits applying backup changes.
118
+ - Do edit starter **skills** during setup (adapt + claim). After claim they
119
+ are agent-owned; maintainers may edit them freely.
120
+ - Manifest edits are limited to: skill `owner` / `hash` / `lastTemplateHash` /
121
+ `skippedTemplate` / `reconciliations`, deleting resolved backups/incoming,
122
+ and clearing `setupPending`.
75
123
  - Do not add a skill registry row to `AGENTS.md` — native skill discovery
76
124
  replaces it.
77
125
  - Do not invent domain or technical constraints — every stated constraint
@@ -3,96 +3,29 @@ name: commit-convention
3
3
  description: Commit changes through a deterministic commit gate. Use when an agent is about to create a git commit.
4
4
  ---
5
5
 
6
- # Commit Gate
6
+ # Commit Convention
7
7
 
8
8
  ## Project Commands
9
9
 
10
- - Format intended files: `<agent: fill during handoff — formatter command accepting explicit file paths>`
11
- - Fix lint errors in intended files: `<agent: fill during handoff — lint-fix command accepting explicit file paths>`
12
- - Check lint: `<agent: fill during handoff — complete lint command>`
13
- - Typecheck: `<agent: fill during handoff — typecheck command or "not configured">`
14
- - Test suite: `<agent: fill during handoff — complete test-suite command>`
15
- - Build: `<agent: fill during handoff — build command or "not configured">`
16
-
17
- ## Required Workflow
18
-
19
- 1. Establish the intended change set.
20
- - Inspect `git status`, `git diff`, and `git diff --cached`.
21
- - The current task defines the commit allowlist; repository dirtiness does not expand it.
22
- - Classify every changed hunk as intended or unrelated.
23
- - Preserve unrelated staged and unstaged changes exactly.
24
- - For a mixed file, proceed only when tools and hunk staging can leave unrelated changes untouched.
25
- - Completion: every hunk is classified and the intended changes can be isolated safely.
26
-
27
- 2. Format and lint-fix the intended files.
28
- - Pass explicit intended file paths to each configured tool.
29
- - Repository-wide auto-fixes are prohibited.
30
- - Reinspect the diff after each command and require unrelated hunks to remain unchanged.
31
- - Completion: every intended file is formatted and has no lint errors.
32
-
33
- 3. Run the quality gates.
34
- - Run the complete lint check.
35
- - Run typecheck when configured.
36
- - Run the complete test suite.
37
- - Run the build when configured.
38
- - Completion: every configured gate must exit successfully.
39
-
40
- 4. Review and stage the intended changes.
41
- - Reject secrets, debug code, generated noise, and unrelated changes.
42
- - Stage explicit intended paths or hunks; `git add .` and `git add -A` are prohibited.
43
- - Review `git diff --cached`.
44
- - Completion: every staged hunk belongs to the current task.
45
-
46
- 5. Write the commit message.
47
- - Follow the Conventional Commits policy below.
48
- - Describe the staged change rather than the broader task or conversation.
49
- - Completion: the message satisfies the documented grammar and rules.
50
-
51
- 6. Create and verify the commit.
52
- - Commit without bypassing hooks.
53
- - Amend only when the developer explicitly requests it.
54
- - Verify the resulting commit hash and message.
55
- - Report the commit and any unrelated changes left untouched.
56
- - Completion: the commit exists with the intended tree and message.
57
-
58
- ## Conventional Commits
59
-
60
- Use `<type>[optional scope][!]: <imperative summary>`.
61
-
62
- | Type | Use |
63
- |---|---|
64
- | `feat` | New user-visible behavior |
65
- | `fix` | Defect correction |
66
- | `docs` | Documentation only |
67
- | `style` | Formatting with no behavior change |
68
- | `refactor` | Internal change with no feature or fix |
69
- | `perf` | Performance improvement |
70
- | `test` | Tests only |
71
- | `build` | Build system or dependency changes |
72
- | `ci` | CI configuration |
73
- | `chore` | Maintenance not covered above |
74
- | `revert` | Revert a previous commit |
75
-
76
- Message rules:
77
-
78
- - Use a short, imperative summary beginning with lowercase text.
79
- - Keep the complete subject line at or below 72 characters and omit a trailing period.
80
- - Use a stable package or subsystem name as the optional scope.
81
- - Mark breaking changes with `!` and add a `BREAKING CHANGE:` footer.
82
- - Add a body only when the reason or migration impact is not clear from the subject.
83
- - Add issue references as footers when applicable.
84
- - Add co-author or agent attribution only when explicitly requested.
85
-
86
- Examples:
87
-
88
- - `feat(cli): add copy-mode verification`
89
- - `fix(adapter): preserve unrelated staged changes`
90
- - `docs: clarify setup workflow`
91
- - `refactor(generator)!: replace manifest ownership model`
92
-
93
- ## Stop Conditions
94
-
95
- - Unsafe change isolation -> report the overlap and create no commit.
96
- - A required gate fails because of intended changes -> fix it and rerun that gate.
97
- - A required gate fails for an unrelated or pre-existing reason -> report it and create no commit.
98
- - A commit hook fails -> treat it as a required gate; keep the hook enabled.
10
+ - Format intended files: `<agent: fill during handoff — formatter command accepting explicit file paths, or "not configured">`
11
+ - Fix lint errors in intended files: `<agent: fill during handoff — lint-fix command accepting explicit file paths, or "not configured">`
12
+ - Check lint: `<agent: fill during handoff — complete lint command, or "not configured">`
13
+ - Typecheck: `<agent: fill during handoff — typecheck command, or "not configured">`
14
+ - Other quality gates: `<agent: fill during handoff — remaining quality commands such as dependency-cruiser, tests, or build, or "not configured">`
15
+
16
+ ## Workflow
17
+
18
+ 1. Inspect the worktree and staged diff. The current task defines the commit
19
+ allowlist; leave unrelated changes untouched. Stop if safe isolation is
20
+ impossible.
21
+ 2. On the intended files, run the format and lint-fix commands, then run the
22
+ lint and typecheck commands to verify. If any check reports errors, fix them
23
+ and re-run that check; repeat until it passes clean. Then run the remaining
24
+ quality gates and resolve any failures the same way before continuing.
25
+ 3. Stage only intended paths or hunks, then review the staged diff.
26
+ 4. Write a Conventional Commit describing the staged change. Use an
27
+ imperative lowercase subject of at most 72 characters with no trailing
28
+ period. Add a body only for rationale or migration impact. Add attribution
29
+ only when explicitly requested.
30
+ 5. Commit without bypassing hooks. Amend only when explicitly requested.
31
+ Verify the resulting commit and report unrelated changes left untouched.