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 +7 -3
- package/dist/{chunk-VRWQ3OWC.js → chunk-GEIRG7UT.js} +57 -6
- package/dist/cli.js +20 -2
- package/dist/{update-5F7TZEUH.js → update-XOUXGD2Z.js} +132 -6
- package/package.json +1 -1
- package/templates/AGENTS.md +3 -1
- package/templates/agents/commands/setup-repo.md +66 -18
- package/templates/agents/skills/commit-convention/SKILL.md +23 -90
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
|
|
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 |
|
|
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
|
|
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
|
|
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"
|
|
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-
|
|
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-
|
|
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" &&
|
|
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-
|
|
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
|
|
597
|
-
|
|
598
|
-
|
|
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
package/templates/AGENTS.md
CHANGED
|
@@ -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
|
-
##
|
|
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
|
|
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.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
102
|
+
8. Expose any newly created skills into each configured harness using the
|
|
57
103
|
manifest's existing symlink/copy mode.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
step
|
|
63
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
- Do
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|