@xiongxianfei/rigorloop 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -2
- package/dist/bin/rigorloop.js +564 -165
- package/dist/lib/adapters.js +50 -0
- package/dist/lib/lockfile.js +196 -29
- package/dist/metadata/adapter-artifacts-v0.2.0.json +30 -0
- package/dist/metadata/releases.json +4 -4
- package/package.json +1 -1
package/dist/bin/rigorloop.js
CHANGED
|
@@ -7,16 +7,13 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
import { EXIT, exitCodeForResult } from "../lib/command-result.js";
|
|
10
|
+
import { adapterDescriptor, supportedAdapterNames } from "../lib/adapters.js";
|
|
10
11
|
import { parseLockfile, serializeLockfile, sha256NormalizedText } from "../lib/lockfile.js";
|
|
11
12
|
import { buildNewChangeDraft, parseNewChangeArgs } from "../lib/new-change.js";
|
|
12
13
|
import { runNewChangePlan } from "../lib/new-change-filesystem.js";
|
|
13
14
|
import { validateOfficialArchiveUrl } from "../lib/official-archive-url.js";
|
|
14
15
|
|
|
15
|
-
const ADAPTER = "codex";
|
|
16
|
-
const AGENTS_ROOT = ".agents";
|
|
17
|
-
const INSTALL_ROOT = ".agents/skills";
|
|
18
16
|
const LOCKFILE_PATH = "rigorloop.lock";
|
|
19
|
-
const DIRECTORY_PLAN = [AGENTS_ROOT, INSTALL_ROOT];
|
|
20
17
|
|
|
21
18
|
function packageInfo() {
|
|
22
19
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -113,12 +110,13 @@ function usage() {
|
|
|
113
110
|
Usage:
|
|
114
111
|
rigorloop --help
|
|
115
112
|
rigorloop version
|
|
116
|
-
rigorloop init --adapter codex [--dry-run] [--json]
|
|
113
|
+
rigorloop init --adapter codex|claude|opencode [--dry-run] [--json]
|
|
117
114
|
rigorloop new-change <change-id> --title <title> [--dry-run] [--json]
|
|
118
115
|
|
|
119
116
|
Commands:
|
|
120
117
|
version Print package name and version.
|
|
121
|
-
init --adapter codex
|
|
118
|
+
init --adapter codex|claude|opencode
|
|
119
|
+
Initialize a verified adapter install plan.
|
|
122
120
|
new-change Plan a change metadata scaffold.
|
|
123
121
|
`;
|
|
124
122
|
}
|
|
@@ -127,43 +125,159 @@ function releaseForPackage(version) {
|
|
|
127
125
|
return `v${version}`;
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
function sourceForFlags(flags, info) {
|
|
128
|
+
function sourceForFlags(flags, info, descriptor) {
|
|
131
129
|
if (flags.fromArchiveProvided) {
|
|
132
130
|
return {
|
|
133
131
|
type: "local-archive",
|
|
134
|
-
archive: flags.fromArchive,
|
|
132
|
+
archive: basename(flags.fromArchive),
|
|
133
|
+
inputPath: flags.fromArchive,
|
|
135
134
|
};
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
return {
|
|
139
138
|
type: "release-archive",
|
|
140
139
|
release: releaseForPackage(info.version),
|
|
141
|
-
archive:
|
|
140
|
+
archive: descriptor.archiveName(releaseForPackage(info.version)),
|
|
142
141
|
};
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
function
|
|
144
|
+
function manifestAdapterBlock(source, descriptor, artifact) {
|
|
146
145
|
const sourceLines =
|
|
147
146
|
source.type === "local-archive"
|
|
148
147
|
? [` type: local-archive`, ` archive: "${source.archive}"`]
|
|
149
148
|
: [` type: release-archive`, ` release: "${source.release}"`];
|
|
149
|
+
const installRoots = rootsForArtifact(descriptor, artifact);
|
|
150
|
+
const rootLines =
|
|
151
|
+
descriptor.name !== "opencode" && Object.keys(installRoots).length === 1
|
|
152
|
+
? [` install_root: "${Object.values(installRoots)[0]}"`]
|
|
153
|
+
: [
|
|
154
|
+
` install_roots:`,
|
|
155
|
+
...Object.entries(installRoots).map(([role, root]) => ` ${role}: "${root}"`),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
return ` - name: ${descriptor.name}
|
|
159
|
+
${rootLines.join("\n")}
|
|
160
|
+
source:
|
|
161
|
+
${sourceLines.join("\n")}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseManifestAdapterBlocks(content) {
|
|
165
|
+
if (!content.includes("schema_version: 1") || !content.includes("adapters:")) {
|
|
166
|
+
return { error: { code: "invalid-config", message: "Existing rigorloop.yaml is not compatible with the init contract." } };
|
|
167
|
+
}
|
|
168
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
169
|
+
const adapterStart = lines.findIndex((line) => line === "adapters:");
|
|
170
|
+
const blocks = [];
|
|
171
|
+
let current = [];
|
|
172
|
+
for (const line of lines.slice(adapterStart + 1)) {
|
|
173
|
+
if (line.startsWith(" - name: ")) {
|
|
174
|
+
if (current.length) {
|
|
175
|
+
blocks.push(current);
|
|
176
|
+
}
|
|
177
|
+
current = [line];
|
|
178
|
+
} else if (current.length) {
|
|
179
|
+
current.push(line);
|
|
180
|
+
} else if (line.trim() !== "") {
|
|
181
|
+
return { error: { code: "invalid-config", message: "Existing rigorloop.yaml has malformed adapter entries." } };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (current.length) {
|
|
185
|
+
blocks.push(current);
|
|
186
|
+
}
|
|
187
|
+
const adapters = [];
|
|
188
|
+
for (const block of blocks) {
|
|
189
|
+
const name = block[0].slice(" - name: ".length).trim();
|
|
190
|
+
const descriptor = adapterDescriptor(name);
|
|
191
|
+
if (!descriptor) {
|
|
192
|
+
return { error: { code: "invalid-config", message: `Existing rigorloop.yaml includes unsupported adapter ${name}.` } };
|
|
193
|
+
}
|
|
194
|
+
adapters.push({ name, block: block.join("\n").replace(/\n+$/, "") });
|
|
195
|
+
}
|
|
196
|
+
return { adapters };
|
|
197
|
+
}
|
|
150
198
|
|
|
199
|
+
function manifestContent(info, source, descriptor, existingContent) {
|
|
200
|
+
const selectedBlock = manifestAdapterBlock(source, descriptor, source.artifact);
|
|
201
|
+
if (existingContent) {
|
|
202
|
+
const parsed = parseManifestAdapterBlocks(existingContent);
|
|
203
|
+
if (!parsed.error) {
|
|
204
|
+
const preserved = parsed.adapters.filter((entry) => entry.name !== descriptor.name).map((entry) => entry.block);
|
|
205
|
+
return `schema_version: 1
|
|
206
|
+
rigorloop:
|
|
207
|
+
package: "${info.name}"
|
|
208
|
+
package_version: "${info.version}"
|
|
209
|
+
adapters:
|
|
210
|
+
${[...preserved, selectedBlock].join("\n")}
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
151
214
|
return `schema_version: 1
|
|
152
215
|
rigorloop:
|
|
153
216
|
package: "${info.name}"
|
|
154
217
|
package_version: "${info.version}"
|
|
155
218
|
adapters:
|
|
156
|
-
|
|
157
|
-
install_root: "${INSTALL_ROOT}"
|
|
158
|
-
source:
|
|
159
|
-
${sourceLines.join("\n")}
|
|
219
|
+
${selectedBlock}
|
|
160
220
|
`;
|
|
161
221
|
}
|
|
162
222
|
|
|
163
|
-
function
|
|
223
|
+
function rootsForArtifact(descriptor, artifact) {
|
|
224
|
+
if (artifact?.install_roots) {
|
|
225
|
+
return artifact.install_roots;
|
|
226
|
+
}
|
|
227
|
+
if (artifact?.install_root) {
|
|
228
|
+
return { skills: artifact.install_root };
|
|
229
|
+
}
|
|
230
|
+
return descriptor.installRoots;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function rootHashesForArtifact(descriptor, artifact) {
|
|
234
|
+
if (artifact?.root_hashes) {
|
|
235
|
+
return artifact.root_hashes;
|
|
236
|
+
}
|
|
237
|
+
return Object.fromEntries(
|
|
238
|
+
Object.keys(rootsForArtifact(descriptor, artifact)).map((role) => [
|
|
239
|
+
role,
|
|
240
|
+
{
|
|
241
|
+
tree_sha256: artifact?.tree_sha256 ?? "<planned-after-install>",
|
|
242
|
+
file_count: artifact?.file_count ?? "<planned-after-install>",
|
|
243
|
+
},
|
|
244
|
+
]),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function usesMultiRootLockfile(descriptor, artifact) {
|
|
249
|
+
return descriptor.name === "opencode" || Object.keys(rootsForArtifact(descriptor, artifact)).length > 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes = rootHashesForArtifact(descriptor, artifact)) {
|
|
253
|
+
const entry = {
|
|
254
|
+
adapter: descriptor.name,
|
|
255
|
+
release: releaseForPackage(info.version),
|
|
256
|
+
source: source.type,
|
|
257
|
+
archive: source.archive,
|
|
258
|
+
archive_sha256: artifact?.sha256 ?? "<planned>",
|
|
259
|
+
tree_hash_algorithm: "rigorloop-tree-hash-v1",
|
|
260
|
+
};
|
|
261
|
+
if (usesMultiRootLockfile(descriptor, artifact)) {
|
|
262
|
+
return {
|
|
263
|
+
...entry,
|
|
264
|
+
installed_roots: rootsForArtifact(descriptor, artifact),
|
|
265
|
+
root_hashes: rootHashes,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const [role] = Object.keys(rootsForArtifact(descriptor, artifact));
|
|
269
|
+
return {
|
|
270
|
+
...entry,
|
|
271
|
+
installed_root: rootsForArtifact(descriptor, artifact)[role],
|
|
272
|
+
tree_sha256: rootHashes[role].tree_sha256,
|
|
273
|
+
file_count: rootHashes[role].file_count,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function plannedLockfile(info, source, manifest, descriptor) {
|
|
164
278
|
const artifact = source.artifact;
|
|
165
279
|
return {
|
|
166
|
-
schema_version:
|
|
280
|
+
schema_version: 2,
|
|
167
281
|
rigorloop: {
|
|
168
282
|
package: info.name,
|
|
169
283
|
version: info.version,
|
|
@@ -173,26 +287,30 @@ function plannedLockfile(info, source, manifest) {
|
|
|
173
287
|
sha256: sha256NormalizedText(manifest),
|
|
174
288
|
},
|
|
175
289
|
generated: {
|
|
176
|
-
adapters: [
|
|
177
|
-
{
|
|
178
|
-
adapter: ADAPTER,
|
|
179
|
-
release: releaseForPackage(info.version),
|
|
180
|
-
source: source.type,
|
|
181
|
-
archive: source.type === "local-archive" ? basename(source.archive) : source.archive,
|
|
182
|
-
archive_sha256: artifact?.sha256 ?? "<planned>",
|
|
183
|
-
installed_root: INSTALL_ROOT,
|
|
184
|
-
tree_hash_algorithm: "rigorloop-tree-hash-v1",
|
|
185
|
-
tree_sha256: artifact?.tree_sha256 ?? "<planned-after-install>",
|
|
186
|
-
file_count: "<planned-after-install>",
|
|
187
|
-
},
|
|
188
|
-
],
|
|
290
|
+
adapters: [lockfileEntryForAdapter(info, source, artifact, descriptor)],
|
|
189
291
|
},
|
|
190
292
|
};
|
|
191
293
|
}
|
|
192
294
|
|
|
193
|
-
function
|
|
295
|
+
function existingLockfileEntries(selectedAdapter) {
|
|
296
|
+
const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
297
|
+
if (!existsSync(lockfileAbsolutePath)) {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
|
|
301
|
+
if (!parsed.ok) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
return parsed.lockfile.generated.adapters.filter((entry) => entry.adapter !== selectedAdapter);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function lockfileForVerifiedInstall(info, source, manifest, artifact, rootHashes, descriptor) {
|
|
308
|
+
const adapters = [
|
|
309
|
+
...existingLockfileEntries(descriptor.name),
|
|
310
|
+
lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes),
|
|
311
|
+
];
|
|
194
312
|
return {
|
|
195
|
-
schema_version:
|
|
313
|
+
schema_version: 2,
|
|
196
314
|
rigorloop: {
|
|
197
315
|
package: info.name,
|
|
198
316
|
version: info.version,
|
|
@@ -202,28 +320,19 @@ function lockfileForVerifiedInstall(info, source, manifest, artifact, treeHash,
|
|
|
202
320
|
sha256: sha256NormalizedText(manifest),
|
|
203
321
|
},
|
|
204
322
|
generated: {
|
|
205
|
-
adapters
|
|
206
|
-
{
|
|
207
|
-
adapter: ADAPTER,
|
|
208
|
-
release: releaseForPackage(info.version),
|
|
209
|
-
source: source.type,
|
|
210
|
-
archive: source.type === "local-archive" ? basename(source.archive) : source.archive,
|
|
211
|
-
archive_sha256: artifact.sha256,
|
|
212
|
-
installed_root: INSTALL_ROOT,
|
|
213
|
-
tree_hash_algorithm: "rigorloop-tree-hash-v1",
|
|
214
|
-
tree_sha256: treeHash,
|
|
215
|
-
file_count: fileCount,
|
|
216
|
-
},
|
|
217
|
-
],
|
|
323
|
+
adapters,
|
|
218
324
|
},
|
|
219
325
|
};
|
|
220
326
|
}
|
|
221
327
|
|
|
222
|
-
function compatibleManifest(content) {
|
|
328
|
+
function compatibleManifest(content, descriptor, artifact) {
|
|
329
|
+
if (descriptor.name === "opencode" && !rootsForArtifact(descriptor, artifact).commands && content.includes(".opencode/commands")) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
223
332
|
return (
|
|
224
333
|
content.includes("schema_version: 1") &&
|
|
225
|
-
content.includes(
|
|
226
|
-
content.includes(`
|
|
334
|
+
content.includes(`name: ${descriptor.name}`) &&
|
|
335
|
+
content.includes(`"${Object.values(rootsForArtifact(descriptor, artifact))[0]}"`)
|
|
227
336
|
);
|
|
228
337
|
}
|
|
229
338
|
|
|
@@ -234,8 +343,11 @@ function pathState(path) {
|
|
|
234
343
|
return statSync(path).isDirectory() ? "directory" : "file";
|
|
235
344
|
}
|
|
236
345
|
|
|
237
|
-
function directoryKind(path) {
|
|
238
|
-
|
|
346
|
+
function directoryKind(path, descriptor, artifact) {
|
|
347
|
+
if (Object.values(rootsForArtifact(descriptor, artifact)).includes(path)) {
|
|
348
|
+
return `${descriptor.name}-install-root`;
|
|
349
|
+
}
|
|
350
|
+
return `${descriptor.name}-adapter-root`;
|
|
239
351
|
}
|
|
240
352
|
|
|
241
353
|
function sha256(bytes) {
|
|
@@ -338,11 +450,77 @@ function loadJsonFile(path) {
|
|
|
338
450
|
async function fetchBytes(url) {
|
|
339
451
|
const response = await fetch(url);
|
|
340
452
|
if (!response.ok) {
|
|
341
|
-
throw new Error(`HTTP ${response.status}`);
|
|
453
|
+
throw Object.assign(new Error(`HTTP ${response.status}`), { downloadFailureClass: "http-status" });
|
|
342
454
|
}
|
|
343
455
|
return Buffer.from(await response.arrayBuffer());
|
|
344
456
|
}
|
|
345
457
|
|
|
458
|
+
const PROXY_ENV_VAR_ALLOWLIST = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"];
|
|
459
|
+
const DOWNLOAD_FAILURE_CLASSES = new Set(["dns", "tls", "timeout", "http-status", "proxy", "network", "unknown"]);
|
|
460
|
+
|
|
461
|
+
function detectedProxyEnvVars(env = process.env) {
|
|
462
|
+
return PROXY_ENV_VAR_ALLOWLIST.filter((name) => Object.prototype.hasOwnProperty.call(env, name) && env[name]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function nodeEnvProxyStatus(env = process.env, execArgv = process.execArgv) {
|
|
466
|
+
const nodeOptions = String(env.NODE_OPTIONS ?? "");
|
|
467
|
+
const useEnvProxy = String(env.NODE_USE_ENV_PROXY ?? "").toLowerCase();
|
|
468
|
+
// CR-M4-R1-F1: Node can enable fetch env-proxy via env vars or the runtime flag.
|
|
469
|
+
if (nodeOptions.includes("--use-env-proxy") || execArgv.includes("--use-env-proxy") || ["1", "true", "yes"].includes(useEnvProxy)) {
|
|
470
|
+
return "enabled";
|
|
471
|
+
}
|
|
472
|
+
if (detectedProxyEnvVars(env).length > 0) {
|
|
473
|
+
return "disabled";
|
|
474
|
+
}
|
|
475
|
+
return "unknown";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function downloadFailureClass(error) {
|
|
479
|
+
if (DOWNLOAD_FAILURE_CLASSES.has(error?.downloadFailureClass)) {
|
|
480
|
+
return error.downloadFailureClass;
|
|
481
|
+
}
|
|
482
|
+
const code = String(error?.code ?? error?.cause?.code ?? "").toUpperCase();
|
|
483
|
+
const message = String(error?.message ?? "").toLowerCase();
|
|
484
|
+
if (["ENOTFOUND", "EAI_AGAIN"].includes(code)) {
|
|
485
|
+
return "dns";
|
|
486
|
+
}
|
|
487
|
+
if (code.includes("CERT") || code.includes("TLS") || message.includes("certificate") || message.includes("tls")) {
|
|
488
|
+
return "tls";
|
|
489
|
+
}
|
|
490
|
+
if (code.includes("TIMEOUT") || code === "ABORT_ERR" || message.includes("timeout") || message.includes("timed out")) {
|
|
491
|
+
return "timeout";
|
|
492
|
+
}
|
|
493
|
+
if (code.includes("PROXY") || message.includes("proxy")) {
|
|
494
|
+
return "proxy";
|
|
495
|
+
}
|
|
496
|
+
if (["ECONNRESET", "ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"].includes(code) || message.includes("fetch failed")) {
|
|
497
|
+
return "network";
|
|
498
|
+
}
|
|
499
|
+
return "unknown";
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function downloadFailureDiagnostics(error, artifact, descriptor, metadata) {
|
|
503
|
+
return {
|
|
504
|
+
adapter: descriptor.name,
|
|
505
|
+
release: metadata.release.version,
|
|
506
|
+
archive_url: artifact.url,
|
|
507
|
+
download_failure_class: downloadFailureClass(error),
|
|
508
|
+
node_env_proxy_status: nodeEnvProxyStatus(),
|
|
509
|
+
proxy_env_vars_detected: detectedProxyEnvVars(),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function downloadFailureBlocker(error, artifact, descriptor, metadata) {
|
|
514
|
+
const diagnostics = downloadFailureDiagnostics(error, artifact, descriptor, metadata);
|
|
515
|
+
return {
|
|
516
|
+
code: "release-download-failed",
|
|
517
|
+
message: `Network download failed for adapter ${descriptor.name} release ${metadata.release.version} (failure class ${diagnostics.download_failure_class}).`,
|
|
518
|
+
path: artifact.url,
|
|
519
|
+
next_action: `Download ${artifact.url} and rerun with --from-archive ./${artifact.archive}.`,
|
|
520
|
+
diagnostics,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
346
524
|
function parseVerifiedMetadataBytes(bytes, expectedSha256) {
|
|
347
525
|
const actualSha256 = sha256(bytes);
|
|
348
526
|
if (actualSha256 !== expectedSha256) {
|
|
@@ -382,7 +560,15 @@ function isSha256(value) {
|
|
|
382
560
|
return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
|
|
383
561
|
}
|
|
384
562
|
|
|
385
|
-
function
|
|
563
|
+
function releaseListedForSkillsOnlyCompatibility(marker, release) {
|
|
564
|
+
if (!marker) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
const releases = Array.isArray(marker) ? marker : marker.releases;
|
|
568
|
+
return Array.isArray(releases) && releases.includes(release);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function validateMetadata(metadata, info, descriptor) {
|
|
386
572
|
const release = releaseForPackage(info.version);
|
|
387
573
|
if (!metadata || metadata.schema_version !== 1) {
|
|
388
574
|
return { error: { code: "metadata-invalid", message: "Adapter metadata schema_version must be 1." } };
|
|
@@ -407,26 +593,76 @@ function validateMetadata(metadata, info) {
|
|
|
407
593
|
if (!isNonEmptyString(metadata.validation?.command)) {
|
|
408
594
|
return { error: { code: "metadata-invalid", message: "Adapter metadata validation command is missing." } };
|
|
409
595
|
}
|
|
410
|
-
const artifact = metadata.artifacts?.find((entry) => entry.adapter ===
|
|
596
|
+
const artifact = metadata.artifacts?.find((entry) => entry.adapter === descriptor.name);
|
|
411
597
|
if (!artifact) {
|
|
412
|
-
return { blocker: metadataBlocker("
|
|
598
|
+
return { blocker: metadataBlocker("metadata-unavailable", `Adapter metadata does not include ${descriptor.displayName}.`) };
|
|
413
599
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
!isNonEmptyString(artifact.url) ||
|
|
417
|
-
!isSha256(artifact.sha256) ||
|
|
418
|
-
!Number.isInteger(artifact.size_bytes) ||
|
|
419
|
-
artifact.size_bytes < 0 ||
|
|
420
|
-
!isSha256(artifact.tree_sha256)
|
|
421
|
-
) {
|
|
422
|
-
return { error: { code: "metadata-invalid", message: "Codex adapter artifact metadata is incomplete." } };
|
|
423
|
-
}
|
|
424
|
-
if ((artifact.install_root ?? "").replace(/\/$/, "") !== INSTALL_ROOT) {
|
|
425
|
-
return { error: { code: "metadata-invalid", message: "Codex adapter install root is not .agents/skills." } };
|
|
600
|
+
if (!isNonEmptyString(artifact.archive) || !isNonEmptyString(artifact.url) || !isSha256(artifact.sha256) || !Number.isInteger(artifact.size_bytes) || artifact.size_bytes < 0) {
|
|
601
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter artifact metadata is incomplete.` } };
|
|
426
602
|
}
|
|
427
603
|
if (artifact.tree_hash_algorithm && artifact.tree_hash_algorithm !== "rigorloop-tree-hash-v1") {
|
|
428
604
|
return { error: { code: "metadata-invalid", message: "Unsupported tree hash algorithm in adapter metadata." } };
|
|
429
605
|
}
|
|
606
|
+
if (artifact.install_roots || artifact.root_hashes) {
|
|
607
|
+
if (!artifact.install_roots || !artifact.root_hashes) {
|
|
608
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} multi-root metadata is incomplete.` } };
|
|
609
|
+
}
|
|
610
|
+
for (const [role, root] of Object.entries(artifact.install_roots)) {
|
|
611
|
+
if (descriptor.installRoots[role] !== root) {
|
|
612
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter install root for ${role} is not supported.` } };
|
|
613
|
+
}
|
|
614
|
+
const rootHash = artifact.root_hashes[role];
|
|
615
|
+
if (!rootHash || !isSha256(rootHash.tree_sha256) || !Number.isInteger(rootHash.file_count) || rootHash.file_count < 0) {
|
|
616
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} root hash metadata is incomplete.` } };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
if (!isSha256(artifact.tree_sha256) || !Number.isInteger(artifact.file_count) || artifact.file_count < 0) {
|
|
621
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter single-root metadata is incomplete.` } };
|
|
622
|
+
}
|
|
623
|
+
if ((artifact.install_root ?? "").replace(/\/$/, "") !== descriptor.primaryInstallRoot()) {
|
|
624
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter install root is not ${descriptor.primaryInstallRoot()}.` } };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (artifact.command_aliases?.opencode) {
|
|
628
|
+
if (descriptor.name !== "opencode" || !artifact.install_roots?.commands) {
|
|
629
|
+
return { error: { code: "metadata-invalid", message: "opencode command alias metadata requires the opencode commands root." } };
|
|
630
|
+
}
|
|
631
|
+
const aliasPaths = opencodeCommandAliasPaths(artifact);
|
|
632
|
+
if (
|
|
633
|
+
!Number.isInteger(artifact.command_aliases.opencode.count) ||
|
|
634
|
+
!Array.isArray(aliasPaths) ||
|
|
635
|
+
artifact.command_aliases.opencode.count !== aliasPaths.length ||
|
|
636
|
+
aliasPaths.some((aliasPath) => !isNonEmptyString(aliasPath) || !aliasPath.startsWith(`${descriptor.installRoots.commands}/`))
|
|
637
|
+
) {
|
|
638
|
+
return { error: { code: "metadata-invalid", message: "opencode command alias metadata is incomplete." } };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// CR-M3-R2-F1: opencode commands root is valid only with declared command aliases.
|
|
642
|
+
if (descriptor.name === "opencode" && artifact.install_roots?.commands && !artifact.command_aliases?.opencode) {
|
|
643
|
+
return {
|
|
644
|
+
blocker: metadataBlocker(
|
|
645
|
+
"opencode-command-aliases-missing",
|
|
646
|
+
"Opencode commands root metadata requires command_aliases.opencode.",
|
|
647
|
+
artifact.archive,
|
|
648
|
+
"Use opencode metadata that declares command aliases, or use explicitly compatible skills-only metadata without the commands root.",
|
|
649
|
+
),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
// CR-M3-R1-F1: skills-only opencode compatibility must be explicit in trusted metadata.
|
|
653
|
+
if (descriptor.name === "opencode" && !artifact.command_aliases?.opencode && !rootsForArtifact(descriptor, artifact).commands) {
|
|
654
|
+
const marker = artifact.skills_only_compatibility ?? metadata.compatibility?.opencode_skills_only;
|
|
655
|
+
if (!releaseListedForSkillsOnlyCompatibility(marker, release)) {
|
|
656
|
+
return {
|
|
657
|
+
blocker: metadataBlocker(
|
|
658
|
+
"opencode-skills-only-compatibility-unmarked",
|
|
659
|
+
"Opencode skills-only archive metadata is not explicitly marked compatible.",
|
|
660
|
+
artifact.archive,
|
|
661
|
+
"Use an opencode archive with command alias metadata or bundled trusted skills-only compatibility metadata.",
|
|
662
|
+
),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
430
666
|
return { artifact };
|
|
431
667
|
}
|
|
432
668
|
|
|
@@ -500,14 +736,15 @@ function parseZipEntries(buffer) {
|
|
|
500
736
|
return entries;
|
|
501
737
|
}
|
|
502
738
|
|
|
503
|
-
function unsafePathCode(name) {
|
|
739
|
+
function unsafePathCode(name, descriptor, artifact) {
|
|
504
740
|
if (!name || name.startsWith("/") || name.startsWith("\\") || /^[A-Za-z]:/.test(name) || name.includes("\\")) {
|
|
505
741
|
return "archive-path-traversal";
|
|
506
742
|
}
|
|
507
743
|
if (name.split("/").some((part) => part === ".." || part === "")) {
|
|
508
744
|
return "archive-path-traversal";
|
|
509
745
|
}
|
|
510
|
-
|
|
746
|
+
const allowedRoots = Object.values(rootsForArtifact(descriptor, artifact));
|
|
747
|
+
if (!allowedRoots.some((root) => name.startsWith(`${root}/`))) {
|
|
511
748
|
return "archive-install-root-invalid";
|
|
512
749
|
}
|
|
513
750
|
return undefined;
|
|
@@ -517,19 +754,48 @@ function isArchiveSupportEntry(name) {
|
|
|
517
754
|
return name === "AGENTS.md";
|
|
518
755
|
}
|
|
519
756
|
|
|
520
|
-
function
|
|
757
|
+
function fileRowsForTreeRoot(entries, installRoot) {
|
|
521
758
|
return entries
|
|
522
|
-
.filter((entry) => !entry.directory)
|
|
759
|
+
.filter((entry) => !entry.directory && entry.name.startsWith(`${installRoot}/`))
|
|
523
760
|
.map((entry) => {
|
|
524
|
-
const relativePath = entry.name.slice(`${
|
|
761
|
+
const relativePath = entry.name.slice(`${installRoot}/`.length);
|
|
525
762
|
const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
|
|
526
763
|
return [relativePath, sha256(bytes)];
|
|
527
764
|
})
|
|
528
765
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
529
766
|
}
|
|
530
767
|
|
|
531
|
-
function
|
|
532
|
-
|
|
768
|
+
function opencodeCommandAliasPaths(artifact) {
|
|
769
|
+
const aliases = artifact?.command_aliases?.opencode;
|
|
770
|
+
if (!aliases) {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
if (Array.isArray(aliases.paths)) {
|
|
774
|
+
return aliases.paths;
|
|
775
|
+
}
|
|
776
|
+
if (aliases.aliases && typeof aliases.aliases === "object") {
|
|
777
|
+
return Object.values(aliases.aliases);
|
|
778
|
+
}
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function treeHashForEntries(entries, descriptor) {
|
|
783
|
+
return treeHashForRows(fileRowsForTreeRoot(entries, descriptor.primaryInstallRoot()));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function rootHashesForEntries(entries, descriptor, artifact) {
|
|
787
|
+
return Object.fromEntries(
|
|
788
|
+
Object.entries(rootsForArtifact(descriptor, artifact)).map(([role, root]) => {
|
|
789
|
+
const rows = fileRowsForTreeRoot(entries, root);
|
|
790
|
+
return [
|
|
791
|
+
role,
|
|
792
|
+
{
|
|
793
|
+
tree_sha256: treeHashForRows(rows),
|
|
794
|
+
file_count: rows.length,
|
|
795
|
+
},
|
|
796
|
+
];
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
533
799
|
}
|
|
534
800
|
|
|
535
801
|
function treeHashForRows(rows) {
|
|
@@ -573,22 +839,26 @@ function treeHashForFilesystem(root) {
|
|
|
573
839
|
};
|
|
574
840
|
}
|
|
575
841
|
|
|
576
|
-
function
|
|
842
|
+
function currentLockfileEntries() {
|
|
577
843
|
const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
578
844
|
if (!existsSync(lockfileAbsolutePath)) {
|
|
579
|
-
return
|
|
845
|
+
return [];
|
|
580
846
|
}
|
|
581
847
|
const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
|
|
582
848
|
if (!parsed.ok) {
|
|
583
|
-
return
|
|
849
|
+
return [];
|
|
584
850
|
}
|
|
585
|
-
return parsed.lockfile.generated.adapters
|
|
851
|
+
return parsed.lockfile.generated.adapters;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function currentLockfileEntry(descriptor) {
|
|
855
|
+
return currentLockfileEntries().find((entry) => entry.adapter === descriptor.name);
|
|
586
856
|
}
|
|
587
857
|
|
|
588
858
|
function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) {
|
|
589
859
|
return {
|
|
590
860
|
code: "installed-tree-mismatch",
|
|
591
|
-
message: "Installed
|
|
861
|
+
message: "Installed adapter tree does not match trusted metadata.",
|
|
592
862
|
expected_tree_sha256: expectedTreeHash,
|
|
593
863
|
actual_tree_sha256: actualTree.treeHash,
|
|
594
864
|
expected_file_count: expectedFileCount,
|
|
@@ -596,23 +866,29 @@ function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCo
|
|
|
596
866
|
};
|
|
597
867
|
}
|
|
598
868
|
|
|
599
|
-
function verifyInstalledTree(entries, artifact, { allowMissingOrEmpty = false } = {}) {
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
|
|
869
|
+
function verifyInstalledTree(entries, artifact, descriptor, { allowMissingOrEmpty = false } = {}) {
|
|
870
|
+
const expectedRootHashes = rootHashesForEntries(entries, descriptor, artifact);
|
|
871
|
+
for (const [role, root] of Object.entries(rootsForArtifact(descriptor, artifact))) {
|
|
872
|
+
const expectedRows = fileRowsForTreeRoot(entries, root);
|
|
873
|
+
const expectedTreeHash = artifact.root_hashes?.[role]?.tree_sha256 ?? artifact.tree_sha256;
|
|
874
|
+
const expectedFileCount = artifact.root_hashes?.[role]?.file_count ?? expectedRows.length;
|
|
603
875
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
876
|
+
if (!existsSync(resolve(process.cwd(), root))) {
|
|
877
|
+
if (allowMissingOrEmpty) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
return { error: installedTreeMismatchError({ treeHash: "<missing>", fileCount: 0 }, expectedTreeHash, expectedFileCount) };
|
|
881
|
+
}
|
|
607
882
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
883
|
+
const actualTree = treeHashForFilesystem(root);
|
|
884
|
+
if (allowMissingOrEmpty && actualTree.fileCount === 0) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (!rowsEqual(actualTree.rows, expectedRows) || actualTree.treeHash !== expectedTreeHash || actualTree.fileCount !== expectedFileCount) {
|
|
888
|
+
return { error: installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) };
|
|
889
|
+
}
|
|
614
890
|
}
|
|
615
|
-
return { ok: true,
|
|
891
|
+
return { ok: true, rootHashes: expectedRootHashes, expectedFileCount: expectedRootHashes.skills?.file_count ?? 0, treeHash: expectedRootHashes.skills?.tree_sha256 };
|
|
616
892
|
}
|
|
617
893
|
|
|
618
894
|
function generatedOutputConflictBlocker(entries) {
|
|
@@ -655,6 +931,21 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
655
931
|
if (!lockfileEntry) {
|
|
656
932
|
return undefined;
|
|
657
933
|
}
|
|
934
|
+
if (lockfileEntry.installed_roots) {
|
|
935
|
+
for (const [role, root] of Object.entries(lockfileEntry.installed_roots)) {
|
|
936
|
+
const rootHash = lockfileEntry.root_hashes[role];
|
|
937
|
+
const blocker = lockfileDriftBlocker({
|
|
938
|
+
adapter: lockfileEntry.adapter,
|
|
939
|
+
installed_root: root,
|
|
940
|
+
tree_sha256: rootHash.tree_sha256,
|
|
941
|
+
file_count: rootHash.file_count,
|
|
942
|
+
});
|
|
943
|
+
if (blocker) {
|
|
944
|
+
return blocker;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return undefined;
|
|
948
|
+
}
|
|
658
949
|
|
|
659
950
|
const rootState = pathState(resolve(process.cwd(), lockfileEntry.installed_root));
|
|
660
951
|
if (rootState === "absent") {
|
|
@@ -695,7 +986,17 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
695
986
|
return undefined;
|
|
696
987
|
}
|
|
697
988
|
|
|
698
|
-
function
|
|
989
|
+
function firstLockfileDriftBlocker() {
|
|
990
|
+
for (const entry of currentLockfileEntries()) {
|
|
991
|
+
const blocker = lockfileDriftBlocker(entry);
|
|
992
|
+
if (blocker) {
|
|
993
|
+
return blocker;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function inspectArchive(archiveBytes, artifact, descriptor) {
|
|
699
1000
|
if (artifact.size_bytes !== undefined && archiveBytes.length !== artifact.size_bytes) {
|
|
700
1001
|
return { error: { code: "archive-size-mismatch", message: "Archive size does not match metadata." } };
|
|
701
1002
|
}
|
|
@@ -716,7 +1017,7 @@ function inspectArchive(archiveBytes, artifact) {
|
|
|
716
1017
|
if (isArchiveSupportEntry(entry.name)) {
|
|
717
1018
|
continue;
|
|
718
1019
|
}
|
|
719
|
-
const pathCode = unsafePathCode(entry.name);
|
|
1020
|
+
const pathCode = unsafePathCode(entry.name, descriptor, artifact);
|
|
720
1021
|
if (pathCode) {
|
|
721
1022
|
return { error: { code: pathCode, message: `Archive entry is not allowed: ${entry.name}`, path: entry.name } };
|
|
722
1023
|
}
|
|
@@ -727,14 +1028,34 @@ function inspectArchive(archiveBytes, artifact) {
|
|
|
727
1028
|
}
|
|
728
1029
|
|
|
729
1030
|
const files = installEntries.filter((entry) => !entry.directory);
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
1031
|
+
const rootHashes = rootHashesForEntries(files, descriptor, artifact);
|
|
1032
|
+
for (const [role, hash] of Object.entries(rootHashes)) {
|
|
1033
|
+
const expected = artifact.root_hashes?.[role] ?? { tree_sha256: artifact.tree_sha256, file_count: artifact.file_count };
|
|
1034
|
+
if (expected.tree_sha256 && hash.tree_sha256 !== expected.tree_sha256) {
|
|
1035
|
+
return { error: { code: "tree-hash-mismatch", message: "Installed tree hash does not match metadata." } };
|
|
1036
|
+
}
|
|
1037
|
+
if (expected.file_count !== undefined && hash.file_count !== expected.file_count) {
|
|
1038
|
+
return { error: { code: "tree-hash-mismatch", message: "Installed tree file count does not match metadata." } };
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (descriptor.name === "opencode") {
|
|
1042
|
+
for (const aliasPath of opencodeCommandAliasPaths(artifact) ?? []) {
|
|
1043
|
+
if (!files.some((entry) => entry.name === aliasPath)) {
|
|
1044
|
+
return {
|
|
1045
|
+
error: {
|
|
1046
|
+
code: "opencode-command-alias-missing",
|
|
1047
|
+
message: "Declared opencode command alias is missing from archive.",
|
|
1048
|
+
path: aliasPath,
|
|
1049
|
+
},
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
733
1053
|
}
|
|
734
|
-
return { entries: files, archiveHash, treeHash, fileCount: files.length };
|
|
1054
|
+
return { entries: files, archiveHash, rootHashes, treeHash: rootHashes.skills?.tree_sha256, fileCount: rootHashes.skills?.file_count ?? files.length };
|
|
735
1055
|
}
|
|
736
1056
|
|
|
737
|
-
function addArchiveActions(plan, entries) {
|
|
1057
|
+
function addArchiveActions(plan, entries, descriptor) {
|
|
1058
|
+
const installRoot = descriptor.primaryInstallRoot();
|
|
738
1059
|
const directories = new Set();
|
|
739
1060
|
for (const entry of entries) {
|
|
740
1061
|
const parts = entry.name.split("/");
|
|
@@ -770,7 +1091,7 @@ function addArchiveActions(plan, entries) {
|
|
|
770
1091
|
const state = pathState(resolve(process.cwd(), entry.name));
|
|
771
1092
|
let existingMatches = false;
|
|
772
1093
|
if (state === "file") {
|
|
773
|
-
const relativePath = entry.name.slice(`${
|
|
1094
|
+
const relativePath = entry.name.slice(`${installRoot}/`.length);
|
|
774
1095
|
const existingBytes = relativePath.endsWith(".md")
|
|
775
1096
|
? normalizeText(readFileSync(resolve(process.cwd(), entry.name)))
|
|
776
1097
|
: readFileSync(resolve(process.cwd(), entry.name));
|
|
@@ -783,9 +1104,9 @@ function addArchiveActions(plan, entries) {
|
|
|
783
1104
|
status: state === "absent" ? "pending" : existingMatches ? "skipped" : "blocked",
|
|
784
1105
|
reason:
|
|
785
1106
|
state === "absent"
|
|
786
|
-
?
|
|
1107
|
+
? `Install verified ${descriptor.displayName} adapter file.`
|
|
787
1108
|
: existingMatches
|
|
788
|
-
? `${entry.name} already matches verified
|
|
1109
|
+
? `${entry.name} already matches verified ${descriptor.displayName} adapter content.`
|
|
789
1110
|
: `${entry.name} already exists.`,
|
|
790
1111
|
});
|
|
791
1112
|
plan.artifacts.push({
|
|
@@ -804,23 +1125,47 @@ function addArchiveActions(plan, entries) {
|
|
|
804
1125
|
}
|
|
805
1126
|
}
|
|
806
1127
|
|
|
807
|
-
function writeArchiveEntries(entries) {
|
|
1128
|
+
function writeArchiveEntries(entries, descriptor) {
|
|
1129
|
+
const installRoot = descriptor.primaryInstallRoot();
|
|
808
1130
|
for (const entry of entries) {
|
|
809
1131
|
const outputPath = resolve(process.cwd(), entry.name);
|
|
810
1132
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
811
|
-
const relativePath = entry.name.slice(`${
|
|
1133
|
+
const relativePath = entry.name.slice(`${installRoot}/`.length);
|
|
812
1134
|
const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
|
|
813
1135
|
writeFileSync(outputPath, bytes);
|
|
814
1136
|
}
|
|
815
1137
|
}
|
|
816
1138
|
|
|
817
|
-
function
|
|
1139
|
+
function initWarnings(descriptor, artifact) {
|
|
1140
|
+
if (
|
|
1141
|
+
descriptor.name === "opencode" &&
|
|
1142
|
+
artifact &&
|
|
1143
|
+
!artifact.command_aliases?.opencode &&
|
|
1144
|
+
!rootsForArtifact(descriptor, artifact).commands
|
|
1145
|
+
) {
|
|
1146
|
+
return [
|
|
1147
|
+
{
|
|
1148
|
+
code: "opencode-command-aliases-not-declared",
|
|
1149
|
+
message: "Selected opencode archive metadata does not declare command aliases; only skills were installed.",
|
|
1150
|
+
},
|
|
1151
|
+
];
|
|
1152
|
+
}
|
|
1153
|
+
return [];
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function directoryPlanForRoots(roots) {
|
|
1157
|
+
return [...new Set(Object.values(roots).flatMap((root) => [root.split("/").slice(0, -1).join("/"), root]).filter(Boolean))];
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function planDirectoryActions(flags, descriptor, artifact) {
|
|
818
1161
|
const actions = [];
|
|
819
1162
|
const artifacts = [];
|
|
820
1163
|
const blockers = [];
|
|
821
1164
|
let parentBlocked = false;
|
|
822
1165
|
|
|
823
|
-
|
|
1166
|
+
const directoryPlan = directoryPlanForRoots(rootsForArtifact(descriptor, artifact));
|
|
1167
|
+
const rootParent = directoryPlan[0];
|
|
1168
|
+
for (const relativePath of directoryPlan) {
|
|
824
1169
|
const state = parentBlocked ? "blocked-by-parent" : pathState(resolve(process.cwd(), relativePath));
|
|
825
1170
|
if (state === "absent") {
|
|
826
1171
|
actions.push({
|
|
@@ -831,7 +1176,7 @@ function planDirectoryActions(flags) {
|
|
|
831
1176
|
});
|
|
832
1177
|
artifacts.push({
|
|
833
1178
|
path: relativePath,
|
|
834
|
-
kind: directoryKind(relativePath),
|
|
1179
|
+
kind: directoryKind(relativePath, descriptor, artifact),
|
|
835
1180
|
status: flags.dryRun ? "planned" : "pending",
|
|
836
1181
|
});
|
|
837
1182
|
} else if (state === "directory") {
|
|
@@ -843,7 +1188,7 @@ function planDirectoryActions(flags) {
|
|
|
843
1188
|
});
|
|
844
1189
|
artifacts.push({
|
|
845
1190
|
path: relativePath,
|
|
846
|
-
kind: directoryKind(relativePath),
|
|
1191
|
+
kind: directoryKind(relativePath, descriptor, artifact),
|
|
847
1192
|
status: "existing",
|
|
848
1193
|
});
|
|
849
1194
|
} else {
|
|
@@ -853,12 +1198,12 @@ function planDirectoryActions(flags) {
|
|
|
853
1198
|
status: "blocked",
|
|
854
1199
|
reason:
|
|
855
1200
|
state === "blocked-by-parent"
|
|
856
|
-
? `${relativePath} cannot be created because ${
|
|
1201
|
+
? `${relativePath} cannot be created because ${rootParent} is not a directory.`
|
|
857
1202
|
: `${relativePath} exists and is not a directory.`,
|
|
858
1203
|
});
|
|
859
1204
|
artifacts.push({
|
|
860
1205
|
path: relativePath,
|
|
861
|
-
kind: directoryKind(relativePath),
|
|
1206
|
+
kind: directoryKind(relativePath, descriptor, artifact),
|
|
862
1207
|
status: "blocked",
|
|
863
1208
|
});
|
|
864
1209
|
if (state !== "blocked-by-parent") {
|
|
@@ -869,7 +1214,7 @@ function planDirectoryActions(flags) {
|
|
|
869
1214
|
next_action: `Move the existing file before running init.`,
|
|
870
1215
|
});
|
|
871
1216
|
}
|
|
872
|
-
if (relativePath ===
|
|
1217
|
+
if (relativePath === rootParent) {
|
|
873
1218
|
parentBlocked = true;
|
|
874
1219
|
}
|
|
875
1220
|
}
|
|
@@ -944,15 +1289,16 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
|
|
|
944
1289
|
}
|
|
945
1290
|
}
|
|
946
1291
|
|
|
947
|
-
function buildInitPlan(flags, artifact) {
|
|
1292
|
+
function buildInitPlan(flags, descriptor, artifact) {
|
|
948
1293
|
const info = packageInfo();
|
|
949
|
-
const source = sourceForFlags(flags, info);
|
|
1294
|
+
const source = sourceForFlags(flags, info, descriptor);
|
|
950
1295
|
if (artifact) {
|
|
951
1296
|
source.artifact = artifact;
|
|
952
1297
|
}
|
|
953
1298
|
const manifestPath = "rigorloop.yaml";
|
|
954
1299
|
const manifestAbsolutePath = resolve(process.cwd(), manifestPath);
|
|
955
|
-
const
|
|
1300
|
+
const existingManifest = existsSync(manifestAbsolutePath) ? readFileSync(manifestAbsolutePath, "utf8") : undefined;
|
|
1301
|
+
const manifest = manifestContent(info, source, descriptor, existingManifest);
|
|
956
1302
|
const actions = [];
|
|
957
1303
|
const artifacts = [];
|
|
958
1304
|
const blockers = [];
|
|
@@ -963,25 +1309,39 @@ function buildInitPlan(flags, artifact) {
|
|
|
963
1309
|
code: "invalid-archive-path",
|
|
964
1310
|
message: "Missing required value for --from-archive.",
|
|
965
1311
|
path: "--from-archive",
|
|
966
|
-
next_action:
|
|
1312
|
+
next_action: `Provide an existing ${descriptor.displayName} adapter archive path or omit --from-archive.`,
|
|
967
1313
|
});
|
|
968
1314
|
} else if (flags.fromArchiveProvided && !existsSync(resolve(process.cwd(), flags.fromArchive))) {
|
|
969
1315
|
errors.push({
|
|
970
1316
|
code: "invalid-archive-path",
|
|
971
1317
|
message: `Local archive path does not exist: ${flags.fromArchive}`,
|
|
972
1318
|
path: flags.fromArchive,
|
|
973
|
-
next_action:
|
|
1319
|
+
next_action: `Provide an existing ${descriptor.displayName} adapter archive path or omit --from-archive.`,
|
|
974
1320
|
});
|
|
975
1321
|
}
|
|
976
1322
|
|
|
977
|
-
const directoryPlan = planDirectoryActions(flags);
|
|
1323
|
+
const directoryPlan = planDirectoryActions(flags, descriptor, artifact);
|
|
978
1324
|
actions.push(...directoryPlan.actions);
|
|
979
1325
|
artifacts.push(...directoryPlan.artifacts);
|
|
980
1326
|
blockers.push(...directoryPlan.blockers);
|
|
981
1327
|
|
|
982
|
-
if (
|
|
983
|
-
const
|
|
984
|
-
if (
|
|
1328
|
+
if (existingManifest !== undefined) {
|
|
1329
|
+
const parsedManifest = parseManifestAdapterBlocks(existingManifest);
|
|
1330
|
+
if (parsedManifest.error) {
|
|
1331
|
+
errors.push({
|
|
1332
|
+
code: parsedManifest.error.code,
|
|
1333
|
+
message: parsedManifest.error.message,
|
|
1334
|
+
path: manifestPath,
|
|
1335
|
+
next_action: "Review or move the existing file before running init.",
|
|
1336
|
+
});
|
|
1337
|
+
} else if (parsedManifest.adapters.filter((entry) => entry.name === descriptor.name).length > 1) {
|
|
1338
|
+
blockers.push({
|
|
1339
|
+
code: "duplicate-adapter-entry",
|
|
1340
|
+
message: `Existing rigorloop.yaml contains duplicate ${descriptor.displayName} adapter entries.`,
|
|
1341
|
+
path: manifestPath,
|
|
1342
|
+
next_action: "Remove duplicate adapter entries before running init.",
|
|
1343
|
+
});
|
|
1344
|
+
} else if (compatibleManifest(existingManifest, descriptor, artifact)) {
|
|
985
1345
|
actions.push({
|
|
986
1346
|
type: "write",
|
|
987
1347
|
path: manifestPath,
|
|
@@ -994,11 +1354,16 @@ function buildInitPlan(flags, artifact) {
|
|
|
994
1354
|
status: "existing",
|
|
995
1355
|
});
|
|
996
1356
|
} else {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
message: "Existing rigorloop.yaml is not compatible with the first-slice Codex init contract.",
|
|
1357
|
+
actions.push({
|
|
1358
|
+
type: "write",
|
|
1000
1359
|
path: manifestPath,
|
|
1001
|
-
|
|
1360
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1361
|
+
reason: `Update rigorloop.yaml for ${descriptor.displayName} adapter.`,
|
|
1362
|
+
});
|
|
1363
|
+
artifacts.push({
|
|
1364
|
+
path: manifestPath,
|
|
1365
|
+
kind: "project-manifest",
|
|
1366
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1002
1367
|
});
|
|
1003
1368
|
}
|
|
1004
1369
|
} else {
|
|
@@ -1025,7 +1390,7 @@ function buildInitPlan(flags, artifact) {
|
|
|
1025
1390
|
artifacts,
|
|
1026
1391
|
blockers,
|
|
1027
1392
|
errors,
|
|
1028
|
-
planned_lockfile: plannedLockfile(info, source, manifest),
|
|
1393
|
+
planned_lockfile: plannedLockfile(info, source, manifest, descriptor),
|
|
1029
1394
|
};
|
|
1030
1395
|
}
|
|
1031
1396
|
|
|
@@ -1111,19 +1476,19 @@ function invalidArchivePath(message, flags) {
|
|
|
1111
1476
|
code: "invalid-archive-path",
|
|
1112
1477
|
message,
|
|
1113
1478
|
path: flags.fromArchive,
|
|
1114
|
-
next_action: "Provide an existing
|
|
1479
|
+
next_action: "Provide an existing supported adapter archive path or omit --from-archive.",
|
|
1115
1480
|
});
|
|
1116
1481
|
}
|
|
1117
1482
|
|
|
1118
1483
|
function unsupportedAdapter(adapter, flags) {
|
|
1119
1484
|
const result = envelope("init", flags, {
|
|
1120
1485
|
status: "blocked",
|
|
1121
|
-
summary: `Adapter '${adapter}' is not supported
|
|
1486
|
+
summary: `Adapter '${adapter}' is not supported.`,
|
|
1122
1487
|
blockers: [
|
|
1123
1488
|
{
|
|
1124
|
-
code: "adapter-
|
|
1125
|
-
message: `Adapter '${adapter}' is not supported
|
|
1126
|
-
next_action:
|
|
1489
|
+
code: "adapter-unknown",
|
|
1490
|
+
message: `Adapter '${adapter}' is not supported.`,
|
|
1491
|
+
next_action: `Use one of: ${supportedAdapterNames().join(", ")}.`,
|
|
1127
1492
|
},
|
|
1128
1493
|
],
|
|
1129
1494
|
});
|
|
@@ -1131,7 +1496,7 @@ function unsupportedAdapter(adapter, flags) {
|
|
|
1131
1496
|
if (flags.json) {
|
|
1132
1497
|
writeJson(result);
|
|
1133
1498
|
} else {
|
|
1134
|
-
process.stderr.write(`${result.summary}\nUse
|
|
1499
|
+
process.stderr.write(`${result.summary}\nUse one of: ${supportedAdapterNames().join(", ")}.\n`);
|
|
1135
1500
|
}
|
|
1136
1501
|
return exitCodeForResult({ ...result, exit_class: "blocked" });
|
|
1137
1502
|
}
|
|
@@ -1160,6 +1525,9 @@ function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked
|
|
|
1160
1525
|
},
|
|
1161
1526
|
planned_lockfile: plan.planned_lockfile,
|
|
1162
1527
|
});
|
|
1528
|
+
if (blockers[0]?.diagnostics) {
|
|
1529
|
+
result.diagnostics = { ...result.diagnostics, ...blockers[0].diagnostics };
|
|
1530
|
+
}
|
|
1163
1531
|
if (flags.json) {
|
|
1164
1532
|
writeJson(result);
|
|
1165
1533
|
} else {
|
|
@@ -1204,18 +1572,27 @@ function writeValidationErrorResult(flags, plan, error) {
|
|
|
1204
1572
|
return exitCodeForResult({ ...result, exit_class: "validation_failed" });
|
|
1205
1573
|
}
|
|
1206
1574
|
|
|
1207
|
-
async function archiveWorkForInit(flags, info) {
|
|
1208
|
-
if (flags.dryRun) {
|
|
1575
|
+
async function archiveWorkForInit(flags, info, descriptor) {
|
|
1576
|
+
if (flags.dryRun && !flags.fromArchiveProvided) {
|
|
1209
1577
|
return {};
|
|
1210
1578
|
}
|
|
1211
1579
|
|
|
1212
1580
|
const bundledMetadata = loadVerifiedBundledMetadata(info);
|
|
1213
1581
|
if (bundledMetadata.blocker || bundledMetadata.error) {
|
|
1582
|
+
if (flags.dryRun) {
|
|
1583
|
+
return {};
|
|
1584
|
+
}
|
|
1214
1585
|
return bundledMetadata;
|
|
1215
1586
|
}
|
|
1216
1587
|
const metadata = bundledMetadata.metadata;
|
|
1217
|
-
const validation = validateMetadata(metadata, info);
|
|
1588
|
+
const validation = validateMetadata(metadata, info, descriptor);
|
|
1218
1589
|
if (validation.blocker || validation.error) {
|
|
1590
|
+
if (
|
|
1591
|
+
flags.dryRun &&
|
|
1592
|
+
!["opencode-command-aliases-missing", "opencode-skills-only-compatibility-unmarked"].includes(validation.blocker?.code)
|
|
1593
|
+
) {
|
|
1594
|
+
return {};
|
|
1595
|
+
}
|
|
1219
1596
|
return validation;
|
|
1220
1597
|
}
|
|
1221
1598
|
const artifact = validation.artifact;
|
|
@@ -1223,17 +1600,31 @@ async function archiveWorkForInit(flags, info) {
|
|
|
1223
1600
|
if (flags.fromArchiveProvided) {
|
|
1224
1601
|
const archiveName = basename(flags.fromArchive);
|
|
1225
1602
|
if (archiveName !== artifact.archive || !archiveName.includes(metadata.release.version)) {
|
|
1603
|
+
if (!archiveName.startsWith(`rigorloop-adapter-${descriptor.name}-`)) {
|
|
1604
|
+
return {
|
|
1605
|
+
error: {
|
|
1606
|
+
code: "adapter-archive-mismatch",
|
|
1607
|
+
message: `Local archive ${archiveName} is not a ${descriptor.displayName} adapter archive.`,
|
|
1608
|
+
path: flags.fromArchive,
|
|
1609
|
+
},
|
|
1610
|
+
artifact,
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1226
1613
|
return {
|
|
1227
1614
|
blocker: metadataBlocker(
|
|
1228
1615
|
"release-version-incompatible",
|
|
1229
1616
|
`Local archive ${archiveName} is not compatible with ${metadata.release.version}.`,
|
|
1230
1617
|
flags.fromArchive,
|
|
1231
|
-
|
|
1618
|
+
`Use the ${descriptor.displayName} adapter archive matching the installed CLI package version.`,
|
|
1232
1619
|
),
|
|
1233
1620
|
};
|
|
1234
1621
|
}
|
|
1622
|
+
// CR-M2-R2-F1: dry-run planning must use trusted metadata roots without reading or extracting archive bytes.
|
|
1623
|
+
if (flags.dryRun) {
|
|
1624
|
+
return { artifact };
|
|
1625
|
+
}
|
|
1235
1626
|
const archiveBytes = readFileSync(resolve(process.cwd(), flags.fromArchive));
|
|
1236
|
-
const inspected = inspectArchive(archiveBytes, artifact);
|
|
1627
|
+
const inspected = inspectArchive(archiveBytes, artifact, descriptor);
|
|
1237
1628
|
if (inspected.error) {
|
|
1238
1629
|
return { error: inspected.error, artifact };
|
|
1239
1630
|
}
|
|
@@ -1258,17 +1649,12 @@ async function archiveWorkForInit(flags, info) {
|
|
|
1258
1649
|
}
|
|
1259
1650
|
try {
|
|
1260
1651
|
archiveBytes = await fetchBytes(artifact.url);
|
|
1261
|
-
} catch {
|
|
1652
|
+
} catch (error) {
|
|
1262
1653
|
return {
|
|
1263
|
-
blocker:
|
|
1264
|
-
"release-unavailable",
|
|
1265
|
-
"Official Codex adapter archive is unavailable.",
|
|
1266
|
-
artifact.url,
|
|
1267
|
-
"Retry later or use --from-archive with a compatible local archive.",
|
|
1268
|
-
),
|
|
1654
|
+
blocker: downloadFailureBlocker(error, artifact, descriptor, metadata),
|
|
1269
1655
|
};
|
|
1270
1656
|
}
|
|
1271
|
-
const inspected = inspectArchive(archiveBytes, artifact);
|
|
1657
|
+
const inspected = inspectArchive(archiveBytes, artifact, descriptor);
|
|
1272
1658
|
if (inspected.error) {
|
|
1273
1659
|
return { error: inspected.error, artifact };
|
|
1274
1660
|
}
|
|
@@ -1277,9 +1663,10 @@ async function archiveWorkForInit(flags, info) {
|
|
|
1277
1663
|
|
|
1278
1664
|
async function handleInit(flags) {
|
|
1279
1665
|
if (!flags.adapter) {
|
|
1280
|
-
return invalidUsage("Missing required option: --adapter codex.", flags, "init");
|
|
1666
|
+
return invalidUsage("Missing required option: --adapter codex|claude|opencode.", flags, "init");
|
|
1281
1667
|
}
|
|
1282
|
-
|
|
1668
|
+
const descriptor = adapterDescriptor(flags.adapter);
|
|
1669
|
+
if (!descriptor) {
|
|
1283
1670
|
return unsupportedAdapter(flags.adapter, flags);
|
|
1284
1671
|
}
|
|
1285
1672
|
if (flags.fromArchiveProvided && (!flags.fromArchive || flags.fromArchive.startsWith("--"))) {
|
|
@@ -1290,7 +1677,7 @@ async function handleInit(flags) {
|
|
|
1290
1677
|
}
|
|
1291
1678
|
|
|
1292
1679
|
const info = packageInfo();
|
|
1293
|
-
|
|
1680
|
+
let plan = buildInitPlan(flags, descriptor);
|
|
1294
1681
|
if (plan.errors.length > 0) {
|
|
1295
1682
|
const result = envelope("init", flags, {
|
|
1296
1683
|
status: "error",
|
|
@@ -1311,21 +1698,30 @@ async function handleInit(flags) {
|
|
|
1311
1698
|
}
|
|
1312
1699
|
return exitCodeForResult({ ...result, exit_class: "invalid_usage" });
|
|
1313
1700
|
}
|
|
1314
|
-
|
|
1701
|
+
const deferrableRootBlockers =
|
|
1702
|
+
descriptor.name === "opencode" &&
|
|
1703
|
+
plan.blockers.length > 0 &&
|
|
1704
|
+
plan.blockers.every((blocker) => String(blocker.path ?? "").startsWith(".opencode/commands"));
|
|
1705
|
+
if (plan.blockers.length > 0 && !deferrableRootBlockers) {
|
|
1315
1706
|
return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
|
|
1316
1707
|
}
|
|
1317
1708
|
|
|
1318
|
-
const archiveWork = await archiveWorkForInit(flags, info);
|
|
1319
|
-
if (archiveWork.artifact) {
|
|
1320
|
-
plan
|
|
1321
|
-
|
|
1709
|
+
const archiveWork = await archiveWorkForInit(flags, info, descriptor);
|
|
1710
|
+
if (archiveWork.artifact && !archiveWork.blocker && !archiveWork.error) {
|
|
1711
|
+
plan = buildInitPlan(flags, descriptor, archiveWork.artifact);
|
|
1712
|
+
if (plan.errors.length > 0) {
|
|
1713
|
+
return writeValidationErrorResult(flags, plan, plan.errors[0]);
|
|
1714
|
+
}
|
|
1715
|
+
if (plan.blockers.length > 0) {
|
|
1716
|
+
return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
|
|
1717
|
+
}
|
|
1322
1718
|
}
|
|
1323
1719
|
if (archiveWork.entries) {
|
|
1324
1720
|
const conflict = generatedOutputConflictBlocker(archiveWork.entries);
|
|
1325
1721
|
if (conflict) {
|
|
1326
1722
|
return writeBlockedResult(flags, plan, conflict.message, [conflict], "mutation_conflict");
|
|
1327
1723
|
}
|
|
1328
|
-
const drift =
|
|
1724
|
+
const drift = firstLockfileDriftBlocker();
|
|
1329
1725
|
if (drift) {
|
|
1330
1726
|
return writeBlockedResult(
|
|
1331
1727
|
flags,
|
|
@@ -1335,11 +1731,11 @@ async function handleInit(flags) {
|
|
|
1335
1731
|
drift.code === "overwrite-refused" ? "mutation_conflict" : "blocked",
|
|
1336
1732
|
);
|
|
1337
1733
|
}
|
|
1338
|
-
const installedTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, { allowMissingOrEmpty: true });
|
|
1734
|
+
const installedTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, descriptor, { allowMissingOrEmpty: true });
|
|
1339
1735
|
if (installedTree.error) {
|
|
1340
1736
|
return writeValidationErrorResult(flags, plan, installedTree.error);
|
|
1341
1737
|
}
|
|
1342
|
-
addArchiveActions(plan, archiveWork.entries);
|
|
1738
|
+
addArchiveActions(plan, archiveWork.entries, descriptor);
|
|
1343
1739
|
}
|
|
1344
1740
|
|
|
1345
1741
|
if (archiveWork.blocker) {
|
|
@@ -1370,7 +1766,7 @@ async function handleInit(flags) {
|
|
|
1370
1766
|
const pendingCopyPaths = new Set(
|
|
1371
1767
|
plan.actions.filter((action) => action.type === "copy" && action.status === "pending").map((action) => action.path),
|
|
1372
1768
|
);
|
|
1373
|
-
writeArchiveEntries(archiveWork.entries.filter((entry) => pendingCopyPaths.has(entry.name)));
|
|
1769
|
+
writeArchiveEntries(archiveWork.entries.filter((entry) => pendingCopyPaths.has(entry.name)), descriptor);
|
|
1374
1770
|
for (const action of plan.actions.filter((action) => action.type === "copy" && action.status === "pending")) {
|
|
1375
1771
|
action.status = "done";
|
|
1376
1772
|
plan.artifacts.find((artifact) => artifact.path === action.path).status = "created";
|
|
@@ -1407,7 +1803,7 @@ async function handleInit(flags) {
|
|
|
1407
1803
|
const lockfileArtifact = plan.artifacts.find((artifact) => artifact.path === LOCKFILE_PATH);
|
|
1408
1804
|
if (lockfileAction?.status === "pending") {
|
|
1409
1805
|
const lockfilePreviouslyExists = existsSync(resolve(process.cwd(), LOCKFILE_PATH));
|
|
1410
|
-
const verifiedInstalledTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact);
|
|
1806
|
+
const verifiedInstalledTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, descriptor);
|
|
1411
1807
|
if (verifiedInstalledTree.error) {
|
|
1412
1808
|
return writeValidationErrorResult(flags, plan, verifiedInstalledTree.error);
|
|
1413
1809
|
}
|
|
@@ -1416,15 +1812,15 @@ async function handleInit(flags) {
|
|
|
1416
1812
|
plan.source,
|
|
1417
1813
|
plan.manifest,
|
|
1418
1814
|
archiveWork.artifact,
|
|
1419
|
-
|
|
1420
|
-
|
|
1815
|
+
verifiedInstalledTree.rootHashes,
|
|
1816
|
+
descriptor,
|
|
1421
1817
|
);
|
|
1422
1818
|
writeFileSync(resolve(process.cwd(), LOCKFILE_PATH), serializeLockfile(lockfile), "utf8");
|
|
1423
1819
|
plan.planned_lockfile = lockfile;
|
|
1424
1820
|
lockfileAction.status = "done";
|
|
1425
1821
|
lockfileAction.reason = lockfilePreviouslyExists
|
|
1426
|
-
?
|
|
1427
|
-
:
|
|
1822
|
+
? `Updated durable lockfile for verified ${descriptor.displayName} adapter install.`
|
|
1823
|
+
: `Wrote durable lockfile for verified ${descriptor.displayName} adapter install.`;
|
|
1428
1824
|
if (lockfileArtifact) {
|
|
1429
1825
|
lockfileArtifact.status = lockfilePreviouslyExists ? "updated" : "created";
|
|
1430
1826
|
}
|
|
@@ -1432,14 +1828,14 @@ async function handleInit(flags) {
|
|
|
1432
1828
|
}
|
|
1433
1829
|
}
|
|
1434
1830
|
|
|
1435
|
-
const warnings =
|
|
1831
|
+
const warnings = initWarnings(descriptor, archiveWork.artifact);
|
|
1436
1832
|
const result = envelope("init", flags, {
|
|
1437
1833
|
status: warnings.length > 0 ? "warning" : "success",
|
|
1438
1834
|
summary: flags.dryRun
|
|
1439
1835
|
? "RigorLoop init dry run completed. No files were written."
|
|
1440
1836
|
: archiveWork.entries
|
|
1441
|
-
?
|
|
1442
|
-
:
|
|
1837
|
+
? `RigorLoop initialized with verified ${descriptor.displayName} adapter files.`
|
|
1838
|
+
: `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
|
|
1443
1839
|
actions: plan.actions,
|
|
1444
1840
|
artifacts: plan.artifacts,
|
|
1445
1841
|
warnings,
|
|
@@ -1457,10 +1853,13 @@ async function handleInit(flags) {
|
|
|
1457
1853
|
? ["RigorLoop init dry run completed.", "No files were written."]
|
|
1458
1854
|
: [
|
|
1459
1855
|
archiveWork.entries
|
|
1460
|
-
?
|
|
1461
|
-
:
|
|
1856
|
+
? `RigorLoop initialized with verified ${descriptor.displayName} adapter files.`
|
|
1857
|
+
: `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
|
|
1462
1858
|
archiveWork.entries ? "rigorloop.lock was written." : "No adapter files were installed.",
|
|
1463
1859
|
];
|
|
1860
|
+
for (const warning of warnings) {
|
|
1861
|
+
lines.push(`warning ${warning.code}: ${warning.message}`);
|
|
1862
|
+
}
|
|
1464
1863
|
writeHuman(`${lines.join("\n")}\n`, flags);
|
|
1465
1864
|
}
|
|
1466
1865
|
return exitCodeForResult({ ...result, exit_class: "success" });
|