@xiongxianfei/rigorloop 0.1.5 → 0.3.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 +111 -2
- package/dist/bin/rigorloop.js +818 -221
- package/dist/lib/adapters.js +50 -0
- package/dist/lib/lockfile.js +268 -33
- package/dist/metadata/adapter-artifacts-v0.2.0.json +31 -0
- package/dist/metadata/adapter-artifacts-v0.3.0.json +65 -0
- package/dist/metadata/releases.json +4 -4
- package/package.json +22 -2
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));
|
|
@@ -39,6 +36,8 @@ function parseFlags(args) {
|
|
|
39
36
|
noColor: Boolean(process.env.NO_COLOR),
|
|
40
37
|
dryRun: false,
|
|
41
38
|
adapter: undefined,
|
|
39
|
+
adapterOptionUsed: false,
|
|
40
|
+
writeState: false,
|
|
42
41
|
fromArchiveProvided: false,
|
|
43
42
|
fromArchive: undefined,
|
|
44
43
|
force: false,
|
|
@@ -58,10 +57,13 @@ function parseFlags(args) {
|
|
|
58
57
|
} else if (arg === "--dry-run") {
|
|
59
58
|
flags.dryRun = true;
|
|
60
59
|
} else if (arg === "--adapter") {
|
|
60
|
+
flags.adapterOptionUsed = true;
|
|
61
61
|
if (args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
62
62
|
flags.adapter = args[index + 1];
|
|
63
63
|
index += 1;
|
|
64
64
|
}
|
|
65
|
+
} else if (arg === "--write-state") {
|
|
66
|
+
flags.writeState = true;
|
|
65
67
|
} else if (arg === "--from-archive") {
|
|
66
68
|
flags.fromArchiveProvided = true;
|
|
67
69
|
if (args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
@@ -113,12 +115,13 @@ function usage() {
|
|
|
113
115
|
Usage:
|
|
114
116
|
rigorloop --help
|
|
115
117
|
rigorloop version
|
|
116
|
-
rigorloop init
|
|
118
|
+
rigorloop init codex|claude|opencode [--write-state] [--dry-run] [--json]
|
|
117
119
|
rigorloop new-change <change-id> --title <title> [--dry-run] [--json]
|
|
118
120
|
|
|
119
121
|
Commands:
|
|
120
122
|
version Print package name and version.
|
|
121
|
-
init
|
|
123
|
+
init codex|claude|opencode
|
|
124
|
+
Initialize verified target support.
|
|
122
125
|
new-change Plan a change metadata scaffold.
|
|
123
126
|
`;
|
|
124
127
|
}
|
|
@@ -127,43 +130,183 @@ function releaseForPackage(version) {
|
|
|
127
130
|
return `v${version}`;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
function sourceForFlags(flags, info) {
|
|
133
|
+
function sourceForFlags(flags, info, descriptor) {
|
|
131
134
|
if (flags.fromArchiveProvided) {
|
|
132
135
|
return {
|
|
133
136
|
type: "local-archive",
|
|
134
|
-
archive: flags.fromArchive,
|
|
137
|
+
archive: basename(flags.fromArchive),
|
|
138
|
+
inputPath: flags.fromArchive,
|
|
135
139
|
};
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
return {
|
|
139
143
|
type: "release-archive",
|
|
140
144
|
release: releaseForPackage(info.version),
|
|
141
|
-
archive:
|
|
145
|
+
archive: descriptor.archiveName(releaseForPackage(info.version)),
|
|
142
146
|
};
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
function
|
|
149
|
+
function manifestTargetBlock(source, descriptor, artifact) {
|
|
146
150
|
const sourceLines =
|
|
147
151
|
source.type === "local-archive"
|
|
148
152
|
? [` type: local-archive`, ` archive: "${source.archive}"`]
|
|
149
153
|
: [` type: release-archive`, ` release: "${source.release}"`];
|
|
154
|
+
const installRoots = rootsForArtifact(descriptor, artifact);
|
|
155
|
+
const rootLines =
|
|
156
|
+
descriptor.name !== "opencode" && Object.keys(installRoots).length === 1
|
|
157
|
+
? [` install_root: "${Object.values(installRoots)[0]}"`]
|
|
158
|
+
: [
|
|
159
|
+
` install_roots:`,
|
|
160
|
+
...Object.entries(installRoots).map(([role, root]) => ` ${role}: "${root}"`),
|
|
161
|
+
];
|
|
150
162
|
|
|
151
|
-
return `
|
|
163
|
+
return ` - target: ${descriptor.name}
|
|
164
|
+
${rootLines.join("\n")}
|
|
165
|
+
source:
|
|
166
|
+
${sourceLines.join("\n")}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseManifestAdapterBlocks(content) {
|
|
170
|
+
const isLegacySchema = content.includes("schema_version: 1") && content.includes("adapters:");
|
|
171
|
+
const isTargetSchema = content.includes("schema_version: 2") && content.includes("targets:");
|
|
172
|
+
if (!isLegacySchema && !isTargetSchema) {
|
|
173
|
+
return { error: { code: "invalid-config", message: "Existing rigorloop.yaml is not compatible with the init contract." } };
|
|
174
|
+
}
|
|
175
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
176
|
+
const listKey = isTargetSchema ? "targets:" : "adapters:";
|
|
177
|
+
const entryPrefix = isTargetSchema ? " - target: " : " - name: ";
|
|
178
|
+
const adapterStart = lines.findIndex((line) => line === listKey);
|
|
179
|
+
const blocks = [];
|
|
180
|
+
let current = [];
|
|
181
|
+
for (const line of lines.slice(adapterStart + 1)) {
|
|
182
|
+
if (line.startsWith(entryPrefix)) {
|
|
183
|
+
if (current.length) {
|
|
184
|
+
blocks.push(current);
|
|
185
|
+
}
|
|
186
|
+
current = [line];
|
|
187
|
+
} else if (current.length) {
|
|
188
|
+
current.push(line);
|
|
189
|
+
} else if (line.trim() !== "") {
|
|
190
|
+
return { error: { code: "invalid-config", message: "Existing rigorloop.yaml has malformed adapter entries." } };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (current.length) {
|
|
194
|
+
blocks.push(current);
|
|
195
|
+
}
|
|
196
|
+
const adapters = [];
|
|
197
|
+
for (const block of blocks) {
|
|
198
|
+
const name = block[0].slice(entryPrefix.length).trim();
|
|
199
|
+
const descriptor = adapterDescriptor(name);
|
|
200
|
+
if (!descriptor) {
|
|
201
|
+
return { error: { code: "invalid-config", message: `Existing rigorloop.yaml includes unsupported adapter ${name}.` } };
|
|
202
|
+
}
|
|
203
|
+
const blockText = isTargetSchema ? block.join("\n").replace(/\n+$/, "") : block.join("\n").replace(/\n+$/, "").replace(/^ - name:/, " - target:");
|
|
204
|
+
adapters.push({ name, block: blockText, roots: manifestBlockRoots(blockText) });
|
|
205
|
+
}
|
|
206
|
+
return { adapters };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function manifestBlockRoots(block) {
|
|
210
|
+
const roots = [];
|
|
211
|
+
let inInstallRoots = false;
|
|
212
|
+
for (const line of block.split("\n")) {
|
|
213
|
+
const singleRootMatch = line.match(/^ install_root:\s+"([^"]+)"\s*$/);
|
|
214
|
+
if (singleRootMatch) {
|
|
215
|
+
roots.push(singleRootMatch[1]);
|
|
216
|
+
inInstallRoots = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (line === " install_roots:") {
|
|
220
|
+
inInstallRoots = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (/^ [A-Za-z_][A-Za-z0-9_-]*:/.test(line)) {
|
|
224
|
+
inInstallRoots = false;
|
|
225
|
+
}
|
|
226
|
+
const rootMatch = inInstallRoots ? line.match(/^ [A-Za-z_][A-Za-z0-9_-]*:\s+"([^"]+)"\s*$/) : undefined;
|
|
227
|
+
if (rootMatch) {
|
|
228
|
+
roots.push(rootMatch[1]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return roots;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function manifestContent(info, source, descriptor, existingContent) {
|
|
235
|
+
const selectedBlock = manifestTargetBlock(source, descriptor, source.artifact);
|
|
236
|
+
const parsed = existingContent ? parseManifestAdapterBlocks(existingContent) : undefined;
|
|
237
|
+
const preserved =
|
|
238
|
+
parsed && !parsed.error
|
|
239
|
+
? parsed.adapters
|
|
240
|
+
.filter((entry) => entry.name !== descriptor.name)
|
|
241
|
+
.map((entry) => entry.block.replace(/^ - name:/, " - target:"))
|
|
242
|
+
: [];
|
|
243
|
+
return `schema_version: 2
|
|
152
244
|
rigorloop:
|
|
153
245
|
package: "${info.name}"
|
|
154
246
|
package_version: "${info.version}"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
install_root: "${INSTALL_ROOT}"
|
|
158
|
-
source:
|
|
159
|
-
${sourceLines.join("\n")}
|
|
247
|
+
targets:
|
|
248
|
+
${[...preserved, selectedBlock].join("\n")}
|
|
160
249
|
`;
|
|
161
250
|
}
|
|
162
251
|
|
|
163
|
-
function
|
|
252
|
+
function rootsForArtifact(descriptor, artifact) {
|
|
253
|
+
if (artifact?.install_roots) {
|
|
254
|
+
return artifact.install_roots;
|
|
255
|
+
}
|
|
256
|
+
if (artifact?.install_root) {
|
|
257
|
+
return { skills: artifact.install_root };
|
|
258
|
+
}
|
|
259
|
+
return descriptor.installRoots;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function rootHashesForArtifact(descriptor, artifact) {
|
|
263
|
+
if (artifact?.root_hashes) {
|
|
264
|
+
return artifact.root_hashes;
|
|
265
|
+
}
|
|
266
|
+
return Object.fromEntries(
|
|
267
|
+
Object.keys(rootsForArtifact(descriptor, artifact)).map((role) => [
|
|
268
|
+
role,
|
|
269
|
+
{
|
|
270
|
+
tree_sha256: artifact?.tree_sha256 ?? "<planned-after-install>",
|
|
271
|
+
file_count: artifact?.file_count ?? "<planned-after-install>",
|
|
272
|
+
},
|
|
273
|
+
]),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function usesMultiRootLockfile(descriptor, artifact) {
|
|
278
|
+
return descriptor.name === "opencode" || Object.keys(rootsForArtifact(descriptor, artifact)).length > 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes = rootHashesForArtifact(descriptor, artifact)) {
|
|
282
|
+
const entry = {
|
|
283
|
+
target: descriptor.name,
|
|
284
|
+
release: releaseForPackage(info.version),
|
|
285
|
+
source: source.type,
|
|
286
|
+
archive: source.archive,
|
|
287
|
+
archive_sha256: artifact?.sha256 ?? "<planned>",
|
|
288
|
+
tree_hash_algorithm: "rigorloop-tree-hash-v1",
|
|
289
|
+
};
|
|
290
|
+
if (usesMultiRootLockfile(descriptor, artifact)) {
|
|
291
|
+
return {
|
|
292
|
+
...entry,
|
|
293
|
+
installed_roots: rootsForArtifact(descriptor, artifact),
|
|
294
|
+
root_hashes: rootHashes,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const [role] = Object.keys(rootsForArtifact(descriptor, artifact));
|
|
298
|
+
return {
|
|
299
|
+
...entry,
|
|
300
|
+
installed_root: rootsForArtifact(descriptor, artifact)[role],
|
|
301
|
+
tree_sha256: rootHashes[role].tree_sha256,
|
|
302
|
+
file_count: rootHashes[role].file_count,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function plannedLockfile(info, source, manifest, descriptor) {
|
|
164
307
|
const artifact = source.artifact;
|
|
165
308
|
return {
|
|
166
|
-
schema_version:
|
|
309
|
+
schema_version: 3,
|
|
167
310
|
rigorloop: {
|
|
168
311
|
package: info.name,
|
|
169
312
|
version: info.version,
|
|
@@ -173,26 +316,39 @@ function plannedLockfile(info, source, manifest) {
|
|
|
173
316
|
sha256: sha256NormalizedText(manifest),
|
|
174
317
|
},
|
|
175
318
|
generated: {
|
|
176
|
-
|
|
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
|
-
],
|
|
319
|
+
targets: [lockfileEntryForAdapter(info, source, artifact, descriptor)],
|
|
189
320
|
},
|
|
190
321
|
};
|
|
191
322
|
}
|
|
192
323
|
|
|
193
|
-
function
|
|
324
|
+
function existingLockfileEntries(selectedAdapter) {
|
|
325
|
+
const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
326
|
+
if (!existsSync(lockfileAbsolutePath)) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
|
|
330
|
+
if (!parsed.ok) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const entries = parsed.lockfile.generated.targets ?? parsed.lockfile.generated.adapters;
|
|
334
|
+
return entries
|
|
335
|
+
.filter((entry) => (entry.target ?? entry.adapter) !== selectedAdapter)
|
|
336
|
+
.map((entry) => {
|
|
337
|
+
if (entry.target) {
|
|
338
|
+
return entry;
|
|
339
|
+
}
|
|
340
|
+
const { adapter, ...rest } = entry;
|
|
341
|
+
return { target: adapter, ...rest };
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function lockfileForVerifiedInstall(info, source, manifest, artifact, rootHashes, descriptor) {
|
|
346
|
+
const adapters = [
|
|
347
|
+
...existingLockfileEntries(descriptor.name),
|
|
348
|
+
lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes),
|
|
349
|
+
];
|
|
194
350
|
return {
|
|
195
|
-
schema_version:
|
|
351
|
+
schema_version: 3,
|
|
196
352
|
rigorloop: {
|
|
197
353
|
package: info.name,
|
|
198
354
|
version: info.version,
|
|
@@ -202,28 +358,19 @@ function lockfileForVerifiedInstall(info, source, manifest, artifact, treeHash,
|
|
|
202
358
|
sha256: sha256NormalizedText(manifest),
|
|
203
359
|
},
|
|
204
360
|
generated: {
|
|
205
|
-
|
|
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
|
-
],
|
|
361
|
+
targets: adapters,
|
|
218
362
|
},
|
|
219
363
|
};
|
|
220
364
|
}
|
|
221
365
|
|
|
222
|
-
function compatibleManifest(content) {
|
|
366
|
+
function compatibleManifest(content, descriptor, artifact) {
|
|
367
|
+
if (descriptor.name === "opencode" && !rootsForArtifact(descriptor, artifact).commands && content.includes(".opencode/commands")) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
223
370
|
return (
|
|
224
371
|
content.includes("schema_version: 1") &&
|
|
225
|
-
content.includes(
|
|
226
|
-
content.includes(`
|
|
372
|
+
content.includes(`name: ${descriptor.name}`) &&
|
|
373
|
+
content.includes(`"${Object.values(rootsForArtifact(descriptor, artifact))[0]}"`)
|
|
227
374
|
);
|
|
228
375
|
}
|
|
229
376
|
|
|
@@ -234,8 +381,11 @@ function pathState(path) {
|
|
|
234
381
|
return statSync(path).isDirectory() ? "directory" : "file";
|
|
235
382
|
}
|
|
236
383
|
|
|
237
|
-
function directoryKind(path) {
|
|
238
|
-
|
|
384
|
+
function directoryKind(path, descriptor, artifact) {
|
|
385
|
+
if (Object.values(rootsForArtifact(descriptor, artifact)).includes(path)) {
|
|
386
|
+
return `${descriptor.name}-install-root`;
|
|
387
|
+
}
|
|
388
|
+
return `${descriptor.name}-adapter-root`;
|
|
239
389
|
}
|
|
240
390
|
|
|
241
391
|
function sha256(bytes) {
|
|
@@ -338,11 +488,77 @@ function loadJsonFile(path) {
|
|
|
338
488
|
async function fetchBytes(url) {
|
|
339
489
|
const response = await fetch(url);
|
|
340
490
|
if (!response.ok) {
|
|
341
|
-
throw new Error(`HTTP ${response.status}`);
|
|
491
|
+
throw Object.assign(new Error(`HTTP ${response.status}`), { downloadFailureClass: "http-status" });
|
|
342
492
|
}
|
|
343
493
|
return Buffer.from(await response.arrayBuffer());
|
|
344
494
|
}
|
|
345
495
|
|
|
496
|
+
const PROXY_ENV_VAR_ALLOWLIST = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"];
|
|
497
|
+
const DOWNLOAD_FAILURE_CLASSES = new Set(["dns", "tls", "timeout", "http-status", "proxy", "network", "unknown"]);
|
|
498
|
+
|
|
499
|
+
function detectedProxyEnvVars(env = process.env) {
|
|
500
|
+
return PROXY_ENV_VAR_ALLOWLIST.filter((name) => Object.prototype.hasOwnProperty.call(env, name) && env[name]);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function nodeEnvProxyStatus(env = process.env, execArgv = process.execArgv) {
|
|
504
|
+
const nodeOptions = String(env.NODE_OPTIONS ?? "");
|
|
505
|
+
const useEnvProxy = String(env.NODE_USE_ENV_PROXY ?? "").toLowerCase();
|
|
506
|
+
// CR-M4-R1-F1: Node can enable fetch env-proxy via env vars or the runtime flag.
|
|
507
|
+
if (nodeOptions.includes("--use-env-proxy") || execArgv.includes("--use-env-proxy") || ["1", "true", "yes"].includes(useEnvProxy)) {
|
|
508
|
+
return "enabled";
|
|
509
|
+
}
|
|
510
|
+
if (detectedProxyEnvVars(env).length > 0) {
|
|
511
|
+
return "disabled";
|
|
512
|
+
}
|
|
513
|
+
return "unknown";
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function downloadFailureClass(error) {
|
|
517
|
+
if (DOWNLOAD_FAILURE_CLASSES.has(error?.downloadFailureClass)) {
|
|
518
|
+
return error.downloadFailureClass;
|
|
519
|
+
}
|
|
520
|
+
const code = String(error?.code ?? error?.cause?.code ?? "").toUpperCase();
|
|
521
|
+
const message = String(error?.message ?? "").toLowerCase();
|
|
522
|
+
if (["ENOTFOUND", "EAI_AGAIN"].includes(code)) {
|
|
523
|
+
return "dns";
|
|
524
|
+
}
|
|
525
|
+
if (code.includes("CERT") || code.includes("TLS") || message.includes("certificate") || message.includes("tls")) {
|
|
526
|
+
return "tls";
|
|
527
|
+
}
|
|
528
|
+
if (code.includes("TIMEOUT") || code === "ABORT_ERR" || message.includes("timeout") || message.includes("timed out")) {
|
|
529
|
+
return "timeout";
|
|
530
|
+
}
|
|
531
|
+
if (code.includes("PROXY") || message.includes("proxy")) {
|
|
532
|
+
return "proxy";
|
|
533
|
+
}
|
|
534
|
+
if (["ECONNRESET", "ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"].includes(code) || message.includes("fetch failed")) {
|
|
535
|
+
return "network";
|
|
536
|
+
}
|
|
537
|
+
return "unknown";
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function downloadFailureDiagnostics(error, artifact, descriptor, metadata) {
|
|
541
|
+
return {
|
|
542
|
+
adapter: descriptor.name,
|
|
543
|
+
release: metadata.release.version,
|
|
544
|
+
archive_url: artifact.url,
|
|
545
|
+
download_failure_class: downloadFailureClass(error),
|
|
546
|
+
node_env_proxy_status: nodeEnvProxyStatus(),
|
|
547
|
+
proxy_env_vars_detected: detectedProxyEnvVars(),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function downloadFailureBlocker(error, artifact, descriptor, metadata) {
|
|
552
|
+
const diagnostics = downloadFailureDiagnostics(error, artifact, descriptor, metadata);
|
|
553
|
+
return {
|
|
554
|
+
code: "release-download-failed",
|
|
555
|
+
message: `Network download failed for adapter ${descriptor.name} release ${metadata.release.version} (failure class ${diagnostics.download_failure_class}).`,
|
|
556
|
+
path: artifact.url,
|
|
557
|
+
next_action: `Download ${artifact.url} and rerun with --from-archive ./${artifact.archive}.`,
|
|
558
|
+
diagnostics,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
346
562
|
function parseVerifiedMetadataBytes(bytes, expectedSha256) {
|
|
347
563
|
const actualSha256 = sha256(bytes);
|
|
348
564
|
if (actualSha256 !== expectedSha256) {
|
|
@@ -382,7 +598,15 @@ function isSha256(value) {
|
|
|
382
598
|
return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
|
|
383
599
|
}
|
|
384
600
|
|
|
385
|
-
function
|
|
601
|
+
function releaseListedForSkillsOnlyCompatibility(marker, release) {
|
|
602
|
+
if (!marker) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
const releases = Array.isArray(marker) ? marker : marker.releases;
|
|
606
|
+
return Array.isArray(releases) && releases.includes(release);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function validateMetadata(metadata, info, descriptor) {
|
|
386
610
|
const release = releaseForPackage(info.version);
|
|
387
611
|
if (!metadata || metadata.schema_version !== 1) {
|
|
388
612
|
return { error: { code: "metadata-invalid", message: "Adapter metadata schema_version must be 1." } };
|
|
@@ -407,26 +631,76 @@ function validateMetadata(metadata, info) {
|
|
|
407
631
|
if (!isNonEmptyString(metadata.validation?.command)) {
|
|
408
632
|
return { error: { code: "metadata-invalid", message: "Adapter metadata validation command is missing." } };
|
|
409
633
|
}
|
|
410
|
-
const artifact = metadata.artifacts?.find((entry) => entry.adapter ===
|
|
634
|
+
const artifact = metadata.artifacts?.find((entry) => entry.adapter === descriptor.name);
|
|
411
635
|
if (!artifact) {
|
|
412
|
-
return { blocker: metadataBlocker("
|
|
636
|
+
return { blocker: metadataBlocker("metadata-unavailable", `Adapter metadata does not include ${descriptor.displayName}.`) };
|
|
413
637
|
}
|
|
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." } };
|
|
638
|
+
if (!isNonEmptyString(artifact.archive) || !isNonEmptyString(artifact.url) || !isSha256(artifact.sha256) || !Number.isInteger(artifact.size_bytes) || artifact.size_bytes < 0) {
|
|
639
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter artifact metadata is incomplete.` } };
|
|
426
640
|
}
|
|
427
641
|
if (artifact.tree_hash_algorithm && artifact.tree_hash_algorithm !== "rigorloop-tree-hash-v1") {
|
|
428
642
|
return { error: { code: "metadata-invalid", message: "Unsupported tree hash algorithm in adapter metadata." } };
|
|
429
643
|
}
|
|
644
|
+
if (artifact.install_roots || artifact.root_hashes) {
|
|
645
|
+
if (!artifact.install_roots || !artifact.root_hashes) {
|
|
646
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} multi-root metadata is incomplete.` } };
|
|
647
|
+
}
|
|
648
|
+
for (const [role, root] of Object.entries(artifact.install_roots)) {
|
|
649
|
+
if (descriptor.installRoots[role] !== root) {
|
|
650
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter install root for ${role} is not supported.` } };
|
|
651
|
+
}
|
|
652
|
+
const rootHash = artifact.root_hashes[role];
|
|
653
|
+
if (!rootHash || !isSha256(rootHash.tree_sha256) || !Number.isInteger(rootHash.file_count) || rootHash.file_count < 0) {
|
|
654
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} root hash metadata is incomplete.` } };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
if (!isSha256(artifact.tree_sha256) || !Number.isInteger(artifact.file_count) || artifact.file_count < 0) {
|
|
659
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter single-root metadata is incomplete.` } };
|
|
660
|
+
}
|
|
661
|
+
if ((artifact.install_root ?? "").replace(/\/$/, "") !== descriptor.primaryInstallRoot()) {
|
|
662
|
+
return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter install root is not ${descriptor.primaryInstallRoot()}.` } };
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (artifact.command_aliases?.opencode) {
|
|
666
|
+
if (descriptor.name !== "opencode" || !artifact.install_roots?.commands) {
|
|
667
|
+
return { error: { code: "metadata-invalid", message: "opencode command alias metadata requires the opencode commands root." } };
|
|
668
|
+
}
|
|
669
|
+
const aliasPaths = opencodeCommandAliasPaths(artifact);
|
|
670
|
+
if (
|
|
671
|
+
!Number.isInteger(artifact.command_aliases.opencode.count) ||
|
|
672
|
+
!Array.isArray(aliasPaths) ||
|
|
673
|
+
artifact.command_aliases.opencode.count !== aliasPaths.length ||
|
|
674
|
+
aliasPaths.some((aliasPath) => !isNonEmptyString(aliasPath) || !aliasPath.startsWith(`${descriptor.installRoots.commands}/`))
|
|
675
|
+
) {
|
|
676
|
+
return { error: { code: "metadata-invalid", message: "opencode command alias metadata is incomplete." } };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// CR-M3-R2-F1: opencode commands root is valid only with declared command aliases.
|
|
680
|
+
if (descriptor.name === "opencode" && artifact.install_roots?.commands && !artifact.command_aliases?.opencode) {
|
|
681
|
+
return {
|
|
682
|
+
blocker: metadataBlocker(
|
|
683
|
+
"opencode-command-aliases-missing",
|
|
684
|
+
"Opencode commands root metadata requires command_aliases.opencode.",
|
|
685
|
+
artifact.archive,
|
|
686
|
+
"Use opencode metadata that declares command aliases, or use explicitly compatible skills-only metadata without the commands root.",
|
|
687
|
+
),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// CR-M3-R1-F1: skills-only opencode compatibility must be explicit in trusted metadata.
|
|
691
|
+
if (descriptor.name === "opencode" && !artifact.command_aliases?.opencode && !rootsForArtifact(descriptor, artifact).commands) {
|
|
692
|
+
const marker = artifact.skills_only_compatibility ?? metadata.compatibility?.opencode_skills_only;
|
|
693
|
+
if (!releaseListedForSkillsOnlyCompatibility(marker, release)) {
|
|
694
|
+
return {
|
|
695
|
+
blocker: metadataBlocker(
|
|
696
|
+
"opencode-skills-only-compatibility-unmarked",
|
|
697
|
+
"Opencode skills-only archive metadata is not explicitly marked compatible.",
|
|
698
|
+
artifact.archive,
|
|
699
|
+
"Use an opencode archive with command alias metadata or bundled trusted skills-only compatibility metadata.",
|
|
700
|
+
),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
430
704
|
return { artifact };
|
|
431
705
|
}
|
|
432
706
|
|
|
@@ -500,36 +774,66 @@ function parseZipEntries(buffer) {
|
|
|
500
774
|
return entries;
|
|
501
775
|
}
|
|
502
776
|
|
|
503
|
-
function unsafePathCode(name) {
|
|
777
|
+
function unsafePathCode(name, descriptor, artifact) {
|
|
504
778
|
if (!name || name.startsWith("/") || name.startsWith("\\") || /^[A-Za-z]:/.test(name) || name.includes("\\")) {
|
|
505
779
|
return "archive-path-traversal";
|
|
506
780
|
}
|
|
507
781
|
if (name.split("/").some((part) => part === ".." || part === "")) {
|
|
508
782
|
return "archive-path-traversal";
|
|
509
783
|
}
|
|
510
|
-
|
|
784
|
+
const allowedRoots = Object.values(rootsForArtifact(descriptor, artifact));
|
|
785
|
+
if (!allowedRoots.some((root) => name.startsWith(`${root}/`))) {
|
|
511
786
|
return "archive-install-root-invalid";
|
|
512
787
|
}
|
|
513
788
|
return undefined;
|
|
514
789
|
}
|
|
515
790
|
|
|
516
791
|
function isArchiveSupportEntry(name) {
|
|
517
|
-
return name === "AGENTS.md";
|
|
792
|
+
return name === "AGENTS.md" || name === "CLAUDE.md";
|
|
518
793
|
}
|
|
519
794
|
|
|
520
|
-
function
|
|
795
|
+
function fileRowsForTreeRoot(entries, installRoot) {
|
|
521
796
|
return entries
|
|
522
|
-
.filter((entry) => !entry.directory)
|
|
797
|
+
.filter((entry) => !entry.directory && entry.name.startsWith(`${installRoot}/`))
|
|
523
798
|
.map((entry) => {
|
|
524
|
-
const relativePath = entry.name.slice(`${
|
|
799
|
+
const relativePath = entry.name.slice(`${installRoot}/`.length);
|
|
525
800
|
const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
|
|
526
801
|
return [relativePath, sha256(bytes)];
|
|
527
802
|
})
|
|
528
803
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
529
804
|
}
|
|
530
805
|
|
|
531
|
-
function
|
|
532
|
-
|
|
806
|
+
function opencodeCommandAliasPaths(artifact) {
|
|
807
|
+
const aliases = artifact?.command_aliases?.opencode;
|
|
808
|
+
if (!aliases) {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
if (Array.isArray(aliases.paths)) {
|
|
812
|
+
return aliases.paths;
|
|
813
|
+
}
|
|
814
|
+
if (aliases.aliases && typeof aliases.aliases === "object") {
|
|
815
|
+
return Object.values(aliases.aliases);
|
|
816
|
+
}
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function treeHashForEntries(entries, descriptor) {
|
|
821
|
+
return treeHashForRows(fileRowsForTreeRoot(entries, descriptor.primaryInstallRoot()));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function rootHashesForEntries(entries, descriptor, artifact) {
|
|
825
|
+
return Object.fromEntries(
|
|
826
|
+
Object.entries(rootsForArtifact(descriptor, artifact)).map(([role, root]) => {
|
|
827
|
+
const rows = fileRowsForTreeRoot(entries, root);
|
|
828
|
+
return [
|
|
829
|
+
role,
|
|
830
|
+
{
|
|
831
|
+
tree_sha256: treeHashForRows(rows),
|
|
832
|
+
file_count: rows.length,
|
|
833
|
+
},
|
|
834
|
+
];
|
|
835
|
+
}),
|
|
836
|
+
);
|
|
533
837
|
}
|
|
534
838
|
|
|
535
839
|
function treeHashForRows(rows) {
|
|
@@ -573,22 +877,26 @@ function treeHashForFilesystem(root) {
|
|
|
573
877
|
};
|
|
574
878
|
}
|
|
575
879
|
|
|
576
|
-
function
|
|
880
|
+
function currentLockfileEntries() {
|
|
577
881
|
const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
578
882
|
if (!existsSync(lockfileAbsolutePath)) {
|
|
579
|
-
return
|
|
883
|
+
return [];
|
|
580
884
|
}
|
|
581
885
|
const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
|
|
582
886
|
if (!parsed.ok) {
|
|
583
|
-
return
|
|
887
|
+
return [];
|
|
584
888
|
}
|
|
585
|
-
return parsed.lockfile.generated.
|
|
889
|
+
return parsed.lockfile.generated.targets ?? parsed.lockfile.generated.adapters;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function currentLockfileEntry(descriptor) {
|
|
893
|
+
return currentLockfileEntries().find((entry) => (entry.target ?? entry.adapter) === descriptor.name);
|
|
586
894
|
}
|
|
587
895
|
|
|
588
896
|
function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) {
|
|
589
897
|
return {
|
|
590
898
|
code: "installed-tree-mismatch",
|
|
591
|
-
message: "Installed
|
|
899
|
+
message: "Installed adapter tree does not match trusted metadata.",
|
|
592
900
|
expected_tree_sha256: expectedTreeHash,
|
|
593
901
|
actual_tree_sha256: actualTree.treeHash,
|
|
594
902
|
expected_file_count: expectedFileCount,
|
|
@@ -596,23 +904,29 @@ function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCo
|
|
|
596
904
|
};
|
|
597
905
|
}
|
|
598
906
|
|
|
599
|
-
function verifyInstalledTree(entries, artifact, { allowMissingOrEmpty = false } = {}) {
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
|
|
907
|
+
function verifyInstalledTree(entries, artifact, descriptor, { allowMissingOrEmpty = false } = {}) {
|
|
908
|
+
const expectedRootHashes = rootHashesForEntries(entries, descriptor, artifact);
|
|
909
|
+
for (const [role, root] of Object.entries(rootsForArtifact(descriptor, artifact))) {
|
|
910
|
+
const expectedRows = fileRowsForTreeRoot(entries, root);
|
|
911
|
+
const expectedTreeHash = artifact.root_hashes?.[role]?.tree_sha256 ?? artifact.tree_sha256;
|
|
912
|
+
const expectedFileCount = artifact.root_hashes?.[role]?.file_count ?? expectedRows.length;
|
|
603
913
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
914
|
+
if (!existsSync(resolve(process.cwd(), root))) {
|
|
915
|
+
if (allowMissingOrEmpty) {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
return { error: installedTreeMismatchError({ treeHash: "<missing>", fileCount: 0 }, expectedTreeHash, expectedFileCount) };
|
|
919
|
+
}
|
|
607
920
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
921
|
+
const actualTree = treeHashForFilesystem(root);
|
|
922
|
+
if (allowMissingOrEmpty && actualTree.fileCount === 0) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (!rowsEqual(actualTree.rows, expectedRows) || actualTree.treeHash !== expectedTreeHash || actualTree.fileCount !== expectedFileCount) {
|
|
926
|
+
return { error: installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) };
|
|
927
|
+
}
|
|
614
928
|
}
|
|
615
|
-
return { ok: true,
|
|
929
|
+
return { ok: true, rootHashes: expectedRootHashes, expectedFileCount: expectedRootHashes.skills?.file_count ?? 0, treeHash: expectedRootHashes.skills?.tree_sha256 };
|
|
616
930
|
}
|
|
617
931
|
|
|
618
932
|
function generatedOutputConflictBlocker(entries) {
|
|
@@ -655,13 +969,29 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
655
969
|
if (!lockfileEntry) {
|
|
656
970
|
return undefined;
|
|
657
971
|
}
|
|
972
|
+
if (lockfileEntry.installed_roots) {
|
|
973
|
+
for (const [role, root] of Object.entries(lockfileEntry.installed_roots)) {
|
|
974
|
+
const rootHash = lockfileEntry.root_hashes[role];
|
|
975
|
+
const blocker = lockfileDriftBlocker({
|
|
976
|
+
adapter: lockfileEntry.adapter,
|
|
977
|
+
target: lockfileEntry.target,
|
|
978
|
+
installed_root: root,
|
|
979
|
+
tree_sha256: rootHash.tree_sha256,
|
|
980
|
+
file_count: rootHash.file_count,
|
|
981
|
+
});
|
|
982
|
+
if (blocker) {
|
|
983
|
+
return blocker;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
658
988
|
|
|
659
989
|
const rootState = pathState(resolve(process.cwd(), lockfileEntry.installed_root));
|
|
660
990
|
if (rootState === "absent") {
|
|
661
991
|
return {
|
|
662
992
|
code: "generated-output-missing",
|
|
663
993
|
message: "Codex generated output recorded in rigorloop.lock is missing.",
|
|
664
|
-
|
|
994
|
+
target: lockfileEntry.target ?? lockfileEntry.adapter,
|
|
665
995
|
installed_root: lockfileEntry.installed_root,
|
|
666
996
|
expected_tree_sha256: lockfileEntry.tree_sha256,
|
|
667
997
|
actual_tree_sha256: null,
|
|
@@ -682,7 +1012,7 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
682
1012
|
return {
|
|
683
1013
|
code: "generated-output-drift",
|
|
684
1014
|
message: "Codex generated output differs from rigorloop.lock.",
|
|
685
|
-
|
|
1015
|
+
target: lockfileEntry.target ?? lockfileEntry.adapter,
|
|
686
1016
|
installed_root: lockfileEntry.installed_root,
|
|
687
1017
|
expected_tree_sha256: lockfileEntry.tree_sha256,
|
|
688
1018
|
actual_tree_sha256: actualTree.treeHash,
|
|
@@ -695,7 +1025,127 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
695
1025
|
return undefined;
|
|
696
1026
|
}
|
|
697
1027
|
|
|
698
|
-
function
|
|
1028
|
+
function lockfileEntryTarget(entry) {
|
|
1029
|
+
return entry.target ?? entry.adapter;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function lockfileEntryRoots(entry) {
|
|
1033
|
+
if (entry.installed_roots) {
|
|
1034
|
+
return Object.values(entry.installed_roots);
|
|
1035
|
+
}
|
|
1036
|
+
return entry.installed_root ? [entry.installed_root] : [];
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function rootsOverlap(leftRoots, rightRoots) {
|
|
1040
|
+
return leftRoots.some((left) =>
|
|
1041
|
+
rightRoots.some((right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`)),
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function targetRootConflictBlocker(target, path, reason) {
|
|
1046
|
+
return {
|
|
1047
|
+
code: "target-root-conflict",
|
|
1048
|
+
message: reason,
|
|
1049
|
+
target,
|
|
1050
|
+
path,
|
|
1051
|
+
next_action: "Resolve the existing RigorLoop state before running init.",
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function existingStateSafetyBlocker(descriptor, artifact) {
|
|
1056
|
+
const targetRoots = Object.values(rootsForArtifact(descriptor, artifact));
|
|
1057
|
+
const manifestPath = resolve(process.cwd(), "rigorloop.yaml");
|
|
1058
|
+
if (existsSync(manifestPath)) {
|
|
1059
|
+
const parsedManifest = parseManifestAdapterBlocks(readFileSync(manifestPath, "utf8"));
|
|
1060
|
+
if (parsedManifest.error) {
|
|
1061
|
+
return {
|
|
1062
|
+
code: "state-invalid",
|
|
1063
|
+
message: "Existing RigorLoop state is malformed; refusing to mutate target roots.",
|
|
1064
|
+
path: "rigorloop.yaml",
|
|
1065
|
+
next_action: "Fix or move rigorloop.yaml before running init.",
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const selectedEntries = parsedManifest.adapters.filter((entry) => entry.name === descriptor.name);
|
|
1069
|
+
if (selectedEntries.length > 1) {
|
|
1070
|
+
return {
|
|
1071
|
+
code: "duplicate-target-entry",
|
|
1072
|
+
message: `Existing rigorloop.yaml contains duplicate ${descriptor.displayName} target entries.`,
|
|
1073
|
+
path: "rigorloop.yaml",
|
|
1074
|
+
next_action: "Remove duplicate target entries before running init.",
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
for (const entry of parsedManifest.adapters) {
|
|
1078
|
+
if (entry.name === descriptor.name && entry.roots.length && !rootsOverlap(entry.roots, targetRoots)) {
|
|
1079
|
+
return targetRootConflictBlocker(
|
|
1080
|
+
descriptor.name,
|
|
1081
|
+
"rigorloop.yaml",
|
|
1082
|
+
`Existing RigorLoop state records target ${descriptor.name} with a conflicting install root.`,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
if (entry.name !== descriptor.name && rootsOverlap(entry.roots, targetRoots)) {
|
|
1086
|
+
return targetRootConflictBlocker(
|
|
1087
|
+
descriptor.name,
|
|
1088
|
+
"rigorloop.yaml",
|
|
1089
|
+
`Existing RigorLoop state records an overlapping install root for target ${entry.name}.`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const lockfilePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
1096
|
+
if (!existsSync(lockfilePath)) {
|
|
1097
|
+
return undefined;
|
|
1098
|
+
}
|
|
1099
|
+
const parsedLockfile = parseLockfile(readFileSync(lockfilePath, "utf8"));
|
|
1100
|
+
if (!parsedLockfile.ok) {
|
|
1101
|
+
return {
|
|
1102
|
+
code: parsedLockfile.code,
|
|
1103
|
+
message: "Existing RigorLoop lock state is malformed or unsupported; refusing to mutate target roots.",
|
|
1104
|
+
path: LOCKFILE_PATH,
|
|
1105
|
+
next_action: "Fix or move rigorloop.lock before running init.",
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const entries = parsedLockfile.lockfile.generated.targets ?? parsedLockfile.lockfile.generated.adapters;
|
|
1109
|
+
for (const entry of entries) {
|
|
1110
|
+
const entryTarget = lockfileEntryTarget(entry);
|
|
1111
|
+
const entryRoots = lockfileEntryRoots(entry);
|
|
1112
|
+
const selected = entryTarget === descriptor.name;
|
|
1113
|
+
const overlapping = rootsOverlap(entryRoots, targetRoots);
|
|
1114
|
+
if (selected && entryRoots.length && !overlapping) {
|
|
1115
|
+
return targetRootConflictBlocker(
|
|
1116
|
+
descriptor.name,
|
|
1117
|
+
LOCKFILE_PATH,
|
|
1118
|
+
`Existing RigorLoop lock state records target ${descriptor.name} with a conflicting install root.`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (!selected && overlapping) {
|
|
1122
|
+
return targetRootConflictBlocker(
|
|
1123
|
+
descriptor.name,
|
|
1124
|
+
LOCKFILE_PATH,
|
|
1125
|
+
`Existing RigorLoop lock state records an overlapping install root for target ${entryTarget}.`,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
if (selected || overlapping) {
|
|
1129
|
+
const drift = lockfileDriftBlocker(entry);
|
|
1130
|
+
if (drift) {
|
|
1131
|
+
return drift;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return undefined;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function firstLockfileDriftBlocker() {
|
|
1139
|
+
for (const entry of currentLockfileEntries()) {
|
|
1140
|
+
const blocker = lockfileDriftBlocker(entry);
|
|
1141
|
+
if (blocker) {
|
|
1142
|
+
return blocker;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function inspectArchive(archiveBytes, artifact, descriptor) {
|
|
699
1149
|
if (artifact.size_bytes !== undefined && archiveBytes.length !== artifact.size_bytes) {
|
|
700
1150
|
return { error: { code: "archive-size-mismatch", message: "Archive size does not match metadata." } };
|
|
701
1151
|
}
|
|
@@ -716,7 +1166,7 @@ function inspectArchive(archiveBytes, artifact) {
|
|
|
716
1166
|
if (isArchiveSupportEntry(entry.name)) {
|
|
717
1167
|
continue;
|
|
718
1168
|
}
|
|
719
|
-
const pathCode = unsafePathCode(entry.name);
|
|
1169
|
+
const pathCode = unsafePathCode(entry.name, descriptor, artifact);
|
|
720
1170
|
if (pathCode) {
|
|
721
1171
|
return { error: { code: pathCode, message: `Archive entry is not allowed: ${entry.name}`, path: entry.name } };
|
|
722
1172
|
}
|
|
@@ -727,14 +1177,34 @@ function inspectArchive(archiveBytes, artifact) {
|
|
|
727
1177
|
}
|
|
728
1178
|
|
|
729
1179
|
const files = installEntries.filter((entry) => !entry.directory);
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
1180
|
+
const rootHashes = rootHashesForEntries(files, descriptor, artifact);
|
|
1181
|
+
for (const [role, hash] of Object.entries(rootHashes)) {
|
|
1182
|
+
const expected = artifact.root_hashes?.[role] ?? { tree_sha256: artifact.tree_sha256, file_count: artifact.file_count };
|
|
1183
|
+
if (expected.tree_sha256 && hash.tree_sha256 !== expected.tree_sha256) {
|
|
1184
|
+
return { error: { code: "tree-hash-mismatch", message: "Installed tree hash does not match metadata." } };
|
|
1185
|
+
}
|
|
1186
|
+
if (expected.file_count !== undefined && hash.file_count !== expected.file_count) {
|
|
1187
|
+
return { error: { code: "tree-hash-mismatch", message: "Installed tree file count does not match metadata." } };
|
|
1188
|
+
}
|
|
733
1189
|
}
|
|
734
|
-
|
|
1190
|
+
if (descriptor.name === "opencode") {
|
|
1191
|
+
for (const aliasPath of opencodeCommandAliasPaths(artifact) ?? []) {
|
|
1192
|
+
if (!files.some((entry) => entry.name === aliasPath)) {
|
|
1193
|
+
return {
|
|
1194
|
+
error: {
|
|
1195
|
+
code: "opencode-command-alias-missing",
|
|
1196
|
+
message: "Declared opencode command alias is missing from archive.",
|
|
1197
|
+
path: aliasPath,
|
|
1198
|
+
},
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return { entries: files, archiveHash, rootHashes, treeHash: rootHashes.skills?.tree_sha256, fileCount: rootHashes.skills?.file_count ?? files.length };
|
|
735
1204
|
}
|
|
736
1205
|
|
|
737
|
-
function addArchiveActions(plan, entries) {
|
|
1206
|
+
function addArchiveActions(plan, entries, descriptor) {
|
|
1207
|
+
const installRoot = descriptor.primaryInstallRoot();
|
|
738
1208
|
const directories = new Set();
|
|
739
1209
|
for (const entry of entries) {
|
|
740
1210
|
const parts = entry.name.split("/");
|
|
@@ -770,7 +1240,7 @@ function addArchiveActions(plan, entries) {
|
|
|
770
1240
|
const state = pathState(resolve(process.cwd(), entry.name));
|
|
771
1241
|
let existingMatches = false;
|
|
772
1242
|
if (state === "file") {
|
|
773
|
-
const relativePath = entry.name.slice(`${
|
|
1243
|
+
const relativePath = entry.name.slice(`${installRoot}/`.length);
|
|
774
1244
|
const existingBytes = relativePath.endsWith(".md")
|
|
775
1245
|
? normalizeText(readFileSync(resolve(process.cwd(), entry.name)))
|
|
776
1246
|
: readFileSync(resolve(process.cwd(), entry.name));
|
|
@@ -783,9 +1253,9 @@ function addArchiveActions(plan, entries) {
|
|
|
783
1253
|
status: state === "absent" ? "pending" : existingMatches ? "skipped" : "blocked",
|
|
784
1254
|
reason:
|
|
785
1255
|
state === "absent"
|
|
786
|
-
?
|
|
1256
|
+
? `Install verified ${descriptor.displayName} adapter file.`
|
|
787
1257
|
: existingMatches
|
|
788
|
-
? `${entry.name} already matches verified
|
|
1258
|
+
? `${entry.name} already matches verified ${descriptor.displayName} adapter content.`
|
|
789
1259
|
: `${entry.name} already exists.`,
|
|
790
1260
|
});
|
|
791
1261
|
plan.artifacts.push({
|
|
@@ -804,23 +1274,47 @@ function addArchiveActions(plan, entries) {
|
|
|
804
1274
|
}
|
|
805
1275
|
}
|
|
806
1276
|
|
|
807
|
-
function writeArchiveEntries(entries) {
|
|
1277
|
+
function writeArchiveEntries(entries, descriptor) {
|
|
1278
|
+
const installRoot = descriptor.primaryInstallRoot();
|
|
808
1279
|
for (const entry of entries) {
|
|
809
1280
|
const outputPath = resolve(process.cwd(), entry.name);
|
|
810
1281
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
811
|
-
const relativePath = entry.name.slice(`${
|
|
1282
|
+
const relativePath = entry.name.slice(`${installRoot}/`.length);
|
|
812
1283
|
const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
|
|
813
1284
|
writeFileSync(outputPath, bytes);
|
|
814
1285
|
}
|
|
815
1286
|
}
|
|
816
1287
|
|
|
817
|
-
function
|
|
1288
|
+
function initWarnings(descriptor, artifact) {
|
|
1289
|
+
if (
|
|
1290
|
+
descriptor.name === "opencode" &&
|
|
1291
|
+
artifact &&
|
|
1292
|
+
!artifact.command_aliases?.opencode &&
|
|
1293
|
+
!rootsForArtifact(descriptor, artifact).commands
|
|
1294
|
+
) {
|
|
1295
|
+
return [
|
|
1296
|
+
{
|
|
1297
|
+
code: "opencode-command-aliases-not-declared",
|
|
1298
|
+
message: "Selected opencode archive metadata does not declare command aliases; only skills were installed.",
|
|
1299
|
+
},
|
|
1300
|
+
];
|
|
1301
|
+
}
|
|
1302
|
+
return [];
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function directoryPlanForRoots(roots) {
|
|
1306
|
+
return [...new Set(Object.values(roots).flatMap((root) => [root.split("/").slice(0, -1).join("/"), root]).filter(Boolean))];
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function planDirectoryActions(flags, descriptor, artifact) {
|
|
818
1310
|
const actions = [];
|
|
819
1311
|
const artifacts = [];
|
|
820
1312
|
const blockers = [];
|
|
821
1313
|
let parentBlocked = false;
|
|
822
1314
|
|
|
823
|
-
|
|
1315
|
+
const directoryPlan = directoryPlanForRoots(rootsForArtifact(descriptor, artifact));
|
|
1316
|
+
const rootParent = directoryPlan[0];
|
|
1317
|
+
for (const relativePath of directoryPlan) {
|
|
824
1318
|
const state = parentBlocked ? "blocked-by-parent" : pathState(resolve(process.cwd(), relativePath));
|
|
825
1319
|
if (state === "absent") {
|
|
826
1320
|
actions.push({
|
|
@@ -831,7 +1325,7 @@ function planDirectoryActions(flags) {
|
|
|
831
1325
|
});
|
|
832
1326
|
artifacts.push({
|
|
833
1327
|
path: relativePath,
|
|
834
|
-
kind: directoryKind(relativePath),
|
|
1328
|
+
kind: directoryKind(relativePath, descriptor, artifact),
|
|
835
1329
|
status: flags.dryRun ? "planned" : "pending",
|
|
836
1330
|
});
|
|
837
1331
|
} else if (state === "directory") {
|
|
@@ -843,7 +1337,7 @@ function planDirectoryActions(flags) {
|
|
|
843
1337
|
});
|
|
844
1338
|
artifacts.push({
|
|
845
1339
|
path: relativePath,
|
|
846
|
-
kind: directoryKind(relativePath),
|
|
1340
|
+
kind: directoryKind(relativePath, descriptor, artifact),
|
|
847
1341
|
status: "existing",
|
|
848
1342
|
});
|
|
849
1343
|
} else {
|
|
@@ -853,12 +1347,12 @@ function planDirectoryActions(flags) {
|
|
|
853
1347
|
status: "blocked",
|
|
854
1348
|
reason:
|
|
855
1349
|
state === "blocked-by-parent"
|
|
856
|
-
? `${relativePath} cannot be created because ${
|
|
1350
|
+
? `${relativePath} cannot be created because ${rootParent} is not a directory.`
|
|
857
1351
|
: `${relativePath} exists and is not a directory.`,
|
|
858
1352
|
});
|
|
859
1353
|
artifacts.push({
|
|
860
1354
|
path: relativePath,
|
|
861
|
-
kind: directoryKind(relativePath),
|
|
1355
|
+
kind: directoryKind(relativePath, descriptor, artifact),
|
|
862
1356
|
status: "blocked",
|
|
863
1357
|
});
|
|
864
1358
|
if (state !== "blocked-by-parent") {
|
|
@@ -869,7 +1363,7 @@ function planDirectoryActions(flags) {
|
|
|
869
1363
|
next_action: `Move the existing file before running init.`,
|
|
870
1364
|
});
|
|
871
1365
|
}
|
|
872
|
-
if (relativePath ===
|
|
1366
|
+
if (relativePath === rootParent) {
|
|
873
1367
|
parentBlocked = true;
|
|
874
1368
|
}
|
|
875
1369
|
}
|
|
@@ -887,7 +1381,7 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
|
|
|
887
1381
|
status: flags.dryRun ? "planned" : "pending",
|
|
888
1382
|
reason: flags.dryRun
|
|
889
1383
|
? "Plan durable lockfile content."
|
|
890
|
-
: "Write durable lockfile after verified
|
|
1384
|
+
: "Write durable lockfile after verified target install.",
|
|
891
1385
|
});
|
|
892
1386
|
artifacts.push({
|
|
893
1387
|
path: LOCKFILE_PATH,
|
|
@@ -905,7 +1399,7 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
|
|
|
905
1399
|
status: flags.dryRun ? "planned" : "pending",
|
|
906
1400
|
reason: flags.dryRun
|
|
907
1401
|
? "Plan update to supported rigorloop.lock."
|
|
908
|
-
: "Update supported rigorloop.lock after verified
|
|
1402
|
+
: "Update supported rigorloop.lock after verified target install.",
|
|
909
1403
|
});
|
|
910
1404
|
artifacts.push({
|
|
911
1405
|
path: LOCKFILE_PATH,
|
|
@@ -944,15 +1438,16 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
|
|
|
944
1438
|
}
|
|
945
1439
|
}
|
|
946
1440
|
|
|
947
|
-
function buildInitPlan(flags, artifact) {
|
|
1441
|
+
function buildInitPlan(flags, descriptor, artifact) {
|
|
948
1442
|
const info = packageInfo();
|
|
949
|
-
const source = sourceForFlags(flags, info);
|
|
1443
|
+
const source = sourceForFlags(flags, info, descriptor);
|
|
950
1444
|
if (artifact) {
|
|
951
1445
|
source.artifact = artifact;
|
|
952
1446
|
}
|
|
953
1447
|
const manifestPath = "rigorloop.yaml";
|
|
954
1448
|
const manifestAbsolutePath = resolve(process.cwd(), manifestPath);
|
|
955
|
-
const
|
|
1449
|
+
const existingManifest = existsSync(manifestAbsolutePath) ? readFileSync(manifestAbsolutePath, "utf8") : undefined;
|
|
1450
|
+
const manifest = flags.writeState ? manifestContent(info, source, descriptor, existingManifest) : undefined;
|
|
956
1451
|
const actions = [];
|
|
957
1452
|
const artifacts = [];
|
|
958
1453
|
const blockers = [];
|
|
@@ -963,59 +1458,67 @@ function buildInitPlan(flags, artifact) {
|
|
|
963
1458
|
code: "invalid-archive-path",
|
|
964
1459
|
message: "Missing required value for --from-archive.",
|
|
965
1460
|
path: "--from-archive",
|
|
966
|
-
next_action:
|
|
1461
|
+
next_action: `Provide an existing ${descriptor.displayName} adapter archive path or omit --from-archive.`,
|
|
967
1462
|
});
|
|
968
1463
|
} else if (flags.fromArchiveProvided && !existsSync(resolve(process.cwd(), flags.fromArchive))) {
|
|
969
1464
|
errors.push({
|
|
970
1465
|
code: "invalid-archive-path",
|
|
971
1466
|
message: `Local archive path does not exist: ${flags.fromArchive}`,
|
|
972
1467
|
path: flags.fromArchive,
|
|
973
|
-
next_action:
|
|
1468
|
+
next_action: `Provide an existing ${descriptor.displayName} adapter archive path or omit --from-archive.`,
|
|
974
1469
|
});
|
|
975
1470
|
}
|
|
976
1471
|
|
|
977
|
-
const directoryPlan = planDirectoryActions(flags);
|
|
1472
|
+
const directoryPlan = planDirectoryActions(flags, descriptor, artifact);
|
|
978
1473
|
actions.push(...directoryPlan.actions);
|
|
979
1474
|
artifacts.push(...directoryPlan.artifacts);
|
|
980
1475
|
blockers.push(...directoryPlan.blockers);
|
|
981
1476
|
|
|
982
|
-
if (
|
|
983
|
-
|
|
984
|
-
|
|
1477
|
+
if (flags.writeState) {
|
|
1478
|
+
if (existingManifest !== undefined) {
|
|
1479
|
+
const parsedManifest = parseManifestAdapterBlocks(existingManifest);
|
|
1480
|
+
if (parsedManifest.error) {
|
|
1481
|
+
errors.push({
|
|
1482
|
+
code: parsedManifest.error.code,
|
|
1483
|
+
message: parsedManifest.error.message,
|
|
1484
|
+
path: manifestPath,
|
|
1485
|
+
next_action: "Review or move the existing file before running init.",
|
|
1486
|
+
});
|
|
1487
|
+
} else if (parsedManifest.adapters.filter((entry) => entry.name === descriptor.name).length > 1) {
|
|
1488
|
+
blockers.push({
|
|
1489
|
+
code: "duplicate-target-entry",
|
|
1490
|
+
message: `Existing rigorloop.yaml contains duplicate ${descriptor.displayName} target entries.`,
|
|
1491
|
+
path: manifestPath,
|
|
1492
|
+
next_action: "Remove duplicate target entries before running init.",
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
985
1495
|
actions.push({
|
|
986
1496
|
type: "write",
|
|
987
1497
|
path: manifestPath,
|
|
988
|
-
status: flags.dryRun ? "planned" : "
|
|
989
|
-
reason:
|
|
1498
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1499
|
+
reason: `Write target-oriented rigorloop.yaml for ${descriptor.displayName} support.`,
|
|
990
1500
|
});
|
|
991
1501
|
artifacts.push({
|
|
992
1502
|
path: manifestPath,
|
|
993
1503
|
kind: "project-manifest",
|
|
994
|
-
status: "
|
|
1504
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
995
1505
|
});
|
|
996
1506
|
} else {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
message: "Existing rigorloop.yaml is not compatible with the first-slice Codex init contract.",
|
|
1507
|
+
actions.push({
|
|
1508
|
+
type: "write",
|
|
1000
1509
|
path: manifestPath,
|
|
1001
|
-
|
|
1510
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1511
|
+
reason: "Create target-oriented RigorLoop project manifest.",
|
|
1512
|
+
});
|
|
1513
|
+
artifacts.push({
|
|
1514
|
+
path: manifestPath,
|
|
1515
|
+
kind: "project-manifest",
|
|
1516
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1002
1517
|
});
|
|
1003
1518
|
}
|
|
1004
|
-
} else {
|
|
1005
|
-
actions.push({
|
|
1006
|
-
type: "write",
|
|
1007
|
-
path: manifestPath,
|
|
1008
|
-
status: flags.dryRun ? "planned" : "pending",
|
|
1009
|
-
reason: "Create first-slice RigorLoop project manifest.",
|
|
1010
|
-
});
|
|
1011
|
-
artifacts.push({
|
|
1012
|
-
path: manifestPath,
|
|
1013
|
-
kind: "project-manifest",
|
|
1014
|
-
status: flags.dryRun ? "planned" : "pending",
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
1519
|
|
|
1018
|
-
|
|
1520
|
+
addLockfilePlan(flags, actions, artifacts, blockers, errors);
|
|
1521
|
+
}
|
|
1019
1522
|
|
|
1020
1523
|
return {
|
|
1021
1524
|
info,
|
|
@@ -1025,7 +1528,7 @@ function buildInitPlan(flags, artifact) {
|
|
|
1025
1528
|
artifacts,
|
|
1026
1529
|
blockers,
|
|
1027
1530
|
errors,
|
|
1028
|
-
planned_lockfile: plannedLockfile(info, source, manifest),
|
|
1531
|
+
planned_lockfile: flags.writeState ? plannedLockfile(info, source, manifest, descriptor) : undefined,
|
|
1029
1532
|
};
|
|
1030
1533
|
}
|
|
1031
1534
|
|
|
@@ -1111,19 +1614,19 @@ function invalidArchivePath(message, flags) {
|
|
|
1111
1614
|
code: "invalid-archive-path",
|
|
1112
1615
|
message,
|
|
1113
1616
|
path: flags.fromArchive,
|
|
1114
|
-
next_action: "Provide an existing
|
|
1617
|
+
next_action: "Provide an existing supported adapter archive path or omit --from-archive.",
|
|
1115
1618
|
});
|
|
1116
1619
|
}
|
|
1117
1620
|
|
|
1118
1621
|
function unsupportedAdapter(adapter, flags) {
|
|
1119
1622
|
const result = envelope("init", flags, {
|
|
1120
1623
|
status: "blocked",
|
|
1121
|
-
summary: `
|
|
1624
|
+
summary: `Target '${adapter}' is not supported.`,
|
|
1122
1625
|
blockers: [
|
|
1123
1626
|
{
|
|
1124
|
-
code: "
|
|
1125
|
-
message: `
|
|
1126
|
-
next_action:
|
|
1627
|
+
code: "target-unknown",
|
|
1628
|
+
message: `Target '${adapter}' is not supported.`,
|
|
1629
|
+
next_action: `Use one of: ${supportedAdapterNames().join(", ")}.`,
|
|
1127
1630
|
},
|
|
1128
1631
|
],
|
|
1129
1632
|
});
|
|
@@ -1131,11 +1634,32 @@ function unsupportedAdapter(adapter, flags) {
|
|
|
1131
1634
|
if (flags.json) {
|
|
1132
1635
|
writeJson(result);
|
|
1133
1636
|
} else {
|
|
1134
|
-
process.stderr.write(`${result.summary}\nUse
|
|
1637
|
+
process.stderr.write(`${result.summary}\nUse one of: ${supportedAdapterNames().join(", ")}.\n`);
|
|
1135
1638
|
}
|
|
1136
1639
|
return exitCodeForResult({ ...result, exit_class: "blocked" });
|
|
1137
1640
|
}
|
|
1138
1641
|
|
|
1642
|
+
function removedAdapterSyntax(flags) {
|
|
1643
|
+
const targets = supportedAdapterNames();
|
|
1644
|
+
const result = envelope("init", flags, {
|
|
1645
|
+
status: "error",
|
|
1646
|
+
summary: "`init --adapter` was removed in RigorLoop 0.3.0.",
|
|
1647
|
+
errors: [
|
|
1648
|
+
{
|
|
1649
|
+
code: "adapter-option-removed",
|
|
1650
|
+
message: "`init --adapter` was removed in RigorLoop 0.3.0.",
|
|
1651
|
+
next_action: `Use target-native init: ${targets.map((target) => `rigorloop init ${target}`).join(", ")}.`,
|
|
1652
|
+
},
|
|
1653
|
+
],
|
|
1654
|
+
});
|
|
1655
|
+
if (flags.json) {
|
|
1656
|
+
writeJson(result);
|
|
1657
|
+
} else {
|
|
1658
|
+
process.stderr.write(`${result.summary}\n${result.errors[0].next_action}\n`);
|
|
1659
|
+
}
|
|
1660
|
+
return exitCodeForResult({ ...result, exit_class: "invalid_usage" });
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1139
1663
|
function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked") {
|
|
1140
1664
|
for (const action of plan.actions) {
|
|
1141
1665
|
if (action.status === "pending") {
|
|
@@ -1148,18 +1672,26 @@ function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked
|
|
|
1148
1672
|
artifact.status = "blocked";
|
|
1149
1673
|
}
|
|
1150
1674
|
}
|
|
1675
|
+
const statePlan = flags.writeState
|
|
1676
|
+
? {
|
|
1677
|
+
planned_manifest: {
|
|
1678
|
+
path: "rigorloop.yaml",
|
|
1679
|
+
content: plan.manifest,
|
|
1680
|
+
},
|
|
1681
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1682
|
+
}
|
|
1683
|
+
: {};
|
|
1151
1684
|
const result = envelope("init", flags, {
|
|
1152
1685
|
status: "blocked",
|
|
1153
1686
|
summary,
|
|
1154
1687
|
actions: plan.actions,
|
|
1155
1688
|
artifacts: plan.artifacts,
|
|
1156
1689
|
blockers,
|
|
1157
|
-
|
|
1158
|
-
path: "rigorloop.yaml",
|
|
1159
|
-
content: plan.manifest,
|
|
1160
|
-
},
|
|
1161
|
-
planned_lockfile: plan.planned_lockfile,
|
|
1690
|
+
...statePlan,
|
|
1162
1691
|
});
|
|
1692
|
+
if (blockers[0]?.diagnostics) {
|
|
1693
|
+
result.diagnostics = { ...result.diagnostics, ...blockers[0].diagnostics };
|
|
1694
|
+
}
|
|
1163
1695
|
if (flags.json) {
|
|
1164
1696
|
writeJson(result);
|
|
1165
1697
|
} else {
|
|
@@ -1184,17 +1716,22 @@ function writeValidationErrorResult(flags, plan, error) {
|
|
|
1184
1716
|
artifact.status = "blocked";
|
|
1185
1717
|
}
|
|
1186
1718
|
}
|
|
1719
|
+
const statePlan = flags.writeState
|
|
1720
|
+
? {
|
|
1721
|
+
planned_manifest: {
|
|
1722
|
+
path: "rigorloop.yaml",
|
|
1723
|
+
content: plan.manifest,
|
|
1724
|
+
},
|
|
1725
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1726
|
+
}
|
|
1727
|
+
: {};
|
|
1187
1728
|
const result = envelope("init", flags, {
|
|
1188
1729
|
status: "error",
|
|
1189
1730
|
summary: error.message,
|
|
1190
1731
|
actions: plan.actions,
|
|
1191
1732
|
artifacts: plan.artifacts,
|
|
1192
1733
|
errors: [error],
|
|
1193
|
-
|
|
1194
|
-
path: "rigorloop.yaml",
|
|
1195
|
-
content: plan.manifest,
|
|
1196
|
-
},
|
|
1197
|
-
planned_lockfile: plan.planned_lockfile,
|
|
1734
|
+
...statePlan,
|
|
1198
1735
|
});
|
|
1199
1736
|
if (flags.json) {
|
|
1200
1737
|
writeJson(result);
|
|
@@ -1204,18 +1741,27 @@ function writeValidationErrorResult(flags, plan, error) {
|
|
|
1204
1741
|
return exitCodeForResult({ ...result, exit_class: "validation_failed" });
|
|
1205
1742
|
}
|
|
1206
1743
|
|
|
1207
|
-
async function archiveWorkForInit(flags, info) {
|
|
1208
|
-
if (flags.dryRun) {
|
|
1744
|
+
async function archiveWorkForInit(flags, info, descriptor) {
|
|
1745
|
+
if (flags.dryRun && !flags.fromArchiveProvided) {
|
|
1209
1746
|
return {};
|
|
1210
1747
|
}
|
|
1211
1748
|
|
|
1212
1749
|
const bundledMetadata = loadVerifiedBundledMetadata(info);
|
|
1213
1750
|
if (bundledMetadata.blocker || bundledMetadata.error) {
|
|
1751
|
+
if (flags.dryRun) {
|
|
1752
|
+
return {};
|
|
1753
|
+
}
|
|
1214
1754
|
return bundledMetadata;
|
|
1215
1755
|
}
|
|
1216
1756
|
const metadata = bundledMetadata.metadata;
|
|
1217
|
-
const validation = validateMetadata(metadata, info);
|
|
1757
|
+
const validation = validateMetadata(metadata, info, descriptor);
|
|
1218
1758
|
if (validation.blocker || validation.error) {
|
|
1759
|
+
if (
|
|
1760
|
+
flags.dryRun &&
|
|
1761
|
+
!["opencode-command-aliases-missing", "opencode-skills-only-compatibility-unmarked"].includes(validation.blocker?.code)
|
|
1762
|
+
) {
|
|
1763
|
+
return {};
|
|
1764
|
+
}
|
|
1219
1765
|
return validation;
|
|
1220
1766
|
}
|
|
1221
1767
|
const artifact = validation.artifact;
|
|
@@ -1223,17 +1769,31 @@ async function archiveWorkForInit(flags, info) {
|
|
|
1223
1769
|
if (flags.fromArchiveProvided) {
|
|
1224
1770
|
const archiveName = basename(flags.fromArchive);
|
|
1225
1771
|
if (archiveName !== artifact.archive || !archiveName.includes(metadata.release.version)) {
|
|
1772
|
+
if (!archiveName.startsWith(`rigorloop-adapter-${descriptor.name}-`)) {
|
|
1773
|
+
return {
|
|
1774
|
+
error: {
|
|
1775
|
+
code: "adapter-archive-mismatch",
|
|
1776
|
+
message: `Local archive ${archiveName} is not a ${descriptor.displayName} adapter archive.`,
|
|
1777
|
+
path: flags.fromArchive,
|
|
1778
|
+
},
|
|
1779
|
+
artifact,
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1226
1782
|
return {
|
|
1227
1783
|
blocker: metadataBlocker(
|
|
1228
1784
|
"release-version-incompatible",
|
|
1229
1785
|
`Local archive ${archiveName} is not compatible with ${metadata.release.version}.`,
|
|
1230
1786
|
flags.fromArchive,
|
|
1231
|
-
|
|
1787
|
+
`Use the ${descriptor.displayName} adapter archive matching the installed CLI package version.`,
|
|
1232
1788
|
),
|
|
1233
1789
|
};
|
|
1234
1790
|
}
|
|
1791
|
+
// CR-M2-R2-F1: dry-run planning must use trusted metadata roots without reading or extracting archive bytes.
|
|
1792
|
+
if (flags.dryRun) {
|
|
1793
|
+
return { artifact };
|
|
1794
|
+
}
|
|
1235
1795
|
const archiveBytes = readFileSync(resolve(process.cwd(), flags.fromArchive));
|
|
1236
|
-
const inspected = inspectArchive(archiveBytes, artifact);
|
|
1796
|
+
const inspected = inspectArchive(archiveBytes, artifact, descriptor);
|
|
1237
1797
|
if (inspected.error) {
|
|
1238
1798
|
return { error: inspected.error, artifact };
|
|
1239
1799
|
}
|
|
@@ -1258,29 +1818,29 @@ async function archiveWorkForInit(flags, info) {
|
|
|
1258
1818
|
}
|
|
1259
1819
|
try {
|
|
1260
1820
|
archiveBytes = await fetchBytes(artifact.url);
|
|
1261
|
-
} catch {
|
|
1821
|
+
} catch (error) {
|
|
1262
1822
|
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
|
-
),
|
|
1823
|
+
blocker: downloadFailureBlocker(error, artifact, descriptor, metadata),
|
|
1269
1824
|
};
|
|
1270
1825
|
}
|
|
1271
|
-
const inspected = inspectArchive(archiveBytes, artifact);
|
|
1826
|
+
const inspected = inspectArchive(archiveBytes, artifact, descriptor);
|
|
1272
1827
|
if (inspected.error) {
|
|
1273
1828
|
return { error: inspected.error, artifact };
|
|
1274
1829
|
}
|
|
1275
1830
|
return { artifact, entries: inspected.entries, archiveHash: inspected.archiveHash, treeHash: inspected.treeHash };
|
|
1276
1831
|
}
|
|
1277
1832
|
|
|
1278
|
-
async function handleInit(flags) {
|
|
1279
|
-
if (
|
|
1280
|
-
return
|
|
1833
|
+
async function handleInit(flags, initArgs = []) {
|
|
1834
|
+
if (flags.adapterOptionUsed) {
|
|
1835
|
+
return removedAdapterSyntax(flags);
|
|
1836
|
+
}
|
|
1837
|
+
if (initArgs.length !== 1) {
|
|
1838
|
+
return invalidUsage(`init requires exactly one target: ${supportedAdapterNames().join(", ")}.`, flags, "init");
|
|
1281
1839
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1840
|
+
const target = initArgs[0];
|
|
1841
|
+
const descriptor = adapterDescriptor(target);
|
|
1842
|
+
if (!descriptor) {
|
|
1843
|
+
return unsupportedAdapter(target, flags);
|
|
1284
1844
|
}
|
|
1285
1845
|
if (flags.fromArchiveProvided && (!flags.fromArchive || flags.fromArchive.startsWith("--"))) {
|
|
1286
1846
|
return invalidArchivePath("Missing required value for --from-archive.", flags);
|
|
@@ -1290,7 +1850,7 @@ async function handleInit(flags) {
|
|
|
1290
1850
|
}
|
|
1291
1851
|
|
|
1292
1852
|
const info = packageInfo();
|
|
1293
|
-
|
|
1853
|
+
let plan = buildInitPlan(flags, descriptor);
|
|
1294
1854
|
if (plan.errors.length > 0) {
|
|
1295
1855
|
const result = envelope("init", flags, {
|
|
1296
1856
|
status: "error",
|
|
@@ -1298,11 +1858,15 @@ async function handleInit(flags) {
|
|
|
1298
1858
|
actions: plan.actions,
|
|
1299
1859
|
artifacts: plan.artifacts,
|
|
1300
1860
|
errors: plan.errors,
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1861
|
+
...(flags.writeState
|
|
1862
|
+
? {
|
|
1863
|
+
planned_manifest: {
|
|
1864
|
+
path: "rigorloop.yaml",
|
|
1865
|
+
content: plan.manifest,
|
|
1866
|
+
},
|
|
1867
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1868
|
+
}
|
|
1869
|
+
: {}),
|
|
1306
1870
|
});
|
|
1307
1871
|
if (flags.json) {
|
|
1308
1872
|
writeJson(result);
|
|
@@ -1311,21 +1875,40 @@ async function handleInit(flags) {
|
|
|
1311
1875
|
}
|
|
1312
1876
|
return exitCodeForResult({ ...result, exit_class: "invalid_usage" });
|
|
1313
1877
|
}
|
|
1314
|
-
|
|
1878
|
+
const deferrableRootBlockers =
|
|
1879
|
+
descriptor.name === "opencode" &&
|
|
1880
|
+
plan.blockers.length > 0 &&
|
|
1881
|
+
plan.blockers.every((blocker) => String(blocker.path ?? "").startsWith(".opencode/commands"));
|
|
1882
|
+
if (plan.blockers.length > 0 && !deferrableRootBlockers) {
|
|
1315
1883
|
return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
|
|
1316
1884
|
}
|
|
1885
|
+
if (flags.dryRun) {
|
|
1886
|
+
const stateSafety = existingStateSafetyBlocker(descriptor);
|
|
1887
|
+
if (stateSafety) {
|
|
1888
|
+
return writeBlockedResult(flags, plan, stateSafety.message, [stateSafety], exitClassForBlockers([stateSafety]));
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1317
1891
|
|
|
1318
|
-
const archiveWork = await archiveWorkForInit(flags, info);
|
|
1319
|
-
if (archiveWork.artifact) {
|
|
1320
|
-
plan
|
|
1321
|
-
|
|
1892
|
+
const archiveWork = await archiveWorkForInit(flags, info, descriptor);
|
|
1893
|
+
if (archiveWork.artifact && !archiveWork.blocker && !archiveWork.error) {
|
|
1894
|
+
plan = buildInitPlan(flags, descriptor, archiveWork.artifact);
|
|
1895
|
+
if (plan.errors.length > 0) {
|
|
1896
|
+
return writeValidationErrorResult(flags, plan, plan.errors[0]);
|
|
1897
|
+
}
|
|
1898
|
+
if (plan.blockers.length > 0) {
|
|
1899
|
+
return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
|
|
1900
|
+
}
|
|
1322
1901
|
}
|
|
1323
1902
|
if (archiveWork.entries) {
|
|
1324
1903
|
const conflict = generatedOutputConflictBlocker(archiveWork.entries);
|
|
1325
1904
|
if (conflict) {
|
|
1326
1905
|
return writeBlockedResult(flags, plan, conflict.message, [conflict], "mutation_conflict");
|
|
1327
1906
|
}
|
|
1328
|
-
const
|
|
1907
|
+
const stateSafety = existingStateSafetyBlocker(descriptor, archiveWork.artifact);
|
|
1908
|
+
if (stateSafety) {
|
|
1909
|
+
return writeBlockedResult(flags, plan, stateSafety.message, [stateSafety], exitClassForBlockers([stateSafety]));
|
|
1910
|
+
}
|
|
1911
|
+
const drift = flags.writeState ? firstLockfileDriftBlocker() : undefined;
|
|
1329
1912
|
if (drift) {
|
|
1330
1913
|
return writeBlockedResult(
|
|
1331
1914
|
flags,
|
|
@@ -1335,11 +1918,11 @@ async function handleInit(flags) {
|
|
|
1335
1918
|
drift.code === "overwrite-refused" ? "mutation_conflict" : "blocked",
|
|
1336
1919
|
);
|
|
1337
1920
|
}
|
|
1338
|
-
const installedTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, { allowMissingOrEmpty: true });
|
|
1921
|
+
const installedTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, descriptor, { allowMissingOrEmpty: true });
|
|
1339
1922
|
if (installedTree.error) {
|
|
1340
1923
|
return writeValidationErrorResult(flags, plan, installedTree.error);
|
|
1341
1924
|
}
|
|
1342
|
-
addArchiveActions(plan, archiveWork.entries);
|
|
1925
|
+
addArchiveActions(plan, archiveWork.entries, descriptor);
|
|
1343
1926
|
}
|
|
1344
1927
|
|
|
1345
1928
|
if (archiveWork.blocker) {
|
|
@@ -1353,7 +1936,7 @@ async function handleInit(flags) {
|
|
|
1353
1936
|
}
|
|
1354
1937
|
|
|
1355
1938
|
if (!flags.dryRun) {
|
|
1356
|
-
const manifestAction = plan.actions.find((action) => action.path === "rigorloop.yaml");
|
|
1939
|
+
const manifestAction = flags.writeState ? plan.actions.find((action) => action.path === "rigorloop.yaml") : undefined;
|
|
1357
1940
|
const directoryActions = plan.actions.filter((action) => action.type === "create-dir" && action.status === "pending");
|
|
1358
1941
|
for (const directoryAction of directoryActions) {
|
|
1359
1942
|
mkdirSync(resolve(process.cwd(), directoryAction.path));
|
|
@@ -1370,7 +1953,7 @@ async function handleInit(flags) {
|
|
|
1370
1953
|
const pendingCopyPaths = new Set(
|
|
1371
1954
|
plan.actions.filter((action) => action.type === "copy" && action.status === "pending").map((action) => action.path),
|
|
1372
1955
|
);
|
|
1373
|
-
writeArchiveEntries(archiveWork.entries.filter((entry) => pendingCopyPaths.has(entry.name)));
|
|
1956
|
+
writeArchiveEntries(archiveWork.entries.filter((entry) => pendingCopyPaths.has(entry.name)), descriptor);
|
|
1374
1957
|
for (const action of plan.actions.filter((action) => action.type === "copy" && action.status === "pending")) {
|
|
1375
1958
|
action.status = "done";
|
|
1376
1959
|
plan.artifacts.find((artifact) => artifact.path === action.path).status = "created";
|
|
@@ -1388,11 +1971,15 @@ async function handleInit(flags) {
|
|
|
1388
1971
|
partial_state: "scaffold files may have been written; adapter files may be incomplete.",
|
|
1389
1972
|
},
|
|
1390
1973
|
],
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1974
|
+
...(flags.writeState
|
|
1975
|
+
? {
|
|
1976
|
+
planned_manifest: {
|
|
1977
|
+
path: "rigorloop.yaml",
|
|
1978
|
+
content: plan.manifest,
|
|
1979
|
+
},
|
|
1980
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1981
|
+
}
|
|
1982
|
+
: {}),
|
|
1396
1983
|
});
|
|
1397
1984
|
if (flags.json) {
|
|
1398
1985
|
writeJson(result);
|
|
@@ -1402,12 +1989,12 @@ async function handleInit(flags) {
|
|
|
1402
1989
|
return exitCodeForResult({ ...result, exit_class: "internal" });
|
|
1403
1990
|
}
|
|
1404
1991
|
}
|
|
1405
|
-
if (archiveWork.entries) {
|
|
1992
|
+
if (flags.writeState && archiveWork.entries) {
|
|
1406
1993
|
const lockfileAction = plan.actions.find((action) => action.path === LOCKFILE_PATH);
|
|
1407
1994
|
const lockfileArtifact = plan.artifacts.find((artifact) => artifact.path === LOCKFILE_PATH);
|
|
1408
1995
|
if (lockfileAction?.status === "pending") {
|
|
1409
1996
|
const lockfilePreviouslyExists = existsSync(resolve(process.cwd(), LOCKFILE_PATH));
|
|
1410
|
-
const verifiedInstalledTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact);
|
|
1997
|
+
const verifiedInstalledTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, descriptor);
|
|
1411
1998
|
if (verifiedInstalledTree.error) {
|
|
1412
1999
|
return writeValidationErrorResult(flags, plan, verifiedInstalledTree.error);
|
|
1413
2000
|
}
|
|
@@ -1416,15 +2003,15 @@ async function handleInit(flags) {
|
|
|
1416
2003
|
plan.source,
|
|
1417
2004
|
plan.manifest,
|
|
1418
2005
|
archiveWork.artifact,
|
|
1419
|
-
|
|
1420
|
-
|
|
2006
|
+
verifiedInstalledTree.rootHashes,
|
|
2007
|
+
descriptor,
|
|
1421
2008
|
);
|
|
1422
2009
|
writeFileSync(resolve(process.cwd(), LOCKFILE_PATH), serializeLockfile(lockfile), "utf8");
|
|
1423
2010
|
plan.planned_lockfile = lockfile;
|
|
1424
2011
|
lockfileAction.status = "done";
|
|
1425
2012
|
lockfileAction.reason = lockfilePreviouslyExists
|
|
1426
|
-
?
|
|
1427
|
-
:
|
|
2013
|
+
? `Updated durable lockfile for verified ${descriptor.displayName} adapter install.`
|
|
2014
|
+
: `Wrote durable lockfile for verified ${descriptor.displayName} adapter install.`;
|
|
1428
2015
|
if (lockfileArtifact) {
|
|
1429
2016
|
lockfileArtifact.status = lockfilePreviouslyExists ? "updated" : "created";
|
|
1430
2017
|
}
|
|
@@ -1432,22 +2019,29 @@ async function handleInit(flags) {
|
|
|
1432
2019
|
}
|
|
1433
2020
|
}
|
|
1434
2021
|
|
|
1435
|
-
const warnings =
|
|
2022
|
+
const warnings = initWarnings(descriptor, archiveWork.artifact);
|
|
1436
2023
|
const result = envelope("init", flags, {
|
|
1437
2024
|
status: warnings.length > 0 ? "warning" : "success",
|
|
1438
2025
|
summary: flags.dryRun
|
|
1439
2026
|
? "RigorLoop init dry run completed. No files were written."
|
|
1440
2027
|
: archiveWork.entries
|
|
1441
|
-
|
|
1442
|
-
:
|
|
2028
|
+
? `RigorLoop initialized with verified ${descriptor.displayName} target support.`
|
|
2029
|
+
: `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
|
|
1443
2030
|
actions: plan.actions,
|
|
1444
2031
|
artifacts: plan.artifacts,
|
|
1445
2032
|
warnings,
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
2033
|
+
state_files: flags.writeState
|
|
2034
|
+
? { action: flags.dryRun ? "planned" : "written" }
|
|
2035
|
+
: { action: "skipped", reason: "Use --write-state to write rigorloop.yaml and rigorloop.lock." },
|
|
2036
|
+
...(flags.writeState
|
|
2037
|
+
? {
|
|
2038
|
+
planned_manifest: {
|
|
2039
|
+
path: "rigorloop.yaml",
|
|
2040
|
+
content: plan.manifest,
|
|
2041
|
+
},
|
|
2042
|
+
planned_lockfile: plan.planned_lockfile,
|
|
2043
|
+
}
|
|
2044
|
+
: {}),
|
|
1451
2045
|
});
|
|
1452
2046
|
|
|
1453
2047
|
if (flags.json) {
|
|
@@ -1457,10 +2051,13 @@ async function handleInit(flags) {
|
|
|
1457
2051
|
? ["RigorLoop init dry run completed.", "No files were written."]
|
|
1458
2052
|
: [
|
|
1459
2053
|
archiveWork.entries
|
|
1460
|
-
?
|
|
1461
|
-
:
|
|
1462
|
-
|
|
2054
|
+
? `RigorLoop initialized with verified ${descriptor.displayName} target support.`
|
|
2055
|
+
: `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
|
|
2056
|
+
flags.writeState ? "rigorloop.yaml and rigorloop.lock were written." : "State files were not written; use --write-state to write them.",
|
|
1463
2057
|
];
|
|
2058
|
+
for (const warning of warnings) {
|
|
2059
|
+
lines.push(`warning ${warning.code}: ${warning.message}`);
|
|
2060
|
+
}
|
|
1464
2061
|
writeHuman(`${lines.join("\n")}\n`, flags);
|
|
1465
2062
|
}
|
|
1466
2063
|
return exitCodeForResult({ ...result, exit_class: "success" });
|
|
@@ -1483,7 +2080,7 @@ async function main() {
|
|
|
1483
2080
|
return handleVersion(flags);
|
|
1484
2081
|
}
|
|
1485
2082
|
if (command === "init") {
|
|
1486
|
-
return handleInit(flags);
|
|
2083
|
+
return handleInit(flags, positional.slice(1));
|
|
1487
2084
|
}
|
|
1488
2085
|
if (command === "new-change") {
|
|
1489
2086
|
return handleNewChange(rawArgs.slice(rawArgs.indexOf("new-change") + 1));
|