bearings 0.5.3 → 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 -155
|
@@ -0,0 +1,1823 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/paths.ts
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, join, sep } from "path";
|
|
6
|
+
var CHECKLIST_PAYLOAD_PATH = ".agents/skills/checklist/previewer/data.generated.js";
|
|
7
|
+
function isChecklistPayload(path) {
|
|
8
|
+
return path === CHECKLIST_PAYLOAD_PATH || path.endsWith(`${sep}${CHECKLIST_PAYLOAD_PATH.split("/").join(sep)}`);
|
|
9
|
+
}
|
|
10
|
+
function templatesDir() {
|
|
11
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
12
|
+
}
|
|
13
|
+
var STARTER_SKILL_NAMES = [
|
|
14
|
+
"commit-convention",
|
|
15
|
+
"defer-work",
|
|
16
|
+
"resurface-deferred-work",
|
|
17
|
+
"recording-decisions"
|
|
18
|
+
];
|
|
19
|
+
var SKILLS = STARTER_SKILL_NAMES;
|
|
20
|
+
var MANUAL_TESTPLAN_FILES = [
|
|
21
|
+
"SKILL.md"
|
|
22
|
+
];
|
|
23
|
+
var ORCHESTRATE_FILES = [
|
|
24
|
+
"SKILL.md"
|
|
25
|
+
];
|
|
26
|
+
var FORGE_A_SKILL_FILES = [
|
|
27
|
+
"SKILL.md",
|
|
28
|
+
"templates/guardrail.md",
|
|
29
|
+
"templates/probe-set.json"
|
|
30
|
+
];
|
|
31
|
+
var CHECKLIST_FILES = [
|
|
32
|
+
"SKILL.md",
|
|
33
|
+
"schema.reference.json",
|
|
34
|
+
"scripts/validate.mjs",
|
|
35
|
+
"scripts/build.mjs",
|
|
36
|
+
"previewer/index.html",
|
|
37
|
+
"previewer/viewer.css",
|
|
38
|
+
"previewer/viewer.js"
|
|
39
|
+
];
|
|
40
|
+
function starterSkillNameFromTarget(targetPath) {
|
|
41
|
+
return STARTER_SKILL_NAMES.find((skillName) => targetPath === `.agents/skills/${skillName}/SKILL.md`);
|
|
42
|
+
}
|
|
43
|
+
function isStarterSkillTarget(targetPath) {
|
|
44
|
+
return starterSkillNameFromTarget(targetPath) !== void 0;
|
|
45
|
+
}
|
|
46
|
+
function skillBaselinePath(skillName) {
|
|
47
|
+
return `.agents/.bearings-baseline/skills/${skillName}/SKILL.md`;
|
|
48
|
+
}
|
|
49
|
+
function skillIncomingPath(skillName) {
|
|
50
|
+
return `.agents/.bearings-incoming/skills/${skillName}/SKILL.md`;
|
|
51
|
+
}
|
|
52
|
+
var SCAFFOLD = [
|
|
53
|
+
{ template: "AGENTS.md", target: "AGENTS.md", owner: "agent" },
|
|
54
|
+
{ template: "CLAUDE.md", target: "CLAUDE.md", owner: "bearings" },
|
|
55
|
+
{ template: "docs/DOMAIN.md", target: "docs/DOMAIN.md", owner: "agent" },
|
|
56
|
+
{ template: "docs/ARCHITECTURE.md", target: "docs/ARCHITECTURE.md", owner: "agent" },
|
|
57
|
+
{ template: "docs/CODEBASE_MAP.md", target: "docs/CODEBASE_MAP.md", owner: "agent" },
|
|
58
|
+
{ template: "docs/adr/INDEX.md", target: "docs/adr/INDEX.md", owner: "agent" },
|
|
59
|
+
{ template: "docs/adr/0000-template.md", target: "docs/adr/0000-template.md", owner: "bearings" },
|
|
60
|
+
{ template: "docs/deferred/INDEX.md", target: "docs/deferred/INDEX.md", owner: "agent" },
|
|
61
|
+
{ template: "agents/commands/refresh-repo-map.md", target: ".agents/commands/refresh-repo-map.md", owner: "bearings" },
|
|
62
|
+
{ template: "agents/commands/update-bearings-setup.md", target: ".agents/commands/update-bearings-setup.md", owner: "bearings" },
|
|
63
|
+
...SKILLS.map((skill) => ({
|
|
64
|
+
template: `agents/skills/${skill}/SKILL.md`,
|
|
65
|
+
target: `.agents/skills/${skill}/SKILL.md`,
|
|
66
|
+
owner: "bearings"
|
|
67
|
+
})),
|
|
68
|
+
...MANUAL_TESTPLAN_FILES.map((file) => ({
|
|
69
|
+
template: `agents/skills/manual-testplan/${file}`,
|
|
70
|
+
target: `.agents/skills/manual-testplan/${file}`,
|
|
71
|
+
owner: "bearings"
|
|
72
|
+
})),
|
|
73
|
+
...ORCHESTRATE_FILES.map((file) => ({
|
|
74
|
+
template: `agents/skills/orchestrate/${file}`,
|
|
75
|
+
target: `.agents/skills/orchestrate/${file}`,
|
|
76
|
+
owner: "bearings"
|
|
77
|
+
})),
|
|
78
|
+
...FORGE_A_SKILL_FILES.map((file) => ({
|
|
79
|
+
template: `agents/skills/forge-a-skill/${file}`,
|
|
80
|
+
target: `.agents/skills/forge-a-skill/${file}`,
|
|
81
|
+
owner: "bearings"
|
|
82
|
+
})),
|
|
83
|
+
...CHECKLIST_FILES.map((file) => ({
|
|
84
|
+
template: `agents/skills/checklist/${file}`,
|
|
85
|
+
target: `.agents/skills/checklist/${file}`,
|
|
86
|
+
owner: "bearings"
|
|
87
|
+
}))
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// src/manifest.ts
|
|
91
|
+
import { createHash } from "crypto";
|
|
92
|
+
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
93
|
+
import { join as join2 } from "path";
|
|
94
|
+
function sha256(content) {
|
|
95
|
+
return "sha256:" + createHash("sha256").update(content).digest("hex");
|
|
96
|
+
}
|
|
97
|
+
function manifestPath(repoDir) {
|
|
98
|
+
return join2(repoDir, ".agents", "bearings.json");
|
|
99
|
+
}
|
|
100
|
+
async function saveManifest(repoDir, m) {
|
|
101
|
+
await mkdir(join2(repoDir, ".agents"), { recursive: true });
|
|
102
|
+
await writeFile(manifestPath(repoDir), JSON.stringify(m, null, 2) + "\n");
|
|
103
|
+
}
|
|
104
|
+
var HASH = /^sha256:[0-9a-f]{64}$/;
|
|
105
|
+
var HARNESSES = ["claude", "opencode"];
|
|
106
|
+
var EXPOSURES = ["symlink", "copy"];
|
|
107
|
+
var OWNERS = ["bearings", "agent"];
|
|
108
|
+
var RECONCILIATION_REASONS = ["init-collision", "update-merge", "skill-update"];
|
|
109
|
+
var SETUP_PENDING_KINDS = ["init", "update", "reconstruction"];
|
|
110
|
+
function fail(message) {
|
|
111
|
+
throw new Error(message);
|
|
112
|
+
}
|
|
113
|
+
function isPlainObject(value) {
|
|
114
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
}
|
|
116
|
+
function isSafeManifestPath(value) {
|
|
117
|
+
if (typeof value !== "string" || !value || /[\\\x00-\x1f]/.test(value)) return false;
|
|
118
|
+
if (value.startsWith("/") || /^[A-Za-z]:\//.test(value) || value.split("/").some((part) => !part || part === "." || part === "..")) return false;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
function isNonEmptyString(value) {
|
|
122
|
+
return typeof value === "string" && value.length > 0;
|
|
123
|
+
}
|
|
124
|
+
function isHash(value) {
|
|
125
|
+
return typeof value === "string" && HASH.test(value);
|
|
126
|
+
}
|
|
127
|
+
function isHarnessArray(value) {
|
|
128
|
+
return Array.isArray(value) && value.every((h) => HARNESSES.includes(h));
|
|
129
|
+
}
|
|
130
|
+
function isExposure(value) {
|
|
131
|
+
return typeof value === "string" && EXPOSURES.includes(value);
|
|
132
|
+
}
|
|
133
|
+
function isOwner(value) {
|
|
134
|
+
return typeof value === "string" && OWNERS.includes(value);
|
|
135
|
+
}
|
|
136
|
+
function validateReconciliation(value, context) {
|
|
137
|
+
if (!isPlainObject(value)) fail(`${context} must be an object`);
|
|
138
|
+
const { backup, reason, sourceHash, incomingTemplateVersion, incomingHash } = value;
|
|
139
|
+
if (!isSafeManifestPath(backup)) fail(`${context}.backup is unsafe: ${String(backup)}`);
|
|
140
|
+
if (typeof reason !== "string" || !RECONCILIATION_REASONS.includes(reason)) {
|
|
141
|
+
fail(`${context}.reason must be "init-collision", "update-merge", or "skill-update"`);
|
|
142
|
+
}
|
|
143
|
+
if (reason === "skill-update") {
|
|
144
|
+
const { basePath, incomingPath, baseHash } = value;
|
|
145
|
+
if (!isSafeManifestPath(basePath)) fail(`${context}.basePath is unsafe: ${String(basePath)}`);
|
|
146
|
+
if (!isSafeManifestPath(incomingPath)) fail(`${context}.incomingPath is unsafe: ${String(incomingPath)}`);
|
|
147
|
+
if (!isHash(baseHash)) fail(`${context}.baseHash must match sha256 format`);
|
|
148
|
+
if (!isHash(sourceHash)) fail(`${context}.sourceHash must match sha256 format`);
|
|
149
|
+
if (!isNonEmptyString(incomingTemplateVersion)) fail(`${context}.incomingTemplateVersion must be a string`);
|
|
150
|
+
if (!isHash(incomingHash)) fail(`${context}.incomingHash must match sha256 format`);
|
|
151
|
+
return { backup, reason, basePath, incomingPath, baseHash, sourceHash, incomingTemplateVersion, incomingHash };
|
|
152
|
+
}
|
|
153
|
+
if (!isHash(sourceHash)) fail(`${context}.sourceHash must match sha256 format`);
|
|
154
|
+
if (!isNonEmptyString(incomingTemplateVersion)) fail(`${context}.incomingTemplateVersion must be a string`);
|
|
155
|
+
if (!isHash(incomingHash)) fail(`${context}.incomingHash must match sha256 format`);
|
|
156
|
+
return {
|
|
157
|
+
backup,
|
|
158
|
+
reason,
|
|
159
|
+
sourceHash,
|
|
160
|
+
incomingTemplateVersion,
|
|
161
|
+
incomingHash
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function validateFileV1(value, index) {
|
|
165
|
+
const context = `files[${index}]`;
|
|
166
|
+
if (!isPlainObject(value)) fail(`${context} must be an object`);
|
|
167
|
+
const { path, template, templateVersion, hash, owner, backup } = value;
|
|
168
|
+
if (!isSafeManifestPath(path)) fail(`${context}.path is unsafe: ${String(path)}`);
|
|
169
|
+
if (!isNonEmptyString(template)) fail(`${context}.template must be a string`);
|
|
170
|
+
if (!isNonEmptyString(templateVersion)) fail(`${context}.templateVersion must be a string`);
|
|
171
|
+
if (!isHash(hash)) fail(`${context}.hash must match sha256 format`);
|
|
172
|
+
if (!isOwner(owner)) fail(`${context}.owner must be "bearings" or "agent"`);
|
|
173
|
+
if (backup !== void 0 && !isSafeManifestPath(backup)) fail(`${context}.backup is unsafe: ${String(backup)}`);
|
|
174
|
+
return {
|
|
175
|
+
path,
|
|
176
|
+
template,
|
|
177
|
+
templateVersion,
|
|
178
|
+
hash,
|
|
179
|
+
owner,
|
|
180
|
+
...backup !== void 0 ? { backup } : {}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function validateFileV2(value, index) {
|
|
184
|
+
const context = `files[${index}]`;
|
|
185
|
+
if (!isPlainObject(value)) fail(`${context} must be an object`);
|
|
186
|
+
const { path, template, templateVersion, hash, owner, lastTemplateHash, retired, skippedTemplate, reconciliations } = value;
|
|
187
|
+
if (!isSafeManifestPath(path)) fail(`${context}.path is unsafe: ${String(path)}`);
|
|
188
|
+
if (!isNonEmptyString(template)) fail(`${context}.template must be a string`);
|
|
189
|
+
if (!isNonEmptyString(templateVersion)) fail(`${context}.templateVersion must be a string`);
|
|
190
|
+
if (!isHash(hash)) fail(`${context}.hash must match sha256 format`);
|
|
191
|
+
if (!isOwner(owner)) fail(`${context}.owner must be "bearings" or "agent"`);
|
|
192
|
+
if (lastTemplateHash !== void 0 && !isHash(lastTemplateHash)) {
|
|
193
|
+
fail(`${context}.lastTemplateHash must match sha256 format`);
|
|
194
|
+
}
|
|
195
|
+
if (retired !== void 0 && retired !== true) fail(`${context}.retired must be true when present`);
|
|
196
|
+
let normalizedSkippedTemplate;
|
|
197
|
+
if (skippedTemplate !== void 0) {
|
|
198
|
+
if (!isPlainObject(skippedTemplate)) fail(`${context}.skippedTemplate must be an object`);
|
|
199
|
+
const { templateVersion: skippedVersion, hash: skippedHash } = skippedTemplate;
|
|
200
|
+
if (!isNonEmptyString(skippedVersion)) fail(`${context}.skippedTemplate.templateVersion must be a string`);
|
|
201
|
+
if (!isHash(skippedHash)) fail(`${context}.skippedTemplate.hash must match sha256 format`);
|
|
202
|
+
normalizedSkippedTemplate = { templateVersion: skippedVersion, hash: skippedHash };
|
|
203
|
+
}
|
|
204
|
+
let normalizedReconciliations;
|
|
205
|
+
if (reconciliations !== void 0) {
|
|
206
|
+
if (!Array.isArray(reconciliations)) fail(`${context}.reconciliations must be an array`);
|
|
207
|
+
normalizedReconciliations = reconciliations.map((entry, reconciliationIndex) => validateReconciliation(entry, `${context}.reconciliations[${reconciliationIndex}]`));
|
|
208
|
+
}
|
|
209
|
+
if (retired === true && !normalizedReconciliations?.length) {
|
|
210
|
+
fail(`${context}.retired requires at least one reconciliation`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
path,
|
|
214
|
+
template,
|
|
215
|
+
templateVersion,
|
|
216
|
+
hash,
|
|
217
|
+
owner,
|
|
218
|
+
...lastTemplateHash !== void 0 ? { lastTemplateHash } : {},
|
|
219
|
+
...retired === true ? { retired: true } : {},
|
|
220
|
+
...normalizedSkippedTemplate ? { skippedTemplate: normalizedSkippedTemplate } : {},
|
|
221
|
+
...normalizedReconciliations ? { reconciliations: normalizedReconciliations } : {}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function assertUniquePaths(paths) {
|
|
225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
226
|
+
for (const path of paths) {
|
|
227
|
+
if (seen.has(path)) fail(`duplicate managed path: ${path}`);
|
|
228
|
+
seen.add(path);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function validateArtifacts(value, context) {
|
|
232
|
+
if (value === void 0) return void 0;
|
|
233
|
+
if (!Array.isArray(value)) fail(`${context} must be an array`);
|
|
234
|
+
const artifacts = value.map((artifact, index) => {
|
|
235
|
+
const item = `${context}[${index}]`;
|
|
236
|
+
if (!isPlainObject(artifact) || !isSafeManifestPath(artifact.path)) fail(`${item}.path is unsafe`);
|
|
237
|
+
if (!isHash(artifact.hash)) fail(`${item}.hash is invalid`);
|
|
238
|
+
if (!["backup", "incoming", "base"].includes(String(artifact.kind))) fail(`${item}.kind is invalid`);
|
|
239
|
+
return { path: artifact.path, hash: artifact.hash, kind: artifact.kind };
|
|
240
|
+
});
|
|
241
|
+
assertUniquePaths(artifacts.map((artifact) => artifact.path));
|
|
242
|
+
return artifacts;
|
|
243
|
+
}
|
|
244
|
+
function validateTaskValidation(value, context) {
|
|
245
|
+
if (value === void 0) return void 0;
|
|
246
|
+
if (!isPlainObject(value)) fail(`${context} must be an object`);
|
|
247
|
+
if (value.kind !== "checklist-repair") fail(`${context}.kind is invalid`);
|
|
248
|
+
if (!isSafeManifestPath(value.source)) fail(`${context}.source is unsafe`);
|
|
249
|
+
return { kind: value.kind, source: value.source };
|
|
250
|
+
}
|
|
251
|
+
function validateTask(value, context) {
|
|
252
|
+
if (!isPlainObject(value) || !isSafeManifestPath(value.path)) fail(`${context} path is unsafe`);
|
|
253
|
+
if (!["tailor", "map", "reconcile"].includes(String(value.reason))) fail(`${context} reason is invalid`);
|
|
254
|
+
let acknowledged;
|
|
255
|
+
if (value.acknowledged !== void 0) {
|
|
256
|
+
if (!isPlainObject(value.acknowledged)) fail(`${context}.acknowledged must be an object`);
|
|
257
|
+
const artifacts = validateArtifacts(value.acknowledged.artifacts, `${context}.acknowledged.artifacts`);
|
|
258
|
+
const decision = value.acknowledged.decision;
|
|
259
|
+
if (decision !== void 0 && decision !== "keep-local" && decision !== "discard") fail(`${context}.acknowledged.decision is invalid`);
|
|
260
|
+
const validation = validateTaskValidation(value.acknowledged.validation, `${context}.acknowledged.validation`);
|
|
261
|
+
acknowledged = {
|
|
262
|
+
...artifacts ? { artifacts } : {},
|
|
263
|
+
...decision ? { decision } : {},
|
|
264
|
+
...validation ? { validation } : {}
|
|
265
|
+
};
|
|
266
|
+
if (!acknowledged.artifacts?.length && !acknowledged.decision && !acknowledged.validation) {
|
|
267
|
+
fail(`${context}.acknowledged must record an artifact, decision, or validation`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
path: value.path,
|
|
272
|
+
reason: value.reason,
|
|
273
|
+
...acknowledged ? { acknowledged } : {}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function validateCompletedTask(value, context) {
|
|
277
|
+
if (!isPlainObject(value) || !isSafeManifestPath(value.path)) fail(`${context} path is unsafe`);
|
|
278
|
+
if (!["tailor", "map", "reconcile"].includes(String(value.reason))) fail(`${context} reason is invalid`);
|
|
279
|
+
const { outputHash, decision } = value;
|
|
280
|
+
if (outputHash !== void 0 && !isHash(outputHash)) fail(`${context}.outputHash is invalid`);
|
|
281
|
+
if (decision !== void 0 && decision !== "keep-local" && decision !== "discard") fail(`${context}.decision is invalid`);
|
|
282
|
+
if (decision === "discard" ? outputHash !== void 0 : outputHash === void 0) {
|
|
283
|
+
fail(`${context} must record either outputHash or discard`);
|
|
284
|
+
}
|
|
285
|
+
const artifacts = validateArtifacts(value.artifacts, `${context}.artifacts`);
|
|
286
|
+
const validation = validateTaskValidation(value.validation, `${context}.validation`);
|
|
287
|
+
return {
|
|
288
|
+
path: value.path,
|
|
289
|
+
reason: value.reason,
|
|
290
|
+
...outputHash ? { outputHash } : {},
|
|
291
|
+
...decision ? { decision } : {},
|
|
292
|
+
...artifacts?.length ? { artifacts } : {},
|
|
293
|
+
...validation ? { validation } : {}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function validateSetupHeader(value, context) {
|
|
297
|
+
const { kind, fromVersion, toVersion } = value;
|
|
298
|
+
if (typeof kind !== "string" || !SETUP_PENDING_KINDS.includes(kind)) {
|
|
299
|
+
fail(`${context}.kind must be "init", "update", or "reconstruction"`);
|
|
300
|
+
}
|
|
301
|
+
if (fromVersion !== void 0 && !isNonEmptyString(fromVersion)) fail(`${context}.fromVersion must be a string`);
|
|
302
|
+
if (!isNonEmptyString(toVersion)) fail(`${context}.toVersion must be a string`);
|
|
303
|
+
return { kind, ...fromVersion !== void 0 ? { fromVersion } : {}, toVersion };
|
|
304
|
+
}
|
|
305
|
+
function validateSetupPending(value) {
|
|
306
|
+
if (!isPlainObject(value)) fail("setupPending must be an object");
|
|
307
|
+
const header = validateSetupHeader(value, "setupPending");
|
|
308
|
+
const { tasks, completedTasks, protectedSkills } = value;
|
|
309
|
+
let normalizedTasks;
|
|
310
|
+
if (tasks !== void 0) {
|
|
311
|
+
if (!Array.isArray(tasks)) fail("setupPending.tasks must be an array");
|
|
312
|
+
normalizedTasks = tasks.map((task, index) => validateTask(task, `setupPending.tasks[${index}]`));
|
|
313
|
+
assertUniquePaths(normalizedTasks.map((task) => task.path));
|
|
314
|
+
}
|
|
315
|
+
let normalizedCompleted;
|
|
316
|
+
if (completedTasks !== void 0) {
|
|
317
|
+
if (!Array.isArray(completedTasks)) fail("setupPending.completedTasks must be an array");
|
|
318
|
+
normalizedCompleted = completedTasks.map((task, index) => validateCompletedTask(task, `setupPending.completedTasks[${index}]`));
|
|
319
|
+
assertUniquePaths(normalizedCompleted.map((task) => task.path));
|
|
320
|
+
if (normalizedCompleted.some((task) => normalizedTasks?.some((pending) => pending.path === task.path))) {
|
|
321
|
+
fail("setup task cannot also be completed");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
let normalizedProtected;
|
|
325
|
+
if (protectedSkills !== void 0) {
|
|
326
|
+
if (!Array.isArray(protectedSkills)) fail("setupPending.protectedSkills must be an array");
|
|
327
|
+
normalizedProtected = protectedSkills.map((file) => {
|
|
328
|
+
if (!isPlainObject(file) || !isSafeManifestPath(file.path) || !file.path.startsWith(".agents/skills/")) {
|
|
329
|
+
fail("setupPending.protectedSkills path is unsafe");
|
|
330
|
+
}
|
|
331
|
+
if (!isHash(file.hash)) fail("setupPending.protectedSkills hash is invalid");
|
|
332
|
+
if (normalizedTasks?.some((task) => task.path === file.path)) fail("protected skill is also a setup task");
|
|
333
|
+
return { path: file.path, hash: file.hash };
|
|
334
|
+
});
|
|
335
|
+
assertUniquePaths(normalizedProtected.map((file) => file.path));
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
...header,
|
|
339
|
+
...normalizedTasks ? { tasks: normalizedTasks } : {},
|
|
340
|
+
...normalizedCompleted ? { completedTasks: normalizedCompleted } : {},
|
|
341
|
+
...normalizedProtected ? { protectedSkills: normalizedProtected } : {}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function validateCompletedSetup(value) {
|
|
345
|
+
if (!isPlainObject(value)) fail("completedSetup must be an object");
|
|
346
|
+
const header = validateSetupHeader(value, "completedSetup");
|
|
347
|
+
if (!Array.isArray(value.tasks) || value.tasks.length === 0) fail("completedSetup.tasks must be a non-empty array");
|
|
348
|
+
const tasks = value.tasks.map((task, index) => validateCompletedTask(task, `completedSetup.tasks[${index}]`));
|
|
349
|
+
assertUniquePaths(tasks.map((task) => task.path));
|
|
350
|
+
return { ...header, tasks };
|
|
351
|
+
}
|
|
352
|
+
function validateMigrationReconciliation(value, index) {
|
|
353
|
+
const context = `migrationReconciliations[${index}]`;
|
|
354
|
+
if (!isPlainObject(value)) fail(`${context} must be an object`);
|
|
355
|
+
const { kind, source, target, backup, sourceHash, incomingHash } = value;
|
|
356
|
+
if (kind !== "testplan-collision" && kind !== "invalid-testplan") {
|
|
357
|
+
fail(`${context}.kind must be "testplan-collision" or "invalid-testplan"`);
|
|
358
|
+
}
|
|
359
|
+
if (!isSafeManifestPath(source)) fail(`${context}.source is unsafe: ${String(source)}`);
|
|
360
|
+
if (!isSafeManifestPath(backup)) fail(`${context}.backup is unsafe: ${String(backup)}`);
|
|
361
|
+
if (!isHash(sourceHash)) fail(`${context}.sourceHash must match sha256 format`);
|
|
362
|
+
if (kind === "testplan-collision") {
|
|
363
|
+
if (!isSafeManifestPath(target)) fail(`${context}.target is unsafe: ${String(target)}`);
|
|
364
|
+
if (!isHash(incomingHash)) fail(`${context}.incomingHash must match sha256 format`);
|
|
365
|
+
return { kind, source, target, backup, sourceHash, incomingHash };
|
|
366
|
+
}
|
|
367
|
+
if (target !== void 0 && !isSafeManifestPath(target)) fail(`${context}.target is unsafe: ${String(target)}`);
|
|
368
|
+
return { kind, source, ...target !== void 0 ? { target } : {}, backup, sourceHash };
|
|
369
|
+
}
|
|
370
|
+
function parseManifest(raw) {
|
|
371
|
+
if (!isPlainObject(raw)) fail("manifest must be an object");
|
|
372
|
+
const { version, bearingsVersion, harnesses, exposure, files } = raw;
|
|
373
|
+
if (!isNonEmptyString(bearingsVersion)) fail("bearingsVersion must be a string");
|
|
374
|
+
if (!isHarnessArray(harnesses)) fail('harnesses must contain only "claude" or "opencode"');
|
|
375
|
+
if (!isExposure(exposure)) fail('exposure must be "symlink" or "copy"');
|
|
376
|
+
if (!Array.isArray(files)) fail("files must be an array");
|
|
377
|
+
if (version === 1) {
|
|
378
|
+
const validatedFiles = files.map((file, index) => validateFileV1(file, index));
|
|
379
|
+
assertUniquePaths(validatedFiles.map((file) => file.path));
|
|
380
|
+
return { version: 1, bearingsVersion, harnesses, exposure, files: validatedFiles };
|
|
381
|
+
}
|
|
382
|
+
if (version === 2) {
|
|
383
|
+
const validatedFiles = files.map((file, index) => validateFileV2(file, index));
|
|
384
|
+
assertUniquePaths(validatedFiles.map((file) => file.path));
|
|
385
|
+
const { setupPending, completedSetup, migrationReconciliations } = raw;
|
|
386
|
+
if (setupPending !== void 0 && completedSetup !== void 0) fail("setupPending and completedSetup cannot coexist");
|
|
387
|
+
if (migrationReconciliations !== void 0 && !Array.isArray(migrationReconciliations)) {
|
|
388
|
+
fail("migrationReconciliations must be an array");
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
version: 2,
|
|
392
|
+
bearingsVersion,
|
|
393
|
+
harnesses,
|
|
394
|
+
exposure,
|
|
395
|
+
...setupPending !== void 0 ? { setupPending: validateSetupPending(setupPending) } : {},
|
|
396
|
+
...completedSetup !== void 0 ? { completedSetup: validateCompletedSetup(completedSetup) } : {},
|
|
397
|
+
...migrationReconciliations !== void 0 ? {
|
|
398
|
+
migrationReconciliations: migrationReconciliations.map(validateMigrationReconciliation)
|
|
399
|
+
} : {},
|
|
400
|
+
files: validatedFiles
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
fail(`unsupported manifest version: ${String(version)}`);
|
|
404
|
+
}
|
|
405
|
+
async function inspectManifest(repoDir) {
|
|
406
|
+
let raw;
|
|
407
|
+
try {
|
|
408
|
+
raw = await readFile(manifestPath(repoDir), "utf8");
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error.code === "ENOENT") return { kind: "absent" };
|
|
411
|
+
return { kind: "invalid", message: error.message };
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
return { kind: "valid", manifest: parseManifest(JSON.parse(raw)) };
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return { kind: "invalid", message: error.message };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function requireValidManifest(repoDir) {
|
|
420
|
+
const state = await inspectManifest(repoDir);
|
|
421
|
+
if (state.kind === "absent") fail("No bearings manifest found.");
|
|
422
|
+
if (state.kind === "invalid") fail(`Invalid manifest: ${state.message}`);
|
|
423
|
+
return state.manifest;
|
|
424
|
+
}
|
|
425
|
+
async function migrateV1(repoDir, manifest) {
|
|
426
|
+
const files = await Promise.all(manifest.files.map(async (file) => {
|
|
427
|
+
const backupExists = file.backup ? await access(join2(repoDir, file.backup)).then(() => true, () => false) : false;
|
|
428
|
+
const reconciliation = backupExists && file.backup ? {
|
|
429
|
+
backup: file.backup,
|
|
430
|
+
reason: "init-collision",
|
|
431
|
+
sourceHash: sha256(await readFile(join2(repoDir, file.backup), "utf8")),
|
|
432
|
+
incomingTemplateVersion: file.templateVersion,
|
|
433
|
+
incomingHash: file.hash
|
|
434
|
+
} : void 0;
|
|
435
|
+
return {
|
|
436
|
+
path: file.path,
|
|
437
|
+
template: file.template,
|
|
438
|
+
templateVersion: file.templateVersion,
|
|
439
|
+
hash: file.hash,
|
|
440
|
+
owner: file.owner,
|
|
441
|
+
...reconciliation ? { reconciliations: [reconciliation] } : {}
|
|
442
|
+
};
|
|
443
|
+
}));
|
|
444
|
+
return { version: 2, bearingsVersion: manifest.bearingsVersion, harnesses: manifest.harnesses, exposure: manifest.exposure, files };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/setup-journal.ts
|
|
448
|
+
import { lstat as lstat3, readFile as readFile3, readdir as readdir2, realpath, stat as stat2 } from "fs/promises";
|
|
449
|
+
import { join as join5 } from "path";
|
|
450
|
+
import { isDeepStrictEqual } from "util";
|
|
451
|
+
|
|
452
|
+
// src/setup-checklist-repair.ts
|
|
453
|
+
import { lstat } from "fs/promises";
|
|
454
|
+
import { join as join3 } from "path";
|
|
455
|
+
import { pathToFileURL } from "url";
|
|
456
|
+
async function validateChecklistRepair(repoDir, target, validation, content) {
|
|
457
|
+
const validator = await import(pathToFileURL(join3(templatesDir(), "agents/skills/checklist/scripts/validate.mjs")).href);
|
|
458
|
+
let result;
|
|
459
|
+
try {
|
|
460
|
+
result = validator.validateChecklist(JSON.parse(content));
|
|
461
|
+
} catch {
|
|
462
|
+
throw new Error(`Invalid repaired checklist ${target}: invalid JSON`);
|
|
463
|
+
}
|
|
464
|
+
if (result.length) throw new Error(`Invalid repaired checklist ${target}: ${result.join("; ")}`);
|
|
465
|
+
if (await lstat(join3(repoDir, validation.source)).catch(() => null)) {
|
|
466
|
+
throw new Error(`Legacy source still needs resolution: ${validation.source}. Preserve its content in ${target}, then remove the resolved source.`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/verifier.ts
|
|
471
|
+
import { access as access2, lstat as lstat2, readFile as readFile2, readdir, readlink, stat } from "fs/promises";
|
|
472
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
473
|
+
var KINDS = ["skills", "commands"];
|
|
474
|
+
var PLACEHOLDER = /<agent:[^>\n]*>/;
|
|
475
|
+
async function exists(p) {
|
|
476
|
+
try {
|
|
477
|
+
await access2(p);
|
|
478
|
+
return true;
|
|
479
|
+
} catch {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function verify(repoDir) {
|
|
484
|
+
const failures = [];
|
|
485
|
+
const warnings = [];
|
|
486
|
+
const missingSkillBaselines = /* @__PURE__ */ new Set();
|
|
487
|
+
async function warnMissingSkillBaseline(baseline) {
|
|
488
|
+
if (missingSkillBaselines.has(baseline) || await exists(join4(repoDir, baseline))) return;
|
|
489
|
+
missingSkillBaselines.add(baseline);
|
|
490
|
+
warnings.push({ code: "missing-skill-baseline", path: baseline, message: "use /update-bearings-setup for a scoped skill baseline repair" });
|
|
491
|
+
}
|
|
492
|
+
const state = await inspectManifest(repoDir);
|
|
493
|
+
if (state.kind === "absent") {
|
|
494
|
+
return { failures: [{ code: "no-manifest", path: ".agents/bearings.json", message: "run bearings init first" }], warnings };
|
|
495
|
+
}
|
|
496
|
+
if (state.kind === "invalid") {
|
|
497
|
+
return { failures: [{ code: "invalid-manifest", path: ".agents/bearings.json", message: state.message }], warnings };
|
|
498
|
+
}
|
|
499
|
+
const m = state.manifest;
|
|
500
|
+
const setup = m.version === 2 && m.setupPending?.kind === "init" ? "bearings init" : "/update-bearings-setup";
|
|
501
|
+
if ("setupPending" in m && m.setupPending) {
|
|
502
|
+
warnings.push({ code: "setup-pending", path: ".agents/bearings.json", message: `run bearings init to resume pending setup${m.setupPending.tasks?.length ? ": " + m.setupPending.tasks.map((task) => task.path).join(", ") : ""}` });
|
|
503
|
+
}
|
|
504
|
+
for (const f of m.files) {
|
|
505
|
+
const retired = "retired" in f && f.retired === true;
|
|
506
|
+
if (!retired) {
|
|
507
|
+
const abs = join4(repoDir, f.path);
|
|
508
|
+
if (!await exists(abs)) {
|
|
509
|
+
failures.push({ code: "missing-file", path: f.path, message: "managed file deleted" });
|
|
510
|
+
} else {
|
|
511
|
+
const content = await readFile2(abs, "utf8");
|
|
512
|
+
if ((f.owner === "agent" || isStarterSkillTarget(f.path)) && PLACEHOLDER.test(content)) {
|
|
513
|
+
warnings.push({ code: "unfilled-placeholder", path: f.path, message: `fill through ${setup} when this path is in scope` });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!retired && f.owner === "agent" && isStarterSkillTarget(f.path)) {
|
|
518
|
+
const skillName = starterSkillNameFromTarget(f.path);
|
|
519
|
+
await warnMissingSkillBaseline(skillBaselinePath(skillName));
|
|
520
|
+
}
|
|
521
|
+
if ("backup" in f && f.backup && await exists(join4(repoDir, f.backup))) {
|
|
522
|
+
warnings.push({ code: "unreviewed-backup", path: f.backup, message: `review during ${setup}; completion CLI handles cleanup` });
|
|
523
|
+
}
|
|
524
|
+
if ("reconciliations" in f && f.reconciliations) {
|
|
525
|
+
for (const r of f.reconciliations) {
|
|
526
|
+
if (r.reason === "skill-update") await warnMissingSkillBaseline(r.basePath);
|
|
527
|
+
if (await exists(join4(repoDir, r.backup))) {
|
|
528
|
+
warnings.push({ code: "pending-reconciliation", path: r.backup, message: `review during ${setup}; completion CLI handles cleanup` });
|
|
529
|
+
}
|
|
530
|
+
if (r.reason === "skill-update" && await exists(join4(repoDir, r.incomingPath))) {
|
|
531
|
+
warnings.push({ code: "pending-reconciliation", path: r.incomingPath, message: `review during ${setup}; completion CLI handles cleanup` });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if ("migrationReconciliations" in m && m.migrationReconciliations) {
|
|
537
|
+
for (const reconciliation of m.migrationReconciliations) {
|
|
538
|
+
if (await exists(join4(repoDir, reconciliation.backup))) {
|
|
539
|
+
warnings.push({
|
|
540
|
+
code: "pending-reconciliation",
|
|
541
|
+
path: reconciliation.backup,
|
|
542
|
+
message: reconciliation.kind === "invalid-testplan" ? "repair with the checklist skill during /update-bearings-setup; completion CLI handles cleanup" : "review during /update-bearings-setup; completion CLI handles cleanup"
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const kind of KINDS) {
|
|
548
|
+
const srcDir = join4(repoDir, ".agents", kind);
|
|
549
|
+
let entries = [];
|
|
550
|
+
try {
|
|
551
|
+
entries = await readdir(srcDir);
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
for (const h of m.harnesses) {
|
|
555
|
+
for (const name of entries) {
|
|
556
|
+
const exposurePath = join4(`.${h}`, kind, name);
|
|
557
|
+
const dst = join4(repoDir, `.${h}`, kind, name);
|
|
558
|
+
const l = await lstat2(dst).catch(() => null);
|
|
559
|
+
if (!l) {
|
|
560
|
+
failures.push({ code: "missing-exposure", path: exposurePath, message: `not exposed to ${h}` });
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (l.isSymbolicLink()) {
|
|
564
|
+
if (!await stat(dst).catch(() => null)) {
|
|
565
|
+
failures.push({ code: "broken-symlink", path: exposurePath, message: "symlink target missing" });
|
|
566
|
+
} else {
|
|
567
|
+
const target = await readlink(dst);
|
|
568
|
+
if (resolve(dirname2(dst), target) !== resolve(srcDir, name)) {
|
|
569
|
+
failures.push({ code: "missing-exposure", path: exposurePath, message: `not exposed to ${h}` });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} else if (m.exposure === "symlink") {
|
|
573
|
+
failures.push({ code: "missing-exposure", path: exposurePath, message: `not exposed to ${h}` });
|
|
574
|
+
} else {
|
|
575
|
+
const src = join4(srcDir, name);
|
|
576
|
+
if (await copyDrifted(src, dst)) {
|
|
577
|
+
failures.push({ code: "copy-drift", path: exposurePath, message: "diverged from .agents source" });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { failures, warnings };
|
|
584
|
+
}
|
|
585
|
+
async function copyDrifted(src, dst) {
|
|
586
|
+
const s = await stat(src);
|
|
587
|
+
if (s.isDirectory()) {
|
|
588
|
+
const [srcChildren, dstChildren] = (await Promise.all([
|
|
589
|
+
readdir(src),
|
|
590
|
+
readdir(dst).catch(() => null)
|
|
591
|
+
])).map((names) => names?.filter((name) => !isChecklistPayload(join4(src, name))) ?? null);
|
|
592
|
+
if (srcChildren === null) return true;
|
|
593
|
+
if (dstChildren === null) return true;
|
|
594
|
+
const srcNames = new Set(srcChildren);
|
|
595
|
+
for (const child of dstChildren) {
|
|
596
|
+
if (!srcNames.has(child)) return true;
|
|
597
|
+
}
|
|
598
|
+
for (const child of srcChildren) {
|
|
599
|
+
if (await copyDrifted(join4(src, child), join4(dst, child))) return true;
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
const [a, b] = await Promise.all([
|
|
604
|
+
readFile2(src, "utf8"),
|
|
605
|
+
readFile2(dst, "utf8").catch(() => null)
|
|
606
|
+
]);
|
|
607
|
+
return b === null || sha256(a) !== sha256(b);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/setup-journal.ts
|
|
611
|
+
var C4_COMPONENT_PATH = "docs/diagrams/c4-component.puml";
|
|
612
|
+
var MAP_PATHS = /* @__PURE__ */ new Set([
|
|
613
|
+
...SCAFFOLD.filter((entry) => ["docs/DOMAIN.md", "docs/ARCHITECTURE.md", "docs/CODEBASE_MAP.md"].includes(entry.target)).map((entry) => entry.target),
|
|
614
|
+
C4_COMPONENT_PATH
|
|
615
|
+
]);
|
|
616
|
+
var OBSOLETE_SEED_SECTION = /^## (?:Setup Required|Pending bearings Setup)\s*$/m;
|
|
617
|
+
function tailoringTask(file, content = "", initial = false) {
|
|
618
|
+
if (MAP_PATHS.has(file.path)) return { path: file.path, reason: "map" };
|
|
619
|
+
if (initial && file.path === "AGENTS.md" || (file.owner === "agent" || isStarterSkillTarget(file.path)) && PLACEHOLDER.test(content)) {
|
|
620
|
+
return { path: file.path, reason: "tailor" };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function migrationTarget(record) {
|
|
624
|
+
return record.target ?? `checklists/repairs/${sha256(JSON.stringify([record.source, record.sourceHash])).slice(7, 23)}.json`;
|
|
625
|
+
}
|
|
626
|
+
function reconciliationTasks(manifest) {
|
|
627
|
+
return uniqueSetupTasks([
|
|
628
|
+
...manifest.files.filter((file) => file.reconciliations?.length).map((file) => ({ path: file.path, reason: "reconcile" })),
|
|
629
|
+
...(manifest.migrationReconciliations ?? []).map((record) => ({ path: migrationTarget(record), reason: "reconcile" }))
|
|
630
|
+
]);
|
|
631
|
+
}
|
|
632
|
+
function uniqueSetupTasks(tasks) {
|
|
633
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const task of tasks) {
|
|
635
|
+
if (MAP_PATHS.has(task.path)) byPath.set(task.path, { ...task, reason: "map" });
|
|
636
|
+
else if (!byPath.has(task.path) || task.reason === "reconcile") byPath.set(task.path, task);
|
|
637
|
+
}
|
|
638
|
+
return [...byPath.values()];
|
|
639
|
+
}
|
|
640
|
+
function isKnownSetupTask(task, manifest) {
|
|
641
|
+
return task.path === C4_COMPONENT_PATH && task.reason === "map" || manifest.files.some((file) => file.path === task.path) || manifest.migrationReconciliations?.some((record) => migrationTarget(record) === task.path) === true || task.acknowledged !== void 0;
|
|
642
|
+
}
|
|
643
|
+
async function skillFiles(repoDir, directory = ".agents/skills", ancestors = []) {
|
|
644
|
+
const paths = [];
|
|
645
|
+
const absolute = await realpath(join5(repoDir, directory)).catch((error) => {
|
|
646
|
+
if (error.code === "ENOENT") return null;
|
|
647
|
+
throw error;
|
|
648
|
+
});
|
|
649
|
+
if (absolute === null) return paths;
|
|
650
|
+
if (ancestors.includes(absolute)) throw new Error(`Cyclic skill directory: ${directory}`);
|
|
651
|
+
for (const entry of await readdir2(join5(repoDir, directory), { withFileTypes: true }).catch((error) => {
|
|
652
|
+
if (error.code === "ENOENT") return [];
|
|
653
|
+
throw error;
|
|
654
|
+
})) {
|
|
655
|
+
const path = `${directory}/${entry.name}`;
|
|
656
|
+
const directoryEntry = entry.isDirectory() || entry.isSymbolicLink() && (await stat2(join5(repoDir, path))).isDirectory();
|
|
657
|
+
if (directoryEntry) paths.push(...await skillFiles(repoDir, path, [...ancestors, absolute]));
|
|
658
|
+
else paths.push(path);
|
|
659
|
+
}
|
|
660
|
+
return paths;
|
|
661
|
+
}
|
|
662
|
+
async function protectSkills(repoDir, tasks, actions = [], artifacts = []) {
|
|
663
|
+
const scope = /* @__PURE__ */ new Set([...tasks.map((task) => task.path), ...artifacts]);
|
|
664
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const path of await skillFiles(repoDir)) {
|
|
666
|
+
if (!scope.has(path) && !isChecklistPayload(path)) hashes.set(path, sha256(await readFile3(join5(repoDir, path))));
|
|
667
|
+
}
|
|
668
|
+
for (const action of actions) {
|
|
669
|
+
if (action.kind === "keep" || !action.path.startsWith(".agents/skills/") || scope.has(action.path) || isChecklistPayload(action.path)) continue;
|
|
670
|
+
if (action.kind === "delete") hashes.delete(action.path);
|
|
671
|
+
else if (action.kind === "write" || action.kind === "merge") hashes.set(action.path, sha256(action.content));
|
|
672
|
+
}
|
|
673
|
+
return [...hashes].map(([path, hash]) => ({ path, hash }));
|
|
674
|
+
}
|
|
675
|
+
async function readSetupTaskContent(repoDir, task) {
|
|
676
|
+
const content = await readFile3(join5(repoDir, task.path), "utf8").catch((error) => {
|
|
677
|
+
if (error.code === "ENOENT") throw new Error(`Setup task file is missing: ${task.path}`);
|
|
678
|
+
throw error;
|
|
679
|
+
});
|
|
680
|
+
if (!content.trim() || PLACEHOLDER.test(content)) throw new Error(`Setup task still has unfilled content: ${task.path}`);
|
|
681
|
+
if (task.path === "AGENTS.md" && OBSOLETE_SEED_SECTION.test(content)) {
|
|
682
|
+
throw new Error("Remove the obsolete setup section from AGENTS.md before acknowledging setup.");
|
|
683
|
+
}
|
|
684
|
+
if (task.reason === "map") {
|
|
685
|
+
const marker = task.path === C4_COMPONENT_PATH ? /' repo-map-synced: [^<>\r\n]+\s*$/ : /<!-- repo-map-synced: [^<>\r\n]+ -->\s*$/;
|
|
686
|
+
if (!marker.test(content) || task.path === C4_COMPONENT_PATH && (!/^@startuml\b/m.test(content) || !/^@enduml\s*$/m.test(content))) {
|
|
687
|
+
throw new Error(`Setup map has an invalid sync-marker contract: ${task.path}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return content;
|
|
691
|
+
}
|
|
692
|
+
function acknowledgedTask(receipt) {
|
|
693
|
+
const artifacts = receipt.artifacts;
|
|
694
|
+
const { decision, validation } = receipt;
|
|
695
|
+
const acknowledged = artifacts?.length || decision || validation ? {
|
|
696
|
+
...artifacts?.length ? { artifacts } : {},
|
|
697
|
+
...decision ? { decision } : {},
|
|
698
|
+
...validation ? { validation } : {}
|
|
699
|
+
} : void 0;
|
|
700
|
+
return { path: receipt.path, reason: receipt.reason, ...acknowledged ? { acknowledged } : {} };
|
|
701
|
+
}
|
|
702
|
+
async function checkpointIsCurrent(repoDir, task) {
|
|
703
|
+
try {
|
|
704
|
+
for (const artifact of task.artifacts ?? []) {
|
|
705
|
+
if (artifact.kind !== "base" && await lstat3(join5(repoDir, artifact.path)).catch(() => null)) return false;
|
|
706
|
+
}
|
|
707
|
+
if (task.decision === "discard") return true;
|
|
708
|
+
const content = await readSetupTaskContent(repoDir, task);
|
|
709
|
+
if (sha256(content) !== task.outputHash) return false;
|
|
710
|
+
if (task.validation) await validateChecklistRepair(repoDir, task.path, task.validation, content);
|
|
711
|
+
const starter = starterSkillNameFromTarget(task.path);
|
|
712
|
+
if (starter) {
|
|
713
|
+
const baseline = await readFile3(join5(repoDir, skillBaselinePath(starter)), "utf8").catch(() => null);
|
|
714
|
+
if (baseline !== content) return false;
|
|
715
|
+
}
|
|
716
|
+
return true;
|
|
717
|
+
} catch {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function normalizeSetup(repoDir, manifest) {
|
|
722
|
+
const artifacts = new Set(setupArtifacts(manifest));
|
|
723
|
+
const seed = manifest.files.find((file) => file.path === "AGENTS.md" && !file.retired);
|
|
724
|
+
const seedContent = seed ? await readFile3(join5(repoDir, seed.path), "utf8").catch((error) => {
|
|
725
|
+
if (error.code === "ENOENT") return "";
|
|
726
|
+
throw error;
|
|
727
|
+
}) : "";
|
|
728
|
+
const legacyGate = OBSOLETE_SEED_SECTION.test(seedContent);
|
|
729
|
+
const gateTasks = legacyGate ? [{ path: "AGENTS.md", reason: "tailor" }] : [];
|
|
730
|
+
const priorCompleted = manifest.setupPending?.completedTasks ?? [];
|
|
731
|
+
const completed = [];
|
|
732
|
+
const requeued = [];
|
|
733
|
+
for (const task of priorCompleted) {
|
|
734
|
+
if (await checkpointIsCurrent(repoDir, task)) completed.push(task);
|
|
735
|
+
else requeued.push(acknowledgedTask(task));
|
|
736
|
+
}
|
|
737
|
+
let tasks;
|
|
738
|
+
if (manifest.setupPending?.tasks?.length || priorCompleted.length) {
|
|
739
|
+
tasks = uniqueSetupTasks([...manifest.setupPending?.tasks ?? [], ...requeued, ...gateTasks]);
|
|
740
|
+
} else {
|
|
741
|
+
tasks = [...reconciliationTasks(manifest), ...gateTasks];
|
|
742
|
+
for (const file of manifest.files) {
|
|
743
|
+
if (file.retired) continue;
|
|
744
|
+
const content = await readFile3(join5(repoDir, file.path), "utf8").catch((error) => {
|
|
745
|
+
if (error.code === "ENOENT") return "";
|
|
746
|
+
throw error;
|
|
747
|
+
});
|
|
748
|
+
const task = tailoringTask(file, content);
|
|
749
|
+
if (task && PLACEHOLDER.test(content)) tasks.push(task);
|
|
750
|
+
}
|
|
751
|
+
tasks = uniqueSetupTasks(tasks);
|
|
752
|
+
}
|
|
753
|
+
const next = { ...manifest };
|
|
754
|
+
if (tasks.length || completed.length) {
|
|
755
|
+
const taskPaths = new Set(tasks.map((task) => task.path));
|
|
756
|
+
const protectedSkills = manifest.setupPending?.protectedSkills?.filter((file) => !artifacts.has(file.path) && !isChecklistPayload(file.path) && !taskPaths.has(file.path));
|
|
757
|
+
const { completedTasks: _oldCompleted, ...pending } = manifest.setupPending ?? {
|
|
758
|
+
kind: legacyGate ? "init" : "update",
|
|
759
|
+
toVersion: manifest.bearingsVersion
|
|
760
|
+
};
|
|
761
|
+
next.setupPending = {
|
|
762
|
+
...pending,
|
|
763
|
+
tasks,
|
|
764
|
+
...completed.length ? { completedTasks: completed } : {},
|
|
765
|
+
...protectedSkills ? { protectedSkills } : {
|
|
766
|
+
protectedSkills: await protectSkills(repoDir, tasks, [], [...artifacts])
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
delete next.completedSetup;
|
|
770
|
+
} else {
|
|
771
|
+
delete next.setupPending;
|
|
772
|
+
}
|
|
773
|
+
return isDeepStrictEqual(next, manifest) ? manifest : next;
|
|
774
|
+
}
|
|
775
|
+
function renderSetupScope(manifest) {
|
|
776
|
+
const pending = manifest.setupPending;
|
|
777
|
+
if (!pending) return "No pending setup tasks.";
|
|
778
|
+
const lines = [`Pending setup (${pending.kind}):`, ...(pending.tasks ?? []).map((task) => ` ${task.reason}: ${task.path}${task.acknowledged?.decision ? `; retained choice: ${task.acknowledged.decision}` : ""}`)];
|
|
779
|
+
if (pending.completedTasks?.length) lines.push("Completed checkpoints:", ...pending.completedTasks.map((task) => ` ${task.reason}: ${task.path} (${task.outputHash ?? "discarded"})`));
|
|
780
|
+
for (const file of manifest.files) {
|
|
781
|
+
for (const record of file.reconciliations ?? []) {
|
|
782
|
+
lines.push(` ${file.path}: ${record.reason}; backup: ${record.backup}`);
|
|
783
|
+
if (record.reason === "skill-update") lines.push(` base: ${record.basePath}; incoming: ${record.incomingPath}`);
|
|
784
|
+
}
|
|
785
|
+
if (file.retired) lines.push(` Retired: ${file.path}. Ask the developer before selecting this path to discard its preserved reconciliation content; no live file is required.`);
|
|
786
|
+
}
|
|
787
|
+
for (const record of manifest.migrationReconciliations ?? []) {
|
|
788
|
+
lines.push(` ${migrationTarget(record)}: ${record.kind}; source: ${record.source}; backup: ${record.backup}`);
|
|
789
|
+
lines.push(" Read any existing repair target before replacing it; resolve its content with the developer.");
|
|
790
|
+
}
|
|
791
|
+
lines.push(
|
|
792
|
+
"Checkpoint completed tasks: bearings init --complete-setup --setup-path <path...>",
|
|
793
|
+
"Omit --setup-path to acknowledge all remaining tasks. Unchecked outputs stay pending.",
|
|
794
|
+
"For a skill-update Keep local/decline choice, add --keep-local <path...>; otherwise completion accepts the incoming template lineage.",
|
|
795
|
+
"When the historical base is missing, offer Take new template, Keep local, or Freeform; do not fabricate a historical base."
|
|
796
|
+
);
|
|
797
|
+
if (pending.protectedSkills?.length) lines.push("Protected skills \u2014 preserve these files:", ...pending.protectedSkills.map((file) => ` ${file.path} (${file.hash})`));
|
|
798
|
+
return lines.join("\n");
|
|
799
|
+
}
|
|
800
|
+
function pendingSetupReport(manifest) {
|
|
801
|
+
const pending = manifest.setupPending;
|
|
802
|
+
if (!pending) return "No pending setup tasks.";
|
|
803
|
+
return [
|
|
804
|
+
`Pending setup (${pending.kind}):`,
|
|
805
|
+
...(pending.tasks ?? []).map((task) => ` ${task.reason}: ${task.path}`),
|
|
806
|
+
"",
|
|
807
|
+
`Run bearings init to resume ${pending.kind === "init" ? "initial setup" : "/update-bearings-setup"}.`
|
|
808
|
+
].join("\n");
|
|
809
|
+
}
|
|
810
|
+
function setupArtifacts(manifest) {
|
|
811
|
+
return [
|
|
812
|
+
...manifest.files.flatMap((file) => (file.reconciliations ?? []).flatMap((record) => record.reason === "skill-update" ? [record.backup, record.incomingPath] : [record.backup])),
|
|
813
|
+
...(manifest.migrationReconciliations ?? []).map((record) => record.backup)
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/report.ts
|
|
818
|
+
function renderInitReport(r, exposed, m) {
|
|
819
|
+
const lines = ["bearings init complete.", ""];
|
|
820
|
+
lines.push(`Written (${r.written.length}):`, ...r.written.map((p) => ` + ${p}`));
|
|
821
|
+
if (r.backedUp.length) {
|
|
822
|
+
lines.push("", `Backed up (${r.backedUp.length}) \u2014 review during initial setup:`);
|
|
823
|
+
lines.push(...r.backedUp.map((b) => ` \u26A0 ${b.path} -> ${b.backup}`));
|
|
824
|
+
}
|
|
825
|
+
if (r.skippedUnchanged.length) {
|
|
826
|
+
lines.push(
|
|
827
|
+
"",
|
|
828
|
+
`Skipped, unchanged (${r.skippedUnchanged.length}):`,
|
|
829
|
+
...r.skippedUnchanged.map((p) => ` = ${p}`)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
lines.push(
|
|
833
|
+
"",
|
|
834
|
+
`Exposed to harnesses [${m.harnesses.join(", ")}] via ${m.exposure}:`,
|
|
835
|
+
...exposed.map((p) => ` ~ ${p}`)
|
|
836
|
+
);
|
|
837
|
+
if (m.version === 2 && m.setupPending) lines.push("", pendingSetupReport(m));
|
|
838
|
+
return lines.join("\n");
|
|
839
|
+
}
|
|
840
|
+
function categorizeActions(actions) {
|
|
841
|
+
const result = {
|
|
842
|
+
added: [],
|
|
843
|
+
restored: [],
|
|
844
|
+
replaced: [],
|
|
845
|
+
merged: [],
|
|
846
|
+
skillHandoffs: [],
|
|
847
|
+
skipped: [],
|
|
848
|
+
removed: [],
|
|
849
|
+
keptUntracked: []
|
|
850
|
+
};
|
|
851
|
+
for (const action of actions) {
|
|
852
|
+
if (action.kind === "write") {
|
|
853
|
+
if (action.reason === "add") result.added.push(action.path);
|
|
854
|
+
else if (action.reason === "restore") result.restored.push(action.path);
|
|
855
|
+
else result.replaced.push(action.path);
|
|
856
|
+
} else if (action.kind === "merge") {
|
|
857
|
+
result.merged.push(`${action.path} -> ${action.backup}`);
|
|
858
|
+
} else if (action.kind === "skill-handoff") {
|
|
859
|
+
result.skillHandoffs.push(action.path);
|
|
860
|
+
} else if (action.kind === "skip") {
|
|
861
|
+
result.skipped.push(action.path);
|
|
862
|
+
} else if (action.kind === "delete") {
|
|
863
|
+
result.removed.push(action.path);
|
|
864
|
+
} else if (action.kind === "keep-untracked") {
|
|
865
|
+
result.keptUntracked.push(action.path);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
function pushSection(lines, title, items) {
|
|
871
|
+
if (!items.length) return;
|
|
872
|
+
lines.push("", `${title} (${items.length}):`, ...items.map((i) => ` ${i}`));
|
|
873
|
+
}
|
|
874
|
+
function renderUpdatePlan(input) {
|
|
875
|
+
const categories = categorizeActions(input.actions);
|
|
876
|
+
const lines = [input.reconstruction ? "bearings init: reconstruction plan" : "bearings init: update plan"];
|
|
877
|
+
pushSection(lines, "Added", categories.added);
|
|
878
|
+
pushSection(lines, "Restored", categories.restored);
|
|
879
|
+
pushSection(lines, "Replaced", categories.replaced);
|
|
880
|
+
pushSection(lines, "Merged", categories.merged);
|
|
881
|
+
pushSection(lines, "Skill handoff \u2192 /update-bearings-setup", categories.skillHandoffs);
|
|
882
|
+
pushSection(lines, "Skipped", categories.skipped);
|
|
883
|
+
pushSection(lines, "Removed", categories.removed);
|
|
884
|
+
pushSection(lines, "Kept and untracked", categories.keptUntracked);
|
|
885
|
+
pushMigrationSections(lines, input.migrationActions ?? []);
|
|
886
|
+
if (input.adapterActions.length) {
|
|
887
|
+
lines.push("", `Adapter changes (${input.adapterActions.length}):`);
|
|
888
|
+
for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
|
|
889
|
+
}
|
|
890
|
+
lines.push(
|
|
891
|
+
"",
|
|
892
|
+
`Manifest changes: schema ${input.fromSchema ?? "\u2014"} -> ${input.toSchema}, version ${input.fromVersion ?? "\u2014"} -> ${input.toVersion}`
|
|
893
|
+
);
|
|
894
|
+
lines.push("", "Proceed?");
|
|
895
|
+
return lines.join("\n");
|
|
896
|
+
}
|
|
897
|
+
function renderUpdateReport(input) {
|
|
898
|
+
const categories = categorizeActions(input.actions);
|
|
899
|
+
const lines = [
|
|
900
|
+
input.reconstruction ? "bearings init: reconstruction complete." : "bearings init: update complete."
|
|
901
|
+
];
|
|
902
|
+
pushSection(lines, "Added", categories.added);
|
|
903
|
+
pushSection(lines, "Restored", categories.restored);
|
|
904
|
+
pushSection(lines, "Replaced", categories.replaced);
|
|
905
|
+
pushSection(lines, "Merged", categories.merged);
|
|
906
|
+
pushSection(lines, "Skill handoff \u2192 /update-bearings-setup", categories.skillHandoffs);
|
|
907
|
+
pushSection(lines, "Skipped", categories.skipped);
|
|
908
|
+
pushSection(lines, "Removed", categories.removed);
|
|
909
|
+
pushSection(lines, "Kept and untracked", categories.keptUntracked);
|
|
910
|
+
pushMigrationSections(lines, input.migrationActions ?? []);
|
|
911
|
+
if (input.adapterActions.length) {
|
|
912
|
+
lines.push("", `Adapter changes (${input.adapterActions.length}):`);
|
|
913
|
+
for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
|
|
914
|
+
}
|
|
915
|
+
lines.push(
|
|
916
|
+
"",
|
|
917
|
+
`Manifest changes: schema ${input.fromSchema ?? "\u2014"} -> ${input.toSchema}, version ${input.fromVersion ?? "\u2014"} -> ${input.toVersion}`
|
|
918
|
+
);
|
|
919
|
+
if (input.setupRequired) {
|
|
920
|
+
lines.push("", input.manifest ? pendingSetupReport(input.manifest) : "Run bearings init to resume /update-bearings-setup.");
|
|
921
|
+
} else {
|
|
922
|
+
lines.push("", "Finish by running: bearings verify");
|
|
923
|
+
}
|
|
924
|
+
return lines.join("\n");
|
|
925
|
+
}
|
|
926
|
+
function pushMigrationSections(lines, actions) {
|
|
927
|
+
pushSection(lines, "Migrated legacy test plans", actions.filter((action) => action.kind === "migrate-testplan").map((action) => `${action.source} -> ${action.target}`));
|
|
928
|
+
pushSection(lines, "Migration collisions backed up -> /update-bearings-setup", actions.flatMap((action) => action.kind === "migrate-testplan" && action.backup !== void 0 ? [`${action.target} -> ${action.backup}`] : []));
|
|
929
|
+
pushSection(lines, "Invalid legacy test plans preserved -> /update-bearings-setup", actions.filter((action) => action.kind === "preserve-invalid-testplan").map((action) => `${action.source} -> ${action.backup}`));
|
|
930
|
+
pushSection(lines, "Removed empty legacy plan directories", actions.filter((action) => action.kind === "remove-empty-testplan-dir").map((action) => action.path));
|
|
931
|
+
}
|
|
932
|
+
function renderVerifyReport(v) {
|
|
933
|
+
const lines = [];
|
|
934
|
+
for (const f of v.failures) lines.push(`FAIL ${f.code} ${f.path} \u2014 ${f.message}`);
|
|
935
|
+
for (const w of v.warnings) lines.push(`warn ${w.code} ${w.path} \u2014 ${w.message}`);
|
|
936
|
+
lines.push(v.failures.length ? `
|
|
937
|
+
${v.failures.length} failure(s).` : "\nbearings verify: OK");
|
|
938
|
+
return lines.join("\n");
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/update/transaction.ts
|
|
942
|
+
import { randomUUID } from "crypto";
|
|
943
|
+
import { cp, lstat as lstat4, mkdir as mkdir3, readFile as readFile4, readdir as readdir3, rename, rm as rm2, rmdir, symlink, writeFile as writeFile2 } from "fs/promises";
|
|
944
|
+
import { dirname as dirname4, join as join7, relative, sep as sep2 } from "path";
|
|
945
|
+
|
|
946
|
+
// src/backup-path.ts
|
|
947
|
+
import { access as access3, mkdir as mkdir2, open, rm } from "fs/promises";
|
|
948
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
949
|
+
async function freeBackupPath(repoDir, target) {
|
|
950
|
+
let candidate = `${target}.bkp`;
|
|
951
|
+
for (let index = 1; await access3(join6(repoDir, candidate)).then(() => true, () => false); index++) {
|
|
952
|
+
candidate = `${target}.bkp.${index}`;
|
|
953
|
+
}
|
|
954
|
+
return candidate;
|
|
955
|
+
}
|
|
956
|
+
async function writeBackup(repoDir, target, content, options = {}) {
|
|
957
|
+
let candidate = options.path ?? `${target}.bkp`;
|
|
958
|
+
for (let index = 1; ; index++) {
|
|
959
|
+
await options.beforeCreate?.(candidate);
|
|
960
|
+
const absolute = join6(repoDir, candidate);
|
|
961
|
+
await mkdir2(dirname3(absolute), { recursive: true });
|
|
962
|
+
let handle;
|
|
963
|
+
try {
|
|
964
|
+
handle = await open(absolute, "wx");
|
|
965
|
+
} catch (error) {
|
|
966
|
+
if (!options.path && error.code === "EEXIST") {
|
|
967
|
+
candidate = `${target}.bkp.${index}`;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
throw error;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
await handle.writeFile(content);
|
|
974
|
+
await handle.close();
|
|
975
|
+
return candidate;
|
|
976
|
+
} catch (error) {
|
|
977
|
+
await handle.close().catch(() => {
|
|
978
|
+
});
|
|
979
|
+
await rm(absolute, { force: true }).catch(() => {
|
|
980
|
+
});
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/update/transaction.ts
|
|
987
|
+
var MANIFEST_RELATIVE = join7(".agents", "bearings.json");
|
|
988
|
+
async function capture(repoDir, txDir, relativePath, snapshots) {
|
|
989
|
+
const target = join7(repoDir, relativePath);
|
|
990
|
+
const stat3 = await lstat4(target).catch(() => null);
|
|
991
|
+
if (!stat3) {
|
|
992
|
+
snapshots.push({ target });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const stored = join7(txDir, "snapshots", String(snapshots.length));
|
|
996
|
+
await mkdir3(dirname4(stored), { recursive: true });
|
|
997
|
+
await rename(target, stored);
|
|
998
|
+
snapshots.push({ target, stored });
|
|
999
|
+
}
|
|
1000
|
+
async function captureToBackup(repoDir, txDir, relativePath, backupRelativePath, snapshots) {
|
|
1001
|
+
await capture(repoDir, txDir, relativePath, snapshots);
|
|
1002
|
+
const sourceSnapshot = snapshots.at(-1);
|
|
1003
|
+
if (!sourceSnapshot?.stored) throw new Error(`Cannot back up missing path: ${relativePath}`);
|
|
1004
|
+
await writeBackup(repoDir, relativePath, await readFile4(sourceSnapshot.stored), { path: backupRelativePath });
|
|
1005
|
+
snapshots.push({ target: join7(repoDir, backupRelativePath) });
|
|
1006
|
+
}
|
|
1007
|
+
async function ensureDir(repoDir, absDir, createdDirs) {
|
|
1008
|
+
const rel = relative(repoDir, absDir);
|
|
1009
|
+
if (!rel || rel.startsWith("..")) return;
|
|
1010
|
+
let current = repoDir;
|
|
1011
|
+
for (const part of rel.split(sep2)) {
|
|
1012
|
+
current = join7(current, part);
|
|
1013
|
+
const stat3 = await lstat4(current).catch(() => null);
|
|
1014
|
+
if (!stat3) {
|
|
1015
|
+
await mkdir3(current);
|
|
1016
|
+
createdDirs.push(current);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async function writeContent(repoDir, txDir, relativePath, content, snapshots, createdDirs) {
|
|
1021
|
+
await capture(repoDir, txDir, relativePath, snapshots);
|
|
1022
|
+
const target = join7(repoDir, relativePath);
|
|
1023
|
+
await ensureDir(repoDir, dirname4(target), createdDirs);
|
|
1024
|
+
await writeFile2(target, content);
|
|
1025
|
+
}
|
|
1026
|
+
async function restore(snapshots, hooks, txDir) {
|
|
1027
|
+
for (const snapshot of [...snapshots].reverse()) {
|
|
1028
|
+
try {
|
|
1029
|
+
await hooks.beforeRollbackRestore?.(snapshot.target);
|
|
1030
|
+
await rm2(snapshot.target, { recursive: true, force: true });
|
|
1031
|
+
if (snapshot.stored) {
|
|
1032
|
+
await mkdir3(dirname4(snapshot.target), { recursive: true });
|
|
1033
|
+
await rename(snapshot.stored, snapshot.target);
|
|
1034
|
+
}
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
throw new Error(
|
|
1037
|
+
`Rollback failed while restoring ${snapshot.target}; transaction data retained at ${txDir}. Underlying error: ${error.message}`
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async function removeEmptyDirs(createdDirs) {
|
|
1043
|
+
for (const dir of [...createdDirs].reverse()) {
|
|
1044
|
+
try {
|
|
1045
|
+
const entries = await readdir3(dir);
|
|
1046
|
+
if (entries.length === 0) await rmdir(dir);
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async function removeEmptyCanonicalParents(repoDir, paths) {
|
|
1052
|
+
const skillsRoot = join7(repoDir, ".agents", "skills");
|
|
1053
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1054
|
+
for (const path of paths) {
|
|
1055
|
+
let current = dirname4(join7(repoDir, path));
|
|
1056
|
+
while (current.startsWith(`${skillsRoot}${sep2}`)) {
|
|
1057
|
+
candidates.add(current);
|
|
1058
|
+
current = dirname4(current);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
for (const dir of [...candidates].sort((a, b) => b.length - a.length)) {
|
|
1062
|
+
try {
|
|
1063
|
+
await rmdir(dir);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
const code = error.code;
|
|
1066
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY") throw error;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
async function applyUpdate(repoDir, plan, hooks = {}) {
|
|
1071
|
+
const txDir = join7(repoDir, ".agents", `.bearings-txn-${randomUUID()}`);
|
|
1072
|
+
const snapshots = [];
|
|
1073
|
+
const createdDirs = [];
|
|
1074
|
+
const abs = (relativePath) => join7(repoDir, relativePath);
|
|
1075
|
+
let opIndex = 0;
|
|
1076
|
+
const afterOp = async () => {
|
|
1077
|
+
const current = opIndex;
|
|
1078
|
+
opIndex += 1;
|
|
1079
|
+
await hooks.afterOperation?.(current);
|
|
1080
|
+
};
|
|
1081
|
+
try {
|
|
1082
|
+
for (const action of plan.files) {
|
|
1083
|
+
if (action.kind === "write") {
|
|
1084
|
+
await writeContent(repoDir, txDir, action.path, action.content, snapshots, createdDirs);
|
|
1085
|
+
if (isStarterSkillTarget(action.path)) {
|
|
1086
|
+
await writeContent(
|
|
1087
|
+
repoDir,
|
|
1088
|
+
txDir,
|
|
1089
|
+
skillBaselinePath(starterSkillNameFromTarget(action.path)),
|
|
1090
|
+
action.content,
|
|
1091
|
+
snapshots,
|
|
1092
|
+
createdDirs
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
await afterOp();
|
|
1096
|
+
} else if (action.kind === "merge") {
|
|
1097
|
+
await captureToBackup(repoDir, txDir, action.path, action.backup, snapshots);
|
|
1098
|
+
await writeFile2(abs(action.path), action.content);
|
|
1099
|
+
await afterOp();
|
|
1100
|
+
} else if (action.kind === "skill-handoff") {
|
|
1101
|
+
await writeBackup(repoDir, action.path, await readFile4(abs(action.path)), { path: action.backup });
|
|
1102
|
+
snapshots.push({ target: abs(action.backup) });
|
|
1103
|
+
await writeContent(repoDir, txDir, action.incomingPath, action.content, snapshots, createdDirs);
|
|
1104
|
+
await afterOp();
|
|
1105
|
+
} else if (action.kind === "delete") {
|
|
1106
|
+
await capture(repoDir, txDir, action.path, snapshots);
|
|
1107
|
+
await afterOp();
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
await removeEmptyCanonicalParents(
|
|
1111
|
+
repoDir,
|
|
1112
|
+
plan.files.filter((action) => action.kind === "delete").map((action) => action.path)
|
|
1113
|
+
);
|
|
1114
|
+
for (const action of plan.migrations ?? []) {
|
|
1115
|
+
if (action.kind === "migrate-testplan") {
|
|
1116
|
+
if (action.backup) await captureToBackup(repoDir, txDir, action.target, action.backup, snapshots);
|
|
1117
|
+
else await capture(repoDir, txDir, action.target, snapshots);
|
|
1118
|
+
await capture(repoDir, txDir, action.source, snapshots);
|
|
1119
|
+
await ensureDir(repoDir, dirname4(abs(action.target)), createdDirs);
|
|
1120
|
+
await writeFile2(abs(action.target), action.content);
|
|
1121
|
+
await afterOp();
|
|
1122
|
+
} else if (action.kind === "preserve-invalid-testplan") {
|
|
1123
|
+
await captureToBackup(repoDir, txDir, action.source, action.backup, snapshots);
|
|
1124
|
+
await afterOp();
|
|
1125
|
+
} else {
|
|
1126
|
+
const entries = await readdir3(abs(action.path));
|
|
1127
|
+
if (entries.length === 0) await capture(repoDir, txDir, action.path, snapshots);
|
|
1128
|
+
await afterOp();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
for (const action of plan.adapters) {
|
|
1132
|
+
if (action.kind === "write-symlink") {
|
|
1133
|
+
await capture(repoDir, txDir, action.path, snapshots);
|
|
1134
|
+
await ensureDir(repoDir, dirname4(abs(action.path)), createdDirs);
|
|
1135
|
+
await symlink(action.target, abs(action.path));
|
|
1136
|
+
await afterOp();
|
|
1137
|
+
} else if (action.kind === "write-copy") {
|
|
1138
|
+
await capture(repoDir, txDir, action.path, snapshots);
|
|
1139
|
+
await ensureDir(repoDir, dirname4(abs(action.path)), createdDirs);
|
|
1140
|
+
await cp(action.source, abs(action.path), { recursive: true });
|
|
1141
|
+
await afterOp();
|
|
1142
|
+
} else {
|
|
1143
|
+
await capture(repoDir, txDir, action.path, snapshots);
|
|
1144
|
+
await afterOp();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
await capture(repoDir, txDir, MANIFEST_RELATIVE, snapshots);
|
|
1148
|
+
await ensureDir(repoDir, dirname4(abs(MANIFEST_RELATIVE)), createdDirs);
|
|
1149
|
+
await writeFile2(abs(MANIFEST_RELATIVE), JSON.stringify(plan.manifest, null, 2) + "\n");
|
|
1150
|
+
await afterOp();
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
await restore(snapshots, hooks, txDir);
|
|
1153
|
+
await removeEmptyDirs(createdDirs);
|
|
1154
|
+
await rm2(txDir, { recursive: true, force: true }).catch(() => {
|
|
1155
|
+
});
|
|
1156
|
+
throw error;
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
await hooks.beforeCleanup?.();
|
|
1160
|
+
await rm2(txDir, { recursive: true, force: true });
|
|
1161
|
+
} catch {
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/commands/init.ts
|
|
1166
|
+
import { lstat as lstat7, readFile as readFile8, readdir as readdir6 } from "fs/promises";
|
|
1167
|
+
import { join as join12 } from "path";
|
|
1168
|
+
|
|
1169
|
+
// src/scanner.ts
|
|
1170
|
+
import { access as access4, symlink as symlink2, rm as rm3 } from "fs/promises";
|
|
1171
|
+
import { join as join8 } from "path";
|
|
1172
|
+
async function exists2(p) {
|
|
1173
|
+
try {
|
|
1174
|
+
await access4(p);
|
|
1175
|
+
return true;
|
|
1176
|
+
} catch {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function scan(repoDir) {
|
|
1181
|
+
const collisions = [];
|
|
1182
|
+
for (const e of SCAFFOLD) if (await exists2(join8(repoDir, e.target))) collisions.push(e.target);
|
|
1183
|
+
const harnessDirsPresent = [];
|
|
1184
|
+
for (const h of ["claude", "opencode"])
|
|
1185
|
+
if (await exists2(join8(repoDir, `.${h}`))) harnessDirsPresent.push(h);
|
|
1186
|
+
let symlinksSupported = true;
|
|
1187
|
+
const probe = join8(repoDir, ".bearings-symlink-probe");
|
|
1188
|
+
try {
|
|
1189
|
+
await symlink2(".", probe);
|
|
1190
|
+
await rm3(probe);
|
|
1191
|
+
} catch {
|
|
1192
|
+
symlinksSupported = false;
|
|
1193
|
+
}
|
|
1194
|
+
return { collisions, harnessDirsPresent, symlinksSupported };
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/generator.ts
|
|
1198
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3, access as access5 } from "fs/promises";
|
|
1199
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
1200
|
+
async function exists3(p) {
|
|
1201
|
+
try {
|
|
1202
|
+
await access5(p);
|
|
1203
|
+
return true;
|
|
1204
|
+
} catch {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function generate(repoDir, bearingsVersion, hooks = {}) {
|
|
1209
|
+
const priorState = await inspectManifest(repoDir);
|
|
1210
|
+
if (priorState.kind === "invalid") throw new Error(`Invalid manifest: ${priorState.message}`);
|
|
1211
|
+
const prior = priorState.kind === "valid" ? priorState.manifest.version === 1 ? await migrateV1(repoDir, priorState.manifest) : priorState.manifest : null;
|
|
1212
|
+
const result = { written: [], backedUp: [], skippedUnchanged: [], files: [] };
|
|
1213
|
+
for (const entry of SCAFFOLD) {
|
|
1214
|
+
const abs = join9(repoDir, entry.target);
|
|
1215
|
+
const templateContent = await readFile5(join9(templatesDir(), entry.template), "utf8");
|
|
1216
|
+
const priorEntry = prior?.files.find((f) => f.path === entry.target);
|
|
1217
|
+
let backup;
|
|
1218
|
+
let currentContent;
|
|
1219
|
+
if (await exists3(abs)) {
|
|
1220
|
+
const currentBytes = await readFile5(abs);
|
|
1221
|
+
currentContent = currentBytes.toString("utf8");
|
|
1222
|
+
if (priorEntry && sha256(currentContent) === priorEntry.hash) {
|
|
1223
|
+
const skillName2 = starterSkillNameFromTarget(entry.target);
|
|
1224
|
+
if (skillName2) {
|
|
1225
|
+
const baseline = join9(repoDir, skillBaselinePath(skillName2));
|
|
1226
|
+
if (!await exists3(baseline)) {
|
|
1227
|
+
await mkdir4(dirname5(baseline), { recursive: true });
|
|
1228
|
+
await writeFile3(baseline, currentContent);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
result.skippedUnchanged.push(entry.target);
|
|
1232
|
+
result.files.push(priorEntry);
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
backup = await writeBackup(repoDir, entry.target, currentBytes, {
|
|
1236
|
+
beforeCreate: hooks.beforeBackupCreate
|
|
1237
|
+
});
|
|
1238
|
+
result.backedUp.push({ path: entry.target, backup });
|
|
1239
|
+
}
|
|
1240
|
+
await mkdir4(dirname5(abs), { recursive: true });
|
|
1241
|
+
await writeFile3(abs, templateContent);
|
|
1242
|
+
result.written.push(entry.target);
|
|
1243
|
+
const incomingHash = sha256(templateContent);
|
|
1244
|
+
const skillName = starterSkillNameFromTarget(entry.target);
|
|
1245
|
+
if (skillName) {
|
|
1246
|
+
const baseline = join9(repoDir, skillBaselinePath(skillName));
|
|
1247
|
+
await mkdir4(dirname5(baseline), { recursive: true });
|
|
1248
|
+
await writeFile3(baseline, templateContent);
|
|
1249
|
+
}
|
|
1250
|
+
const reconciliation = backup && currentContent !== void 0 ? {
|
|
1251
|
+
backup,
|
|
1252
|
+
reason: "init-collision",
|
|
1253
|
+
sourceHash: sha256(currentContent),
|
|
1254
|
+
incomingTemplateVersion: bearingsVersion,
|
|
1255
|
+
incomingHash
|
|
1256
|
+
} : void 0;
|
|
1257
|
+
result.files.push({
|
|
1258
|
+
path: entry.target,
|
|
1259
|
+
template: entry.template,
|
|
1260
|
+
templateVersion: bearingsVersion,
|
|
1261
|
+
hash: incomingHash,
|
|
1262
|
+
owner: entry.owner,
|
|
1263
|
+
...isStarterSkillTarget(entry.target) ? { lastTemplateHash: incomingHash } : {},
|
|
1264
|
+
...reconciliation ? { reconciliations: [reconciliation] } : {}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
return result;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/adapters.ts
|
|
1271
|
+
import { mkdir as mkdir5, readdir as readdir4, symlink as symlink3, lstat as lstat5, readlink as readlink2, rm as rm4, cp as cp2, readFile as readFile6 } from "fs/promises";
|
|
1272
|
+
import { join as join10 } from "path";
|
|
1273
|
+
var ADAPTER_KINDS = ["skills", "commands"];
|
|
1274
|
+
async function canonicalAdapterEntries(repoDir) {
|
|
1275
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const kind of ADAPTER_KINDS) {
|
|
1277
|
+
const directory = join10(repoDir, ".agents", kind);
|
|
1278
|
+
for (const name of await readdir4(directory).catch(() => [])) {
|
|
1279
|
+
entries.set(`${kind}/${name}`, join10(directory, name));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return entries;
|
|
1283
|
+
}
|
|
1284
|
+
async function entriesMatch(src, dst) {
|
|
1285
|
+
const srcStat = await lstat5(src).catch(() => null);
|
|
1286
|
+
const dstStat = await lstat5(dst).catch(() => null);
|
|
1287
|
+
if (!srcStat || !dstStat) return false;
|
|
1288
|
+
if (srcStat.isSymbolicLink() || dstStat.isSymbolicLink()) {
|
|
1289
|
+
return srcStat.isSymbolicLink() && dstStat.isSymbolicLink() && await readlink2(src) === await readlink2(dst);
|
|
1290
|
+
}
|
|
1291
|
+
if (srcStat.isDirectory() || dstStat.isDirectory()) {
|
|
1292
|
+
if (!srcStat.isDirectory() || !dstStat.isDirectory()) return false;
|
|
1293
|
+
const [srcEntries, dstEntries] = (await Promise.all([readdir4(src), readdir4(dst)])).map((names) => names.filter((name) => !isChecklistPayload(join10(src, name))));
|
|
1294
|
+
if (srcEntries.length !== dstEntries.length) return false;
|
|
1295
|
+
srcEntries.sort();
|
|
1296
|
+
dstEntries.sort();
|
|
1297
|
+
for (let i = 0; i < srcEntries.length; i++) {
|
|
1298
|
+
if (srcEntries[i] !== dstEntries[i]) return false;
|
|
1299
|
+
if (!await entriesMatch(join10(src, srcEntries[i]), join10(dst, dstEntries[i]))) return false;
|
|
1300
|
+
}
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
if (srcStat.isFile() || dstStat.isFile()) {
|
|
1304
|
+
return srcStat.isFile() && dstStat.isFile() && (await readFile6(src)).equals(await readFile6(dst));
|
|
1305
|
+
}
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
async function expose(repoDir, harness, mode) {
|
|
1309
|
+
const created = [];
|
|
1310
|
+
for (const kind of ADAPTER_KINDS) {
|
|
1311
|
+
const srcDir = join10(repoDir, ".agents", kind);
|
|
1312
|
+
let entries;
|
|
1313
|
+
try {
|
|
1314
|
+
entries = await readdir4(srcDir);
|
|
1315
|
+
} catch {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
const dstDir = join10(repoDir, `.${harness}`, kind);
|
|
1319
|
+
await mkdir5(dstDir, { recursive: true });
|
|
1320
|
+
for (const name of entries) {
|
|
1321
|
+
const dst = join10(dstDir, name);
|
|
1322
|
+
const adapterPath = join10(`.${harness}`, kind, name);
|
|
1323
|
+
const relTarget = join10("..", "..", ".agents", kind, name);
|
|
1324
|
+
const stat3 = await lstat5(dst).catch(() => null);
|
|
1325
|
+
if (mode === "symlink") {
|
|
1326
|
+
if (stat3?.isSymbolicLink() && await readlink2(dst) === relTarget) continue;
|
|
1327
|
+
if (stat3 && !stat3.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
|
|
1328
|
+
if (stat3) await rm4(dst, { recursive: true });
|
|
1329
|
+
await symlink3(relTarget, dst);
|
|
1330
|
+
} else {
|
|
1331
|
+
const src = join10(srcDir, name);
|
|
1332
|
+
if (stat3 && await entriesMatch(src, dst)) continue;
|
|
1333
|
+
if (stat3 && !stat3.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
|
|
1334
|
+
if (stat3) await rm4(dst, { recursive: true });
|
|
1335
|
+
await cp2(src, dst, { recursive: true });
|
|
1336
|
+
}
|
|
1337
|
+
created.push(adapterPath);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return created;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/setup-completion.ts
|
|
1344
|
+
import { lstat as lstat6, readFile as readFile7, readdir as readdir5, readlink as readlink3 } from "fs/promises";
|
|
1345
|
+
import { join as join11, resolve as resolve2 } from "path";
|
|
1346
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
1347
|
+
async function validateProtectedSkills(repoDir, files) {
|
|
1348
|
+
for (const file of files) {
|
|
1349
|
+
if (isChecklistPayload(file.path)) continue;
|
|
1350
|
+
const actual = await readFile7(join11(repoDir, file.path)).catch(() => null);
|
|
1351
|
+
if (!actual || sha256(actual) !== file.hash) throw new Error(`Protected skill changed during setup: ${file.path}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function resolvedRecord(file, content, keepLocal) {
|
|
1355
|
+
const { reconciliations, retired: _retired, ...resolved } = file;
|
|
1356
|
+
if (isStarterSkillTarget(file.path)) {
|
|
1357
|
+
const incoming = reconciliations?.findLast((record) => record.reason === "skill-update");
|
|
1358
|
+
resolved.owner = "agent";
|
|
1359
|
+
resolved.hash = sha256(content);
|
|
1360
|
+
resolved.lastTemplateHash = keepLocal ? file.lastTemplateHash ?? file.hash : incoming?.incomingHash ?? file.lastTemplateHash ?? file.hash;
|
|
1361
|
+
if (keepLocal && incoming) resolved.skippedTemplate = { templateVersion: incoming.incomingTemplateVersion, hash: incoming.incomingHash };
|
|
1362
|
+
else if (!keepLocal) delete resolved.skippedTemplate;
|
|
1363
|
+
}
|
|
1364
|
+
return resolved;
|
|
1365
|
+
}
|
|
1366
|
+
function addArtifact(artifacts, artifact) {
|
|
1367
|
+
const existing = artifacts.get(artifact.path);
|
|
1368
|
+
if (existing && !isDeepStrictEqual2(existing, artifact)) throw new Error(`Conflicting setup artifact receipt: ${artifact.path}`);
|
|
1369
|
+
artifacts.set(artifact.path, artifact);
|
|
1370
|
+
}
|
|
1371
|
+
async function requireArtifact(repoDir, artifact) {
|
|
1372
|
+
const bytes = await readFile7(join11(repoDir, artifact.path)).catch((error) => {
|
|
1373
|
+
if (error.code === "ENOENT") return null;
|
|
1374
|
+
throw error;
|
|
1375
|
+
});
|
|
1376
|
+
if (bytes === null) throw new Error(`Selected setup artifact is missing: ${artifact.path}`);
|
|
1377
|
+
if (sha256(bytes) !== artifact.hash) throw new Error(`Selected setup artifact hash changed: ${artifact.path}`);
|
|
1378
|
+
}
|
|
1379
|
+
async function validateSelectedOutputs(repoDir, manifest, tasks, mode) {
|
|
1380
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1381
|
+
const receipts = [];
|
|
1382
|
+
for (const task of tasks) {
|
|
1383
|
+
const file = manifest.files.find((entry) => entry.path === task.path);
|
|
1384
|
+
const migration = manifest.migrationReconciliations?.find((record) => migrationTarget(record) === task.path);
|
|
1385
|
+
if (!isKnownSetupTask(task, manifest) || task.reason === "reconcile" && !file?.reconciliations?.length && !migration && !task.acknowledged) {
|
|
1386
|
+
throw new Error(`Setup task has no resolution record: ${task.path}`);
|
|
1387
|
+
}
|
|
1388
|
+
const expectedArtifacts = /* @__PURE__ */ new Map();
|
|
1389
|
+
const migrationValidation = migration ? { kind: "checklist-repair", source: migration.source } : void 0;
|
|
1390
|
+
if (task.acknowledged?.validation && migrationValidation && !isDeepStrictEqual2(task.acknowledged.validation, migrationValidation)) {
|
|
1391
|
+
throw new Error(`Setup task has conflicting validation: ${task.path}`);
|
|
1392
|
+
}
|
|
1393
|
+
const validation = task.acknowledged?.validation ?? migrationValidation;
|
|
1394
|
+
for (const artifact of task.acknowledged?.artifacts ?? []) addArtifact(expectedArtifacts, artifact);
|
|
1395
|
+
for (const record of file?.reconciliations ?? []) {
|
|
1396
|
+
addArtifact(expectedArtifacts, { path: record.backup, hash: record.sourceHash, kind: "backup" });
|
|
1397
|
+
if (record.reason === "skill-update") {
|
|
1398
|
+
addArtifact(expectedArtifacts, { path: record.incomingPath, hash: record.incomingHash, kind: "incoming" });
|
|
1399
|
+
const base = { path: record.basePath, hash: record.baseHash, kind: "base" };
|
|
1400
|
+
if (mode.kind === "before-cleanup") {
|
|
1401
|
+
const exists4 = await lstat6(join11(repoDir, base.path)).catch(() => null);
|
|
1402
|
+
if (exists4) addArtifact(expectedArtifacts, base);
|
|
1403
|
+
} else {
|
|
1404
|
+
const actualBase = mode.receipts.get(task.path)?.artifacts?.find((artifact) => artifact.kind === "base" && artifact.path === base.path);
|
|
1405
|
+
if (actualBase) addArtifact(expectedArtifacts, base);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (migration) addArtifact(expectedArtifacts, { path: migration.backup, hash: migration.sourceHash, kind: "backup" });
|
|
1410
|
+
const suppliedReceipt = mode.kind === "after-cleanup" ? mode.receipts.get(task.path) : void 0;
|
|
1411
|
+
const lockedReceipt = mode.kind === "after-cleanup" ? mode.locked.get(task.path) : void 0;
|
|
1412
|
+
let content;
|
|
1413
|
+
let discarded = false;
|
|
1414
|
+
const retainedDiscard = task.acknowledged?.decision === "discard" || lockedReceipt?.decision === "discard" || mode.kind === "after-cleanup" && file?.retired && suppliedReceipt?.decision === "discard";
|
|
1415
|
+
if (retainedDiscard) {
|
|
1416
|
+
discarded = true;
|
|
1417
|
+
} else if (file?.retired) {
|
|
1418
|
+
content = await readFile7(join11(repoDir, task.path), "utf8").catch((error) => {
|
|
1419
|
+
if (error.code === "ENOENT") return void 0;
|
|
1420
|
+
throw error;
|
|
1421
|
+
});
|
|
1422
|
+
discarded = content === void 0;
|
|
1423
|
+
if (content !== void 0) content = await readSetupTaskContent(repoDir, task);
|
|
1424
|
+
} else {
|
|
1425
|
+
content = await readSetupTaskContent(repoDir, task);
|
|
1426
|
+
}
|
|
1427
|
+
if (content !== void 0) contents.set(task.path, content);
|
|
1428
|
+
if (validation) await validateChecklistRepair(repoDir, task.path, validation, content);
|
|
1429
|
+
if (mode.kind === "before-cleanup") {
|
|
1430
|
+
for (const artifact of expectedArtifacts.values()) {
|
|
1431
|
+
const acknowledged = task.acknowledged?.artifacts?.some((prior) => prior.path === artifact.path);
|
|
1432
|
+
if (!acknowledged) await requireArtifact(repoDir, artifact);
|
|
1433
|
+
else if (artifact.kind !== "base" && await lstat6(join11(repoDir, artifact.path)).catch(() => null)) {
|
|
1434
|
+
throw new Error(`Previously cleaned setup artifact returned: ${artifact.path}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const decision = discarded ? "discard" : mode.keepLocal.has(task.path) || task.acknowledged?.decision === "keep-local" ? "keep-local" : void 0;
|
|
1438
|
+
receipts.push({
|
|
1439
|
+
path: task.path,
|
|
1440
|
+
reason: task.reason,
|
|
1441
|
+
...content === void 0 ? {} : { outputHash: sha256(content) },
|
|
1442
|
+
...decision ? { decision } : {},
|
|
1443
|
+
...expectedArtifacts.size ? { artifacts: [...expectedArtifacts.values()] } : {},
|
|
1444
|
+
...validation ? { validation } : {}
|
|
1445
|
+
});
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
const receipt = suppliedReceipt;
|
|
1449
|
+
if (!receipt) throw new Error(`Setup completion receipt is missing task: ${task.path}`);
|
|
1450
|
+
const locked = mode.locked.get(task.path);
|
|
1451
|
+
if (locked && !isDeepStrictEqual2(receipt, locked)) throw new Error(`Setup completion receipt changed a prior checkpoint: ${task.path}`);
|
|
1452
|
+
if (discarded ? receipt.decision !== "discard" || receipt.outputHash !== void 0 : receipt.outputHash !== sha256(content)) {
|
|
1453
|
+
throw new Error(`Setup completion receipt does not match output: ${task.path}`);
|
|
1454
|
+
}
|
|
1455
|
+
const actualArtifacts = new Map((receipt.artifacts ?? []).map((artifact) => [artifact.path, artifact]));
|
|
1456
|
+
if (actualArtifacts.size !== expectedArtifacts.size || [...expectedArtifacts].some(([path, artifact]) => !isDeepStrictEqual2(actualArtifacts.get(path), artifact))) {
|
|
1457
|
+
throw new Error(`Setup completion receipt does not match artifacts: ${task.path}`);
|
|
1458
|
+
}
|
|
1459
|
+
if (!isDeepStrictEqual2(receipt.validation, validation)) throw new Error(`Setup completion receipt does not match validation: ${task.path}`);
|
|
1460
|
+
const expectedDecision = discarded ? "discard" : task.acknowledged?.decision === "keep-local" || file?.reconciliations?.some((record) => record.reason === "skill-update") && receipt.decision === "keep-local" ? receipt.decision : void 0;
|
|
1461
|
+
if (receipt.decision !== expectedDecision) throw new Error(`Setup completion receipt has an invalid decision: ${task.path}`);
|
|
1462
|
+
for (const artifact of receipt.artifacts ?? []) {
|
|
1463
|
+
if (artifact.kind === "base") continue;
|
|
1464
|
+
if (await lstat6(join11(repoDir, artifact.path)).catch(() => null)) throw new Error(`Setup artifact was not cleaned up: ${artifact.path}`);
|
|
1465
|
+
if (!/^\.agents\/(skills|commands)\//.test(artifact.path)) continue;
|
|
1466
|
+
for (const harness of manifest.harnesses) {
|
|
1467
|
+
const adapter = artifact.path.replace(/^\.agents\//, `.${harness}/`);
|
|
1468
|
+
if (await lstat6(join11(repoDir, adapter)).catch(() => null)) throw new Error(`Setup artifact exposure was not cleaned up: ${adapter}`);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
receipts.push(receipt);
|
|
1472
|
+
}
|
|
1473
|
+
return { contents, receipts };
|
|
1474
|
+
}
|
|
1475
|
+
async function completeSetup(repoDir, options = {}) {
|
|
1476
|
+
const loaded = await requireValidManifest(repoDir);
|
|
1477
|
+
const manifest = await normalizeSetup(repoDir, loaded.version === 1 ? await migrateV1(repoDir, loaded) : loaded);
|
|
1478
|
+
if (!manifest.setupPending) {
|
|
1479
|
+
if (options.paths !== void 0 || options.keepLocal?.length) throw new Error("No current setup tasks to select.");
|
|
1480
|
+
if (loaded.version === 2 && loaded.setupPending) {
|
|
1481
|
+
await applyUpdate(repoDir, { files: [], adapters: [], manifest });
|
|
1482
|
+
}
|
|
1483
|
+
return { manifest, report: "bearings setup: already complete." };
|
|
1484
|
+
}
|
|
1485
|
+
const allTasks = manifest.setupPending.tasks ?? [];
|
|
1486
|
+
if (!allTasks.length) throw new Error("Pending setup has no task scope. Resume bearings init first.");
|
|
1487
|
+
const pendingScope = new Set(allTasks.map((task) => task.path));
|
|
1488
|
+
const selected = options.paths ?? [...pendingScope];
|
|
1489
|
+
if (!selected.length || new Set(selected).size !== selected.length || selected.some((path) => !pendingScope.has(path))) {
|
|
1490
|
+
throw new Error("Select unique current setup task paths.");
|
|
1491
|
+
}
|
|
1492
|
+
const scope = new Set(selected);
|
|
1493
|
+
const tasks = allTasks.filter((task) => scope.has(task.path));
|
|
1494
|
+
const remaining = allTasks.filter((task) => !scope.has(task.path));
|
|
1495
|
+
const keepLocal = new Set(options.keepLocal ?? []);
|
|
1496
|
+
if (keepLocal.size !== (options.keepLocal?.length ?? 0) || [...keepLocal].some((path) => !scope.has(path) || !isStarterSkillTarget(path) || !tasks.find((task) => task.path === path && (task.acknowledged?.decision === "keep-local" || manifest.files.find((file) => file.path === path && !file.retired)?.reconciliations?.some((record) => record.reason === "skill-update"))))) {
|
|
1497
|
+
throw new Error("--keep-local requires unique selected skill-update task paths.");
|
|
1498
|
+
}
|
|
1499
|
+
for (const task of reconciliationTasks(manifest)) {
|
|
1500
|
+
if (!pendingScope.has(task.path)) throw new Error(`Reconciliation is outside setup scope: ${task.path}`);
|
|
1501
|
+
}
|
|
1502
|
+
const { contents, receipts } = await validateSelectedOutputs(repoDir, manifest, tasks, { kind: "before-cleanup", keepLocal });
|
|
1503
|
+
await validateProtectedSkills(repoDir, manifest.setupPending.protectedSkills ?? []);
|
|
1504
|
+
const writes = [];
|
|
1505
|
+
const files = [];
|
|
1506
|
+
const receiptsByPath = new Map(receipts.map((receipt) => [receipt.path, receipt]));
|
|
1507
|
+
for (const file of manifest.files) {
|
|
1508
|
+
if (!scope.has(file.path)) {
|
|
1509
|
+
files.push(file);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
for (const record of file.reconciliations ?? []) {
|
|
1513
|
+
writes.push({ kind: "delete", path: record.backup, previous: file });
|
|
1514
|
+
if (record.reason === "skill-update") writes.push({ kind: "delete", path: record.incomingPath, previous: file });
|
|
1515
|
+
}
|
|
1516
|
+
if (file.retired) continue;
|
|
1517
|
+
const resolvedFile = resolvedRecord(file, contents.get(file.path), receiptsByPath.get(file.path)?.decision === "keep-local");
|
|
1518
|
+
if (isStarterSkillTarget(file.path)) {
|
|
1519
|
+
writes.push({ kind: "write", reason: "replace", path: skillBaselinePath(starterSkillNameFromTarget(file.path)), content: contents.get(file.path), record: resolvedFile });
|
|
1520
|
+
}
|
|
1521
|
+
files.push(resolvedFile);
|
|
1522
|
+
}
|
|
1523
|
+
for (const record of manifest.migrationReconciliations ?? []) {
|
|
1524
|
+
if (scope.has(migrationTarget(record))) writes.push({ kind: "delete", path: record.backup, previous: manifest.files[0] });
|
|
1525
|
+
}
|
|
1526
|
+
const cleanup = new Set(writes.filter((action) => action.kind === "delete").map((action) => action.path));
|
|
1527
|
+
const check = await verify(repoDir);
|
|
1528
|
+
for (const issue of check.failures) {
|
|
1529
|
+
const canonicalRoot = issue.path.replace(/^\.(claude|opencode)\//, ".agents/");
|
|
1530
|
+
if (cleanup.has(canonicalRoot) && issue.code === "missing-exposure") continue;
|
|
1531
|
+
const authorized = allTasks.filter((task) => task.path === canonicalRoot || task.path.startsWith(`${canonicalRoot}/`));
|
|
1532
|
+
if (authorized.length && authorized.every((task) => !scope.has(task.path))) continue;
|
|
1533
|
+
if (issue.code !== "copy-drift" || !authorized.length) throw new Error(`Setup validation failed: ${issue.path}: ${issue.message}`);
|
|
1534
|
+
const compare = async (source, copy) => {
|
|
1535
|
+
const relativePath = source.slice(resolve2(repoDir).length + 1);
|
|
1536
|
+
if (isChecklistPayload(relativePath)) return;
|
|
1537
|
+
if (pendingScope.has(relativePath) && !scope.has(relativePath)) return;
|
|
1538
|
+
const sourceStat = await lstat6(source).catch(() => null);
|
|
1539
|
+
const copyStat = await lstat6(copy).catch(() => null);
|
|
1540
|
+
const file = manifest.files.find((entry) => entry.path === relativePath);
|
|
1541
|
+
if (!sourceStat && file?.retired && scope.has(relativePath)) {
|
|
1542
|
+
if (copyStat) {
|
|
1543
|
+
if (!copyStat.isFile() || ![file.hash, ...(file.reconciliations ?? []).map((record) => record.sourceHash)].includes(sha256(await readFile7(copy)))) throw new Error(`Copy adapter drift: ${copy}`);
|
|
1544
|
+
writes.push({ kind: "delete", path: copy.slice(resolve2(repoDir).length + 1), previous: file });
|
|
1545
|
+
}
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
if (!sourceStat || !copyStat || sourceStat.isSymbolicLink() || copyStat.isSymbolicLink()) throw new Error(`Copy adapter drift: ${copy}`);
|
|
1549
|
+
if (sourceStat.isDirectory()) {
|
|
1550
|
+
if (!copyStat.isDirectory()) throw new Error(`Copy adapter drift: ${copy}`);
|
|
1551
|
+
const names = /* @__PURE__ */ new Set([...await readdir5(source), ...await readdir5(copy)]);
|
|
1552
|
+
for (const name of names) await compare(join11(source, name), join11(copy, name));
|
|
1553
|
+
} else if (!await entriesMatch(source, copy)) {
|
|
1554
|
+
const handoff = file?.reconciliations?.findLast((record) => record.reason === "skill-update");
|
|
1555
|
+
const expected = handoff?.sourceHash ?? file?.hash;
|
|
1556
|
+
if (!pendingScope.has(relativePath) || sha256(await readFile7(copy)) !== expected) throw new Error(`Copy adapter drift: ${copy}`);
|
|
1557
|
+
const path = copy.slice(resolve2(repoDir).length + 1);
|
|
1558
|
+
if (scope.has(relativePath)) writes.push({ kind: "write", reason: "replace", path, content: contents.get(relativePath), record: file });
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
await compare(resolve2(repoDir, canonicalRoot), resolve2(repoDir, issue.path));
|
|
1562
|
+
}
|
|
1563
|
+
for (const action of [...writes]) {
|
|
1564
|
+
if (action.kind !== "delete" || !/^\.agents\/(skills|commands)\//.test(action.path)) continue;
|
|
1565
|
+
if (manifest.exposure === "symlink" && !action.path.startsWith(".agents/commands/")) continue;
|
|
1566
|
+
for (const harness of manifest.harnesses) {
|
|
1567
|
+
const path = action.path.replace(/^\.agents\//, `.${harness}/`);
|
|
1568
|
+
const existing = await lstat6(join11(repoDir, path)).catch(() => null);
|
|
1569
|
+
if (existing) {
|
|
1570
|
+
const matches = manifest.exposure === "symlink" ? existing.isSymbolicLink() && await readlink3(join11(repoDir, path)) === join11("..", "..", action.path) : !existing.isSymbolicLink() && await entriesMatch(join11(repoDir, action.path), join11(repoDir, path));
|
|
1571
|
+
if (!matches) throw new Error(`Artifact adapter drift: ${path}`);
|
|
1572
|
+
writes.push({ kind: "delete", path, previous: action.previous });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const { setupPending: _pending, completedSetup: _oldReceipt, migrationReconciliations: _migrations, ...rest } = manifest;
|
|
1577
|
+
const completed = { ...rest, files };
|
|
1578
|
+
const migrations = manifest.migrationReconciliations?.filter((record) => !scope.has(migrationTarget(record)));
|
|
1579
|
+
if (migrations?.length) completed.migrationReconciliations = migrations;
|
|
1580
|
+
if (remaining.length) {
|
|
1581
|
+
const removed = new Set(writes.filter((action) => action.kind === "delete").map((action) => action.path));
|
|
1582
|
+
const protectedSkills = (manifest.setupPending.protectedSkills ?? []).filter((file) => !removed.has(file.path));
|
|
1583
|
+
completed.setupPending = {
|
|
1584
|
+
...manifest.setupPending,
|
|
1585
|
+
tasks: remaining,
|
|
1586
|
+
completedTasks: [...manifest.setupPending.completedTasks ?? [], ...receipts],
|
|
1587
|
+
protectedSkills
|
|
1588
|
+
};
|
|
1589
|
+
} else completed.completedSetup = {
|
|
1590
|
+
kind: manifest.setupPending.kind,
|
|
1591
|
+
...manifest.setupPending.fromVersion ? { fromVersion: manifest.setupPending.fromVersion } : {},
|
|
1592
|
+
toVersion: manifest.setupPending.toVersion,
|
|
1593
|
+
tasks: [...manifest.setupPending.completedTasks ?? [], ...receipts]
|
|
1594
|
+
};
|
|
1595
|
+
await applyUpdate(repoDir, { files: writes, adapters: [], manifest: completed });
|
|
1596
|
+
if (remaining.length) return { manifest: completed, report: `Checkpointed tasks (${tasks.length}):
|
|
1597
|
+
${tasks.map((task) => ` ${task.path}`).join("\n")}
|
|
1598
|
+
|
|
1599
|
+
${pendingSetupReport(completed)}` };
|
|
1600
|
+
return { manifest: completed, report: `bearings setup complete.
|
|
1601
|
+
Resolved tasks (${tasks.length}):
|
|
1602
|
+
${tasks.map((task) => ` ${task.path}`).join("\n")}
|
|
1603
|
+
Finish by running: bearings verify` };
|
|
1604
|
+
}
|
|
1605
|
+
async function verifySetupOutcome(repoDir, expected) {
|
|
1606
|
+
const manifest = await requireValidManifest(repoDir);
|
|
1607
|
+
if (manifest.version !== 2 || manifest.setupPending || manifest.migrationReconciliations?.length || manifest.files.some((file) => file.reconciliations?.length)) {
|
|
1608
|
+
throw new Error("Setup outcome is still pending or has unresolved records.");
|
|
1609
|
+
}
|
|
1610
|
+
if (!manifest.completedSetup) throw new Error("Setup outcome has no durable completion receipt.");
|
|
1611
|
+
if (manifest.bearingsVersion !== expected.bearingsVersion || manifest.exposure !== expected.exposure || !isDeepStrictEqual2(manifest.harnesses, expected.harnesses)) {
|
|
1612
|
+
throw new Error("Setup outcome changed the expected manifest configuration.");
|
|
1613
|
+
}
|
|
1614
|
+
if (!expected.setupPending || !(expected.setupPending.tasks?.length || expected.setupPending.completedTasks?.length)) {
|
|
1615
|
+
throw new Error("Expected setup journal has no task scope. Resume bearings init before launch.");
|
|
1616
|
+
}
|
|
1617
|
+
const tasks = expected.setupPending.tasks ?? [];
|
|
1618
|
+
const completedTasks = expected.setupPending.completedTasks ?? [];
|
|
1619
|
+
const allTasks = [
|
|
1620
|
+
...completedTasks.map(acknowledgedTask),
|
|
1621
|
+
...tasks
|
|
1622
|
+
];
|
|
1623
|
+
const scope = new Set(tasks.map((task) => task.path));
|
|
1624
|
+
for (const task of reconciliationTasks(expected)) {
|
|
1625
|
+
if (!scope.has(task.path)) throw new Error(`Expected reconciliation is outside setup scope: ${task.path}`);
|
|
1626
|
+
}
|
|
1627
|
+
const artifacts = new Set(setupArtifacts(expected));
|
|
1628
|
+
await validateProtectedSkills(repoDir, (expected.setupPending?.protectedSkills ?? []).filter((file) => !artifacts.has(file.path)));
|
|
1629
|
+
const receipt = manifest.completedSetup;
|
|
1630
|
+
if (receipt.kind !== expected.setupPending.kind || receipt.fromVersion !== expected.setupPending.fromVersion || receipt.toVersion !== expected.setupPending.toVersion) {
|
|
1631
|
+
throw new Error("Setup completion receipt does not match the original setup journal.");
|
|
1632
|
+
}
|
|
1633
|
+
const receiptMap = new Map(receipt.tasks.map((task) => [task.path, task]));
|
|
1634
|
+
if (receiptMap.size !== allTasks.length) throw new Error("Setup completion receipt does not match the original task scope.");
|
|
1635
|
+
const { contents } = await validateSelectedOutputs(repoDir, expected, allTasks, {
|
|
1636
|
+
kind: "after-cleanup",
|
|
1637
|
+
receipts: receiptMap,
|
|
1638
|
+
locked: new Map(completedTasks.map((task) => [task.path, task]))
|
|
1639
|
+
});
|
|
1640
|
+
for (const file of expected.files) {
|
|
1641
|
+
const actual = manifest.files.find((record) => record.path === file.path);
|
|
1642
|
+
if (scope.has(file.path) && file.retired) {
|
|
1643
|
+
if (actual) throw new Error(`Retired setup record was not resolved: ${file.path}`);
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
if (!actual) throw new Error(`Setup outcome is missing its file record: ${file.path}`);
|
|
1647
|
+
let required = file;
|
|
1648
|
+
if (scope.has(file.path)) {
|
|
1649
|
+
const declined = receiptMap.get(file.path)?.decision === "keep-local";
|
|
1650
|
+
required = resolvedRecord(file, contents.get(file.path), declined);
|
|
1651
|
+
}
|
|
1652
|
+
if (!isDeepStrictEqual2(actual, required)) throw new Error(`Setup outcome has invalid claims or template lineage: ${file.path}`);
|
|
1653
|
+
}
|
|
1654
|
+
if (manifest.files.some((file) => !expected.files.some((record) => record.path === file.path))) throw new Error("Setup outcome added unexpected managed file records.");
|
|
1655
|
+
for (const task of allTasks) {
|
|
1656
|
+
if (!isStarterSkillTarget(task.path)) continue;
|
|
1657
|
+
const baseline = skillBaselinePath(starterSkillNameFromTarget(task.path));
|
|
1658
|
+
const bytes = await readFile7(join11(repoDir, baseline), "utf8").catch(() => null);
|
|
1659
|
+
if (bytes !== contents.get(task.path)) throw new Error(`Setup skill baseline does not match live content: ${baseline}`);
|
|
1660
|
+
}
|
|
1661
|
+
const check = await verify(repoDir);
|
|
1662
|
+
if (check.failures.length) throw new Error(`Setup outcome validation failed: ${check.failures.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
1663
|
+
return manifest;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// src/commands/init.ts
|
|
1667
|
+
var VALID_HARNESSES = ["claude", "opencode"];
|
|
1668
|
+
var KINDS2 = ["skills", "commands"];
|
|
1669
|
+
function validateHarnesses(harnesses) {
|
|
1670
|
+
if (!harnesses) return void 0;
|
|
1671
|
+
for (const harness of harnesses) {
|
|
1672
|
+
if (!VALID_HARNESSES.includes(harness)) {
|
|
1673
|
+
throw new Error(`Invalid harness: ${String(harness)}`);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return harnesses;
|
|
1677
|
+
}
|
|
1678
|
+
function cancelInit(p) {
|
|
1679
|
+
p.cancel("Init cancelled.");
|
|
1680
|
+
throw new Error("Init cancelled.");
|
|
1681
|
+
}
|
|
1682
|
+
async function plannedAdapterSources(repoDir) {
|
|
1683
|
+
const sources = { skills: /* @__PURE__ */ new Map(), commands: /* @__PURE__ */ new Map() };
|
|
1684
|
+
for (const entry of SCAFFOLD) {
|
|
1685
|
+
for (const kind of KINDS2) {
|
|
1686
|
+
const prefix = `.agents/${kind}/`;
|
|
1687
|
+
if (entry.target.startsWith(prefix)) {
|
|
1688
|
+
const name = entry.target.slice(prefix.length).split("/")[0];
|
|
1689
|
+
sources[kind].set(name, join12(templatesDir(), "agents", kind, name));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
for (const kind of KINDS2) {
|
|
1694
|
+
const srcDir = join12(repoDir, ".agents", kind);
|
|
1695
|
+
for (const name of await readdir6(srcDir).catch(() => [])) {
|
|
1696
|
+
if (!sources[kind].has(name)) sources[kind].set(name, join12(srcDir, name));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return sources;
|
|
1700
|
+
}
|
|
1701
|
+
async function preflightAdapterCollisions(repoDir, harnesses, exposure) {
|
|
1702
|
+
const sources = await plannedAdapterSources(repoDir);
|
|
1703
|
+
for (const h of harnesses) {
|
|
1704
|
+
for (const kind of KINDS2) {
|
|
1705
|
+
for (const [name, source] of sources[kind]) {
|
|
1706
|
+
const adapterPath = join12(`.${h}`, kind, name);
|
|
1707
|
+
const target = join12(repoDir, adapterPath);
|
|
1708
|
+
const stat3 = await lstat7(target).catch(() => null);
|
|
1709
|
+
if (!stat3 || stat3.isSymbolicLink()) continue;
|
|
1710
|
+
if (exposure === "copy" && await entriesMatch(source, target)) continue;
|
|
1711
|
+
throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
async function runFreshInit(repoDir, flags, version) {
|
|
1717
|
+
const s = await scan(repoDir);
|
|
1718
|
+
let harnesses = validateHarnesses(flags.harnesses);
|
|
1719
|
+
let exposure = flags.exposure;
|
|
1720
|
+
if (!harnesses || !exposure) {
|
|
1721
|
+
if (flags.yes || !process.stdin.isTTY) {
|
|
1722
|
+
harnesses ??= s.harnessDirsPresent.length ? s.harnessDirsPresent : ["claude", "opencode"];
|
|
1723
|
+
exposure ??= s.symlinksSupported ? "symlink" : "copy";
|
|
1724
|
+
} else {
|
|
1725
|
+
const p = await import("@clack/prompts");
|
|
1726
|
+
if (!harnesses) {
|
|
1727
|
+
const selectedHarnesses = await p.multiselect({
|
|
1728
|
+
message: "Expose skills/commands to which harnesses?",
|
|
1729
|
+
options: [
|
|
1730
|
+
{ value: "claude", label: "Claude Code (.claude/)" },
|
|
1731
|
+
{ value: "opencode", label: "OpenCode (.opencode/)" }
|
|
1732
|
+
],
|
|
1733
|
+
initialValues: s.harnessDirsPresent.length ? s.harnessDirsPresent : ["claude", "opencode"]
|
|
1734
|
+
});
|
|
1735
|
+
if (p.isCancel(selectedHarnesses)) {
|
|
1736
|
+
cancelInit(p);
|
|
1737
|
+
}
|
|
1738
|
+
harnesses = validateHarnesses(selectedHarnesses) ?? [];
|
|
1739
|
+
}
|
|
1740
|
+
if (!exposure && s.symlinksSupported) {
|
|
1741
|
+
const selectedExposure = await p.select({
|
|
1742
|
+
message: "Exposure mode?",
|
|
1743
|
+
options: [
|
|
1744
|
+
{ value: "symlink", label: "Symlinks (recommended)" },
|
|
1745
|
+
{ value: "copy", label: "Copies (verify checks drift)" }
|
|
1746
|
+
]
|
|
1747
|
+
});
|
|
1748
|
+
if (p.isCancel(selectedExposure)) {
|
|
1749
|
+
cancelInit(p);
|
|
1750
|
+
}
|
|
1751
|
+
exposure = selectedExposure;
|
|
1752
|
+
} else {
|
|
1753
|
+
exposure ??= "copy";
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
await preflightAdapterCollisions(repoDir, harnesses, exposure);
|
|
1758
|
+
const gen = await generate(repoDir, version);
|
|
1759
|
+
const exposed = [];
|
|
1760
|
+
for (const h of harnesses) exposed.push(...await expose(repoDir, h, exposure));
|
|
1761
|
+
const manifest = {
|
|
1762
|
+
version: 2,
|
|
1763
|
+
bearingsVersion: version,
|
|
1764
|
+
harnesses,
|
|
1765
|
+
exposure,
|
|
1766
|
+
files: gen.files
|
|
1767
|
+
};
|
|
1768
|
+
const tailored = (await Promise.all(gen.files.map(async (file) => tailoringTask(file, await readFile8(join12(repoDir, file.path), "utf8"), true)))).filter((task) => task !== void 0);
|
|
1769
|
+
const seedAndMaps = tailored.filter((task) => task.path === "AGENTS.md" || task.reason === "map");
|
|
1770
|
+
const starterTasks = tailored.filter((task) => task.path !== "AGENTS.md" && task.reason !== "map");
|
|
1771
|
+
const tasks = uniqueSetupTasks([
|
|
1772
|
+
...seedAndMaps,
|
|
1773
|
+
{ path: C4_COMPONENT_PATH, reason: "map" },
|
|
1774
|
+
...starterTasks,
|
|
1775
|
+
...reconciliationTasks(manifest)
|
|
1776
|
+
]);
|
|
1777
|
+
manifest.setupPending = { kind: "init", toVersion: version, tasks, protectedSkills: await protectSkills(repoDir, tasks, [], setupArtifacts(manifest)) };
|
|
1778
|
+
await saveManifest(repoDir, manifest);
|
|
1779
|
+
return { manifest, report: renderInitReport(gen, exposed, manifest) };
|
|
1780
|
+
}
|
|
1781
|
+
async function runInit(repoDir, flags, version) {
|
|
1782
|
+
if (flags.completeSetup) return completeSetup(repoDir, { paths: flags.setupPaths, keepLocal: flags.keepLocal });
|
|
1783
|
+
if (flags.keepLocal !== void 0 || flags.setupPaths !== void 0) throw new Error("--keep-local and --setup-path require --complete-setup.");
|
|
1784
|
+
const state = await inspectManifest(repoDir);
|
|
1785
|
+
if (state.kind === "absent") return runFreshInit(repoDir, flags, version);
|
|
1786
|
+
const { runUpdate } = await import("./update-YY5SLPDF.js");
|
|
1787
|
+
return runUpdate(repoDir, flags, version, state);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
export {
|
|
1791
|
+
isChecklistPayload,
|
|
1792
|
+
templatesDir,
|
|
1793
|
+
starterSkillNameFromTarget,
|
|
1794
|
+
isStarterSkillTarget,
|
|
1795
|
+
skillBaselinePath,
|
|
1796
|
+
skillIncomingPath,
|
|
1797
|
+
SCAFFOLD,
|
|
1798
|
+
sha256,
|
|
1799
|
+
saveManifest,
|
|
1800
|
+
inspectManifest,
|
|
1801
|
+
migrateV1,
|
|
1802
|
+
freeBackupPath,
|
|
1803
|
+
ADAPTER_KINDS,
|
|
1804
|
+
canonicalAdapterEntries,
|
|
1805
|
+
entriesMatch,
|
|
1806
|
+
PLACEHOLDER,
|
|
1807
|
+
verify,
|
|
1808
|
+
tailoringTask,
|
|
1809
|
+
reconciliationTasks,
|
|
1810
|
+
uniqueSetupTasks,
|
|
1811
|
+
protectSkills,
|
|
1812
|
+
normalizeSetup,
|
|
1813
|
+
renderSetupScope,
|
|
1814
|
+
pendingSetupReport,
|
|
1815
|
+
setupArtifacts,
|
|
1816
|
+
renderUpdatePlan,
|
|
1817
|
+
renderUpdateReport,
|
|
1818
|
+
renderVerifyReport,
|
|
1819
|
+
applyUpdate,
|
|
1820
|
+
verifySetupOutcome,
|
|
1821
|
+
validateHarnesses,
|
|
1822
|
+
runInit
|
|
1823
|
+
};
|