bearings 0.5.2 → 0.5.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 +40 -22
- package/dist/chunk-MOZJECCW.js +1823 -0
- package/dist/cli.js +189 -158
- package/dist/{update-4DZKUPFP.js → update-YY5SLPDF.js} +84 -221
- package/package.json +1 -1
- package/templates/AGENTS.md +1 -5
- package/templates/agents/commands/refresh-repo-map.md +36 -13
- package/templates/agents/commands/update-bearings-setup.md +144 -0
- package/templates/agents/skills/checklist/previewer/index.html +44 -10
- package/templates/agents/skills/checklist/previewer/viewer.css +321 -77
- package/templates/agents/skills/checklist/previewer/viewer.js +797 -97
- package/templates/initial-setup.md +112 -0
- package/dist/chunk-WYXKSCUW.js +0 -818
- package/templates/agents/commands/setup-repo.md +0 -182
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ADAPTER_KINDS,
|
|
4
|
+
PLACEHOLDER,
|
|
4
5
|
SCAFFOLD,
|
|
6
|
+
applyUpdate,
|
|
5
7
|
canonicalAdapterEntries,
|
|
6
8
|
entriesMatch,
|
|
7
9
|
freeBackupPath,
|
|
10
|
+
isChecklistPayload,
|
|
8
11
|
isStarterSkillTarget,
|
|
9
12
|
migrateV1,
|
|
13
|
+
normalizeSetup,
|
|
14
|
+
pendingSetupReport,
|
|
15
|
+
protectSkills,
|
|
16
|
+
reconciliationTasks,
|
|
10
17
|
renderUpdatePlan,
|
|
11
18
|
renderUpdateReport,
|
|
19
|
+
saveManifest,
|
|
20
|
+
setupArtifacts,
|
|
12
21
|
sha256,
|
|
13
22
|
skillBaselinePath,
|
|
14
23
|
skillIncomingPath,
|
|
15
24
|
starterSkillNameFromTarget,
|
|
25
|
+
tailoringTask,
|
|
16
26
|
templatesDir,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./chunk-
|
|
27
|
+
uniqueSetupTasks,
|
|
28
|
+
validateHarnesses
|
|
29
|
+
} from "./chunk-MOZJECCW.js";
|
|
30
|
+
|
|
31
|
+
// src/commands/update.ts
|
|
32
|
+
import { isDeepStrictEqual } from "util";
|
|
20
33
|
|
|
21
34
|
// src/semver.ts
|
|
22
35
|
function parseSemver(value) {
|
|
@@ -68,7 +81,7 @@ function compareSemver(left, right) {
|
|
|
68
81
|
// src/update/planner.ts
|
|
69
82
|
import { access, readFile } from "fs/promises";
|
|
70
83
|
import { join } from "path";
|
|
71
|
-
var SETUP_COMMAND_PATH = ".agents/commands/setup
|
|
84
|
+
var SETUP_COMMAND_PATH = ".agents/commands/update-bearings-setup.md";
|
|
72
85
|
async function exists(path) {
|
|
73
86
|
try {
|
|
74
87
|
await access(path);
|
|
@@ -104,6 +117,18 @@ async function planFiles(input) {
|
|
|
104
117
|
const templateContent = await readFile(join(source.templatesDir, entry.template), "utf8");
|
|
105
118
|
const incomingHash = sha256(templateContent);
|
|
106
119
|
const prevRecord = previous?.files.find((file) => file.path === entry.target);
|
|
120
|
+
if (!prevRecord && reconstruction && currentExists && entry.target.startsWith(".agents/skills/") && currentHash !== incomingHash) {
|
|
121
|
+
actions.push({ kind: "keep", record: {
|
|
122
|
+
path: entry.target,
|
|
123
|
+
template: entry.template,
|
|
124
|
+
templateVersion: source.bearingsVersion,
|
|
125
|
+
hash: isStarterSkillTarget(entry.target) ? currentHash : incomingHash,
|
|
126
|
+
owner: entry.owner,
|
|
127
|
+
...isStarterSkillTarget(entry.target) ? { lastTemplateHash: incomingHash } : {},
|
|
128
|
+
skippedTemplate: { templateVersion: source.bearingsVersion, hash: incomingHash }
|
|
129
|
+
} });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
107
132
|
if (isStarterSkillTarget(entry.target)) {
|
|
108
133
|
const prevOwner = prevRecord?.owner ?? entry.owner;
|
|
109
134
|
const lastTemplateHash = prevRecord?.lastTemplateHash ?? prevRecord?.hash;
|
|
@@ -113,7 +138,8 @@ async function planFiles(input) {
|
|
|
113
138
|
templateVersion: source.bearingsVersion,
|
|
114
139
|
hash: incomingHash,
|
|
115
140
|
owner: prevOwner === "agent" ? "agent" : entry.owner,
|
|
116
|
-
lastTemplateHash: incomingHash
|
|
141
|
+
lastTemplateHash: incomingHash,
|
|
142
|
+
...prevRecord?.reconciliations ? { reconciliations: prevRecord.reconciliations } : {}
|
|
117
143
|
};
|
|
118
144
|
if (!currentExists) {
|
|
119
145
|
actions.push({
|
|
@@ -146,16 +172,13 @@ async function planFiles(input) {
|
|
|
146
172
|
actions.push({ kind: "keep", record: prevRecord });
|
|
147
173
|
continue;
|
|
148
174
|
}
|
|
149
|
-
if (currentHash ===
|
|
175
|
+
if (currentHash === lastTemplateHash) {
|
|
150
176
|
actions.push({
|
|
151
177
|
kind: "write",
|
|
152
178
|
reason: "replace",
|
|
153
179
|
path: entry.target,
|
|
154
180
|
content: templateContent,
|
|
155
|
-
record:
|
|
156
|
-
...incomingRecordBase,
|
|
157
|
-
...prevRecord.reconciliations ? { reconciliations: prevRecord.reconciliations } : {}
|
|
158
|
-
}
|
|
181
|
+
record: incomingRecordBase
|
|
159
182
|
});
|
|
160
183
|
continue;
|
|
161
184
|
}
|
|
@@ -182,7 +205,8 @@ async function planFiles(input) {
|
|
|
182
205
|
template: entry.template,
|
|
183
206
|
templateVersion: source.bearingsVersion,
|
|
184
207
|
hash: incomingHash,
|
|
185
|
-
owner: entry.owner
|
|
208
|
+
owner: entry.owner,
|
|
209
|
+
...prevRecord?.reconciliations ? { reconciliations: prevRecord.reconciliations } : {}
|
|
186
210
|
};
|
|
187
211
|
const canSkip = entry.target !== SETUP_COMMAND_PATH;
|
|
188
212
|
if (!currentExists) {
|
|
@@ -325,7 +349,6 @@ async function resolveFilePlan(repoDir, draft, decisions) {
|
|
|
325
349
|
}
|
|
326
350
|
const actions = [];
|
|
327
351
|
const files = [];
|
|
328
|
-
let setupRequired = draft.reconstruction;
|
|
329
352
|
for (const action of draft.actions) {
|
|
330
353
|
if (action.kind === "keep") {
|
|
331
354
|
actions.push(action);
|
|
@@ -335,19 +358,16 @@ async function resolveFilePlan(repoDir, draft, decisions) {
|
|
|
335
358
|
if (action.kind === "write") {
|
|
336
359
|
actions.push(action);
|
|
337
360
|
files.push(action.record);
|
|
338
|
-
setupRequired = true;
|
|
339
361
|
continue;
|
|
340
362
|
}
|
|
341
363
|
if (action.kind === "delete") {
|
|
342
364
|
actions.push(action);
|
|
343
|
-
setupRequired = true;
|
|
344
365
|
continue;
|
|
345
366
|
}
|
|
346
367
|
if (action.kind === "untrack-missing") {
|
|
347
368
|
actions.push(action);
|
|
348
369
|
if (action.previous.reconciliations?.length) {
|
|
349
370
|
files.push({ ...action.previous, retired: true });
|
|
350
|
-
setupRequired = true;
|
|
351
371
|
}
|
|
352
372
|
continue;
|
|
353
373
|
}
|
|
@@ -375,7 +395,6 @@ async function resolveFilePlan(repoDir, draft, decisions) {
|
|
|
375
395
|
};
|
|
376
396
|
actions.push({ kind: "skill-handoff", path: action.path, content: action.content, backup, basePath, incomingPath, record: record2 });
|
|
377
397
|
files.push(record2);
|
|
378
|
-
setupRequired = true;
|
|
379
398
|
continue;
|
|
380
399
|
}
|
|
381
400
|
if (action.kind === "conflict") {
|
|
@@ -391,7 +410,6 @@ async function resolveFilePlan(repoDir, draft, decisions) {
|
|
|
391
410
|
actions.push(resolved2);
|
|
392
411
|
if (record2) files.push(record2);
|
|
393
412
|
}
|
|
394
|
-
setupRequired = true;
|
|
395
413
|
continue;
|
|
396
414
|
}
|
|
397
415
|
const decision = decisionsByPath.get(action.path);
|
|
@@ -400,13 +418,27 @@ async function resolveFilePlan(repoDir, draft, decisions) {
|
|
|
400
418
|
const { resolved, record } = resolveRemovedConflict(action, decision);
|
|
401
419
|
actions.push(resolved);
|
|
402
420
|
if (record) files.push(record);
|
|
403
|
-
setupRequired = true;
|
|
404
421
|
}
|
|
405
422
|
if (decisionsByPath.size > 0) {
|
|
406
423
|
const [leftoverPath] = decisionsByPath.keys();
|
|
407
424
|
fail(`Unexpected decision for path: ${leftoverPath}`);
|
|
408
425
|
}
|
|
409
|
-
|
|
426
|
+
const tasks = uniqueSetupTasks([
|
|
427
|
+
...actions.flatMap((action) => {
|
|
428
|
+
if (action.kind !== "write" && action.kind !== "merge") return [];
|
|
429
|
+
const task = tailoringTask(action.record, action.content);
|
|
430
|
+
return task ? [task] : [];
|
|
431
|
+
}),
|
|
432
|
+
...reconciliationTasks({ files })
|
|
433
|
+
]);
|
|
434
|
+
if (draft.reconstruction) {
|
|
435
|
+
for (const action of actions) {
|
|
436
|
+
if (action.kind !== "keep") continue;
|
|
437
|
+
const task = tailoringTask(action.record, await readFile(join(repoDir, action.record.path), "utf8"));
|
|
438
|
+
if (task && PLACEHOLDER.test(await readFile(join(repoDir, action.record.path), "utf8"))) tasks.push(task);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { actions, files, tasks: uniqueSetupTasks(tasks), setupRequired: tasks.length > 0 };
|
|
410
442
|
}
|
|
411
443
|
|
|
412
444
|
// src/update/adapter-plan.ts
|
|
@@ -514,7 +546,7 @@ async function preflightAdapters(repoDir, manifest) {
|
|
|
514
546
|
}
|
|
515
547
|
if (!adapterStat.isDirectory()) driftError(adapterRel);
|
|
516
548
|
const expected = filesUnderRoot(manifest.files, kind, name);
|
|
517
|
-
const actualRelFiles = await listFilesRecursive(adapterAbs);
|
|
549
|
+
const actualRelFiles = (await listFilesRecursive(adapterAbs)).filter((path) => !isChecklistPayload(`.agents/${kind}/${name}/${path}`));
|
|
518
550
|
if (actualRelFiles.length !== expected.length) driftError(adapterRel);
|
|
519
551
|
for (const relFile of actualRelFiles) {
|
|
520
552
|
const record = expected.find((file) => file.path === `.agents/${kind}/${name}/${relFile}`);
|
|
@@ -536,12 +568,16 @@ async function preflightAdapterCollisions(input) {
|
|
|
536
568
|
const currentHarnessSet = new Set(currentHarnesses);
|
|
537
569
|
for (const harness of desiredHarnesses) {
|
|
538
570
|
for (const root of candidateRoots) {
|
|
539
|
-
if (currentHarnessSet.has(harness) && previousRootKeys.has(root)) continue;
|
|
540
571
|
const [kind, name] = splitRoot(root);
|
|
541
572
|
const adapterRel = join2(`.${harness}`, kind, name);
|
|
542
|
-
|
|
543
|
-
|
|
573
|
+
const adapterAbs = join2(repoDir, adapterRel);
|
|
574
|
+
const existing = await lstat(adapterAbs).catch(() => null);
|
|
575
|
+
if (!existing) continue;
|
|
576
|
+
if (currentHarnessSet.has(harness) && previousRootKeys.has(root)) {
|
|
577
|
+
const matches = existing.isSymbolicLink() ? await readlink(adapterAbs) === join2("..", "..", ".agents", kind, name) : await entriesMatch(join2(repoDir, ".agents", kind, name), adapterAbs);
|
|
578
|
+
if (matches) continue;
|
|
544
579
|
}
|
|
580
|
+
throw new Error(`Adapter collision: ${adapterRel} already exists and is not managed by bearings`);
|
|
545
581
|
}
|
|
546
582
|
}
|
|
547
583
|
}
|
|
@@ -618,191 +654,9 @@ async function planAdapterActions(input) {
|
|
|
618
654
|
return actions;
|
|
619
655
|
}
|
|
620
656
|
|
|
621
|
-
// src/update/transaction.ts
|
|
622
|
-
import { randomUUID } from "crypto";
|
|
623
|
-
import { cp, lstat as lstat2, mkdir, readFile as readFile3, readdir as readdir2, rename, rm, rmdir, symlink, writeFile } from "fs/promises";
|
|
624
|
-
import { dirname, join as join3, relative, sep } from "path";
|
|
625
|
-
var MANIFEST_RELATIVE = join3(".agents", "bearings.json");
|
|
626
|
-
async function capture(repoDir, txDir, relativePath, snapshots) {
|
|
627
|
-
const target = join3(repoDir, relativePath);
|
|
628
|
-
const stat = await lstat2(target).catch(() => null);
|
|
629
|
-
if (!stat) {
|
|
630
|
-
snapshots.push({ target });
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
const stored = join3(txDir, "snapshots", String(snapshots.length));
|
|
634
|
-
await mkdir(dirname(stored), { recursive: true });
|
|
635
|
-
await rename(target, stored);
|
|
636
|
-
snapshots.push({ target, stored });
|
|
637
|
-
}
|
|
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) });
|
|
644
|
-
}
|
|
645
|
-
async function ensureDir(repoDir, absDir, createdDirs) {
|
|
646
|
-
const rel = relative(repoDir, absDir);
|
|
647
|
-
if (!rel || rel.startsWith("..")) return;
|
|
648
|
-
let current = repoDir;
|
|
649
|
-
for (const part of rel.split(sep)) {
|
|
650
|
-
current = join3(current, part);
|
|
651
|
-
const stat = await lstat2(current).catch(() => null);
|
|
652
|
-
if (!stat) {
|
|
653
|
-
await mkdir(current);
|
|
654
|
-
createdDirs.push(current);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
async function writeContent(repoDir, txDir, relativePath, content, snapshots, createdDirs) {
|
|
659
|
-
await capture(repoDir, txDir, relativePath, snapshots);
|
|
660
|
-
const target = join3(repoDir, relativePath);
|
|
661
|
-
await ensureDir(repoDir, dirname(target), createdDirs);
|
|
662
|
-
await writeFile(target, content);
|
|
663
|
-
}
|
|
664
|
-
async function restore(snapshots, hooks, txDir) {
|
|
665
|
-
for (const snapshot of [...snapshots].reverse()) {
|
|
666
|
-
try {
|
|
667
|
-
await hooks.beforeRollbackRestore?.(snapshot.target);
|
|
668
|
-
await rm(snapshot.target, { recursive: true, force: true });
|
|
669
|
-
if (snapshot.stored) {
|
|
670
|
-
await mkdir(dirname(snapshot.target), { recursive: true });
|
|
671
|
-
await rename(snapshot.stored, snapshot.target);
|
|
672
|
-
}
|
|
673
|
-
} catch (error) {
|
|
674
|
-
throw new Error(
|
|
675
|
-
`Rollback failed while restoring ${snapshot.target}; transaction data retained at ${txDir}. Underlying error: ${error.message}`
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
async function removeEmptyDirs(createdDirs) {
|
|
681
|
-
for (const dir of [...createdDirs].reverse()) {
|
|
682
|
-
try {
|
|
683
|
-
const entries = await readdir2(dir);
|
|
684
|
-
if (entries.length === 0) await rmdir(dir);
|
|
685
|
-
} catch {
|
|
686
|
-
}
|
|
687
|
-
}
|
|
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
|
-
}
|
|
708
|
-
async function applyUpdate(repoDir, plan, hooks = {}) {
|
|
709
|
-
const txDir = join3(repoDir, ".agents", `.bearings-txn-${randomUUID()}`);
|
|
710
|
-
const snapshots = [];
|
|
711
|
-
const createdDirs = [];
|
|
712
|
-
const abs = (relativePath) => join3(repoDir, relativePath);
|
|
713
|
-
let opIndex = 0;
|
|
714
|
-
const afterOp = async () => {
|
|
715
|
-
const current = opIndex;
|
|
716
|
-
opIndex += 1;
|
|
717
|
-
await hooks.afterOperation?.(current);
|
|
718
|
-
};
|
|
719
|
-
try {
|
|
720
|
-
for (const action of plan.files) {
|
|
721
|
-
if (action.kind === "write") {
|
|
722
|
-
await writeContent(repoDir, txDir, action.path, action.content, snapshots, createdDirs);
|
|
723
|
-
if (isStarterSkillTarget(action.path)) {
|
|
724
|
-
await writeContent(
|
|
725
|
-
repoDir,
|
|
726
|
-
txDir,
|
|
727
|
-
skillBaselinePath(starterSkillNameFromTarget(action.path)),
|
|
728
|
-
action.content,
|
|
729
|
-
snapshots,
|
|
730
|
-
createdDirs
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
await afterOp();
|
|
734
|
-
} else if (action.kind === "merge") {
|
|
735
|
-
await captureToBackup(repoDir, txDir, action.path, action.backup, snapshots);
|
|
736
|
-
await writeFile(abs(action.path), action.content);
|
|
737
|
-
await afterOp();
|
|
738
|
-
} else if (action.kind === "skill-handoff") {
|
|
739
|
-
await writeBackup(repoDir, action.path, await readFile3(abs(action.path)), { path: action.backup });
|
|
740
|
-
snapshots.push({ target: abs(action.backup) });
|
|
741
|
-
await writeContent(repoDir, txDir, action.incomingPath, action.content, snapshots, createdDirs);
|
|
742
|
-
await afterOp();
|
|
743
|
-
} else if (action.kind === "delete") {
|
|
744
|
-
await capture(repoDir, txDir, action.path, snapshots);
|
|
745
|
-
await afterOp();
|
|
746
|
-
}
|
|
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
|
-
}
|
|
769
|
-
for (const action of plan.adapters) {
|
|
770
|
-
if (action.kind === "write-symlink") {
|
|
771
|
-
await capture(repoDir, txDir, action.path, snapshots);
|
|
772
|
-
await ensureDir(repoDir, dirname(abs(action.path)), createdDirs);
|
|
773
|
-
await symlink(action.target, abs(action.path));
|
|
774
|
-
await afterOp();
|
|
775
|
-
} else if (action.kind === "write-copy") {
|
|
776
|
-
await capture(repoDir, txDir, action.path, snapshots);
|
|
777
|
-
await ensureDir(repoDir, dirname(abs(action.path)), createdDirs);
|
|
778
|
-
await cp(action.source, abs(action.path), { recursive: true });
|
|
779
|
-
await afterOp();
|
|
780
|
-
} else {
|
|
781
|
-
await capture(repoDir, txDir, action.path, snapshots);
|
|
782
|
-
await afterOp();
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
await capture(repoDir, txDir, MANIFEST_RELATIVE, snapshots);
|
|
786
|
-
await ensureDir(repoDir, dirname(abs(MANIFEST_RELATIVE)), createdDirs);
|
|
787
|
-
await writeFile(abs(MANIFEST_RELATIVE), JSON.stringify(plan.manifest, null, 2) + "\n");
|
|
788
|
-
await afterOp();
|
|
789
|
-
} catch (error) {
|
|
790
|
-
await restore(snapshots, hooks, txDir);
|
|
791
|
-
await removeEmptyDirs(createdDirs);
|
|
792
|
-
await rm(txDir, { recursive: true, force: true }).catch(() => {
|
|
793
|
-
});
|
|
794
|
-
throw error;
|
|
795
|
-
}
|
|
796
|
-
try {
|
|
797
|
-
await hooks.beforeCleanup?.();
|
|
798
|
-
await rm(txDir, { recursive: true, force: true });
|
|
799
|
-
} catch {
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
657
|
// src/update/testplan-migration.ts
|
|
804
|
-
import { access as access2, readFile as
|
|
805
|
-
import { join as
|
|
658
|
+
import { access as access2, readFile as readFile3, readdir as readdir2 } from "fs/promises";
|
|
659
|
+
import { join as join3 } from "path";
|
|
806
660
|
var LEGACY_NAME = /^(.+)\.testplan\.json$/;
|
|
807
661
|
var SCOPE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
808
662
|
async function exists2(path) {
|
|
@@ -887,8 +741,8 @@ function convertLegacyPlan(plan) {
|
|
|
887
741
|
return JSON.stringify(checklist, null, 2) + "\n";
|
|
888
742
|
}
|
|
889
743
|
async function planTestplanMigrations(repoDir) {
|
|
890
|
-
const dir =
|
|
891
|
-
const entries = await
|
|
744
|
+
const dir = join3(repoDir, "testplans");
|
|
745
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch((error) => {
|
|
892
746
|
if (error.code === "ENOENT") return null;
|
|
893
747
|
throw error;
|
|
894
748
|
});
|
|
@@ -902,7 +756,7 @@ async function planTestplanMigrations(repoDir) {
|
|
|
902
756
|
const source = `testplans/${entry.name}`;
|
|
903
757
|
const slug = match[1];
|
|
904
758
|
const target = SCOPE.test(slug) ? `checklists/tests/${slug}.json` : void 0;
|
|
905
|
-
const raw = await
|
|
759
|
+
const raw = await readFile3(join3(repoDir, source), "utf8");
|
|
906
760
|
let parsed;
|
|
907
761
|
try {
|
|
908
762
|
parsed = JSON.parse(raw);
|
|
@@ -922,9 +776,9 @@ async function planTestplanMigrations(repoDir) {
|
|
|
922
776
|
continue;
|
|
923
777
|
}
|
|
924
778
|
const content = convertLegacyPlan(parsed);
|
|
925
|
-
if (await exists2(
|
|
779
|
+
if (await exists2(join3(repoDir, target))) {
|
|
926
780
|
const backup = await freeBackupPath(repoDir, target);
|
|
927
|
-
const collided = await
|
|
781
|
+
const collided = await readFile3(join3(repoDir, target), "utf8");
|
|
928
782
|
actions.push({ kind: "migrate-testplan", source, target, content, backup });
|
|
929
783
|
reconciliations.push({
|
|
930
784
|
kind: "testplan-collision",
|
|
@@ -953,7 +807,7 @@ function cancelUpdate(p) {
|
|
|
953
807
|
async function chooseConflict(p, action) {
|
|
954
808
|
const options = [
|
|
955
809
|
{ value: "replace", label: "Replace", hint: "discard current content" },
|
|
956
|
-
{ value: "merge", label: "Merge", hint: "back up current content for /setup
|
|
810
|
+
{ value: "merge", label: "Merge", hint: "back up current content for /update-bearings-setup" },
|
|
957
811
|
...action.canSkip ? [{ value: "skip", label: "Skip", hint: "decline this template revision" }] : []
|
|
958
812
|
];
|
|
959
813
|
const selected = await p.select({
|
|
@@ -1010,14 +864,21 @@ async function confirmUpdatePlan(p, message) {
|
|
|
1010
864
|
|
|
1011
865
|
// src/commands/update.ts
|
|
1012
866
|
async function runUpdate(repoDir, flags, version, state) {
|
|
867
|
+
let normalizationChanged = false;
|
|
868
|
+
if (state.kind === "valid" && state.manifest.version === 2) {
|
|
869
|
+
const normalized = await normalizeSetup(repoDir, state.manifest);
|
|
870
|
+
normalizationChanged = !isDeepStrictEqual(normalized, state.manifest);
|
|
871
|
+
state = { kind: "valid", manifest: normalized };
|
|
872
|
+
if (normalized.setupPending) {
|
|
873
|
+
if (normalizationChanged) await saveManifest(repoDir, normalized);
|
|
874
|
+
return { manifest: normalized, report: pendingSetupReport(normalized) };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
1013
877
|
if (!process.stdin.isTTY) {
|
|
1014
878
|
throw new Error("Updating an initialized repository requires an interactive terminal.");
|
|
1015
879
|
}
|
|
1016
880
|
const migratedFromV1 = state.kind === "valid" && state.manifest.version === 1;
|
|
1017
881
|
const current = state.kind === "valid" ? migratedFromV1 ? await migrateV1(repoDir, state.manifest) : state.manifest : null;
|
|
1018
|
-
if (!migratedFromV1 && (current?.setupPending || current?.migrationReconciliations?.length || current?.files.some((file) => file.reconciliations?.length))) {
|
|
1019
|
-
throw new Error("Finish the pending /setup-repo handoff before running bearings init again.");
|
|
1020
|
-
}
|
|
1021
882
|
if (current && compareSemver(version, current.bearingsVersion) < 0) {
|
|
1022
883
|
throw new Error(`Refusing to downgrade bearings from ${current.bearingsVersion} to ${version}.`);
|
|
1023
884
|
}
|
|
@@ -1068,11 +929,11 @@ async function runUpdate(repoDir, flags, version, state) {
|
|
|
1068
929
|
const exposureUnchanged = current ? current.exposure === exposure : false;
|
|
1069
930
|
const noArtifactChanges = resolved.actions.every((a) => a.kind === "keep") && migrationPlan.actions.length === 0 && adapterActions.length === 0;
|
|
1070
931
|
const versionUnchanged = !!current && version === current.bearingsVersion;
|
|
1071
|
-
if (!reconstruction && !migratedFromV1 && noArtifactChanges && versionUnchanged && harnessesUnchanged && exposureUnchanged) {
|
|
932
|
+
if (!reconstruction && !migratedFromV1 && !normalizationChanged && noArtifactChanges && versionUnchanged && harnessesUnchanged && exposureUnchanged) {
|
|
1072
933
|
return { manifest: current, report: "bearings init: already up to date." };
|
|
1073
934
|
}
|
|
1074
|
-
const
|
|
1075
|
-
const setupRequired =
|
|
935
|
+
const tasks = uniqueSetupTasks([...resolved.tasks, ...reconciliationTasks({ files: [...resolved.files], migrationReconciliations: [...migrationPlan.reconciliations] })]);
|
|
936
|
+
const setupRequired = tasks.length > 0;
|
|
1076
937
|
const finalManifest = {
|
|
1077
938
|
version: 2,
|
|
1078
939
|
bearingsVersion: version,
|
|
@@ -1082,7 +943,9 @@ async function runUpdate(repoDir, flags, version, state) {
|
|
|
1082
943
|
setupPending: {
|
|
1083
944
|
kind: reconstruction ? "reconstruction" : "update",
|
|
1084
945
|
...current ? { fromVersion: current.bearingsVersion } : {},
|
|
1085
|
-
toVersion: version
|
|
946
|
+
toVersion: version,
|
|
947
|
+
tasks,
|
|
948
|
+
protectedSkills: await protectSkills(repoDir, tasks, resolved.actions, setupArtifacts({ files: [...resolved.files], migrationReconciliations: [...migrationPlan.reconciliations] }))
|
|
1086
949
|
}
|
|
1087
950
|
} : {},
|
|
1088
951
|
...migrationPlan.reconciliations.length > 0 ? { migrationReconciliations: [...migrationPlan.reconciliations] } : {},
|
|
@@ -1105,7 +968,7 @@ async function runUpdate(repoDir, flags, version, state) {
|
|
|
1105
968
|
adapters: adapterActions,
|
|
1106
969
|
manifest: finalManifest
|
|
1107
970
|
});
|
|
1108
|
-
return { manifest: finalManifest, report: renderUpdateReport({ ...summaryInput, setupRequired }) };
|
|
971
|
+
return { manifest: finalManifest, report: renderUpdateReport({ ...summaryInput, setupRequired, manifest: finalManifest }) };
|
|
1109
972
|
}
|
|
1110
973
|
export {
|
|
1111
974
|
runUpdate
|
package/package.json
CHANGED
package/templates/AGENTS.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# <agent: project name> - Agent Seed
|
|
2
2
|
|
|
3
|
-
## Setup Required
|
|
4
|
-
|
|
5
|
-
Until this section is removed, do not inspect, modify, or run anything in this repository except `/setup-repo`. For every other request, respond only: Run `/setup-repo` to finalize bearings, then retry your request.
|
|
6
|
-
|
|
7
3
|
## Project
|
|
8
4
|
|
|
9
5
|
<agent: fill during handoff — One sentence: problem, primary users, and outcome.>
|
|
@@ -22,4 +18,4 @@ Primary stack: <agent: fill during handoff — language/runtime/framework/platfo
|
|
|
22
18
|
|
|
23
19
|
## User-Facing Output
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
The user has ADHD and cannot comprehend long descriptions. Tailor all your responses to the user's reading ability.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Reconcile
|
|
2
|
+
description: Reconcile repository maps and C4 component source after meaningful drift, or only explicit map targets supplied by bearings setup
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# /refresh-repo-map
|
|
@@ -25,8 +25,25 @@ when the maps remain accurate.
|
|
|
25
25
|
| `docs/CODEBASE_MAP.md` | this command |
|
|
26
26
|
| `docs/diagrams/c4-component.puml` | this command |
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
## Target Scope
|
|
29
|
+
|
|
30
|
+
Ordinary developer-invoked refresh covers all four owned artifacts. In that
|
|
31
|
+
mode, create `docs/diagrams/` and `c4-component.puml` when absent.
|
|
32
|
+
|
|
33
|
+
Setup callers must supply an explicit list of target paths from their pending
|
|
34
|
+
`reason: map` tasks. Select only those paths from the ownership table. An empty
|
|
35
|
+
setup list means no refresh; a missing or inconsistent setup scope is a blocker,
|
|
36
|
+
not a request for the ordinary full set. One map task selects one artifact.
|
|
37
|
+
|
|
38
|
+
Read related repository evidence as needed, but edit or create only selected
|
|
39
|
+
targets, including their sync markers. Do not create missing C4 source when
|
|
40
|
+
it is outside the supplied scope. Apply supplied reconciliation choices through
|
|
41
|
+
this command while retaining the selected artifact's contract. Report related
|
|
42
|
+
out-of-scope drift without expanding setup's write scope.
|
|
43
|
+
|
|
44
|
+
The contracts, mode selection, evidence rules, and validation below apply per
|
|
45
|
+
selected artifact. Maintain source-only PlantUML: never render it and never
|
|
46
|
+
modify another diagram.
|
|
30
47
|
|
|
31
48
|
## Artifact Contracts
|
|
32
49
|
|
|
@@ -75,9 +92,10 @@ existing map wording.
|
|
|
75
92
|
## Baselines and Mode Selection
|
|
76
93
|
|
|
77
94
|
Each artifact records its own reconciliation baseline, so mode selection is
|
|
78
|
-
independent per artifact
|
|
95
|
+
independent per selected artifact; full reconciliation never expands the
|
|
96
|
+
selected target set:
|
|
79
97
|
|
|
80
|
-
- Full reconciliation when the artifact
|
|
98
|
+
- Full reconciliation when the selected artifact is missing, its
|
|
81
99
|
marker is missing, malformed, unavailable locally, or not an ancestor of
|
|
82
100
|
pre-command `HEAD`, the user requests a full refresh, or incremental
|
|
83
101
|
evidence reveals structural drift outside the expected impact closure.
|
|
@@ -95,8 +113,8 @@ independent per artifact:
|
|
|
95
113
|
5. Trace implementations and tests for critical flows and hazards.
|
|
96
114
|
6. Read accepted ADRs and confirmed domain/business intent that constrain
|
|
97
115
|
current code.
|
|
98
|
-
7. Reconcile the artifact's complete required output
|
|
99
|
-
model when its
|
|
116
|
+
7. Reconcile the selected artifact's complete required output. Reconcile the
|
|
117
|
+
C4 component model only when its path is selected.
|
|
100
118
|
8. Set that artifact's sync marker to the captured pre-command `HEAD` only
|
|
101
119
|
after its reconciliation succeeds.
|
|
102
120
|
|
|
@@ -116,8 +134,8 @@ its largest file.
|
|
|
116
134
|
relationships to establish impact closure.
|
|
117
135
|
5. Edit only the affected sections of that artifact and preserve unrelated
|
|
118
136
|
verified content.
|
|
119
|
-
6. Update the C4 model only when component boundaries
|
|
120
|
-
changed.
|
|
137
|
+
6. Update the C4 model only when its path is selected and component boundaries
|
|
138
|
+
or relationships changed.
|
|
121
139
|
7. Set that artifact's sync marker to the captured pre-command `HEAD` only
|
|
122
140
|
after its reconciliation succeeds.
|
|
123
141
|
8. Escalate that artifact to full reconciliation when impact closure cannot
|
|
@@ -127,10 +145,12 @@ its largest file.
|
|
|
127
145
|
|
|
128
146
|
Before reporting completion:
|
|
129
147
|
|
|
130
|
-
1. Validate every retained path, symbol, command, and relationship
|
|
131
|
-
the repository.
|
|
148
|
+
1. Validate every retained path, symbol, command, and relationship in each
|
|
149
|
+
selected artifact against the repository.
|
|
132
150
|
2. Run these probes in fresh context that receives only the draft
|
|
133
|
-
artifacts and the scenario, never the source-reading context
|
|
151
|
+
selected artifacts and the scenario, never the source-reading context.
|
|
152
|
+
Judge each probe against those artifacts' authority and required output;
|
|
153
|
+
do not require an unselected artifact's content:
|
|
134
154
|
- Explain: select a critical behavior; identify entry point, flow,
|
|
135
155
|
state/side effects, failure semantics, and targeted source.
|
|
136
156
|
- Feature: select a plausible extension; identify owner, extension seam,
|
|
@@ -138,7 +158,10 @@ Before reporting completion:
|
|
|
138
158
|
- Bug: select a plausible public symptom; narrow diagnosis to an owning
|
|
139
159
|
flow, likely files, and regression-test location.
|
|
140
160
|
3. Rerun any failed probe after adding the missing knowledge. A failed
|
|
141
|
-
|
|
161
|
+
probe blocks completion for the affected artifact.
|
|
162
|
+
|
|
163
|
+
Keep probe fixes within selected targets. Report missing out-of-scope knowledge
|
|
164
|
+
separately; probes do not authorize creating or refreshing other maps.
|
|
142
165
|
|
|
143
166
|
## Completion
|
|
144
167
|
|